Index: waeup/trunk/src/waeup/README.txt
===================================================================
--- waeup/trunk/src/waeup/README.txt	(revision 3551)
+++ waeup/trunk/src/waeup/README.txt	(revision 4789)
@@ -5,4 +5,13 @@
 
 A portal software for student registration.
+
+Prerequisites
+=============
+
+Before we can work with the environment, we have to register all the
+utilities, adapters, etc. We grok the `waeup` package to do that::
+
+  >>> import grok
+  >>> grok.testing.grok('waeup')
 
 Universities
@@ -22,7 +31,48 @@
 
   >>> u.name
-  u'Unnamed'
+  u'Sample University'
 
-Universities are basically also containers for faculties.
+Universities are basically also containers for faculties, students and
+hostels::
+
+  >>> u['faculties']
+  <waeup.university.facultycontainer.FacultyContainer object at 0x...>
+
+  >>> u['students']
+  <waeup.student.studentcontainer.StudentContainer object at 0x...>
+
+  >>> u['hostels']
+  <waeup.hostel.hostelcontainer.HostelContainer object at 0x...>
+
+We can export universities. For this we lookup an appropriate exporter
+first::
+
+  >>> from waeup.interfaces import IWAeUPExporter
+  >>> exporter = IWAeUPExporter(u)
+  >>> exporter
+  <waeup.utils.importexport.Exporter object at 0x...>
+
+Now we can trigger the export::
+
+  >>> exporter.export()
+  <cStringIO.StringO object at 0x...>
+
+We can also get an XML representation as file. If we do not provide a
+filepath, we will get an instance of `cStringIO.StringIO`, i.e. a
+memory file::
+
+  >>> from waeup.interfaces import IWAeUPXMLExporter
+  >>> exporter = IWAeUPXMLExporter(u)
+  >>> f = exporter.export()
+  >>> f
+  <cStringIO.StringO object at 0x...>
+
+  >>> print f.read()
+  <?xml version="1.0" encoding="utf-8" ?>
+  <pickle>
+    <initialized_object id="...">
+  ...
+  </pickle>
+
 
 Faculties
@@ -34,12 +84,12 @@
 We can create faculties easily::
 
-  >>> from waeup.app import Faculty
+  >>> from waeup.university.faculty import Faculty
   >>> f = Faculty()
   >>> f
-  <waeup.app.Faculty object at 0x...>
+  <waeup.university.faculty.Faculty object at 0x...>
 
 Also faculties want to be named::
 
-  >>> f.name
+  >>> f.title
   u'Unnamed Faculty'
 
Index: waeup/trunk/src/waeup/__init__.py
===================================================================
--- waeup/trunk/src/waeup/__init__.py	(revision 3551)
+++ waeup/trunk/src/waeup/__init__.py	(revision 4789)
@@ -0,0 +1,13 @@
+import grok
+from waeup.interfaces import IWAeUPObject
+from zope.annotation.attribute import AttributeAnnotations
+from zope.annotation.interfaces import IAnnotations
+
+class WAeUPAttributeAnnotations(AttributeAnnotations, grok.Adapter):
+    """An adapter to IAnnotations for any WAeUPObject.
+
+    Providing this adapter, each WAeUP object becomes (attribute)
+    annotatable.
+    """
+    grok.provides(IAnnotations)
+    grok.context(IWAeUPObject)
Index: waeup/trunk/src/waeup/app.py
===================================================================
--- waeup/trunk/src/waeup/app.py	(revision 3551)
+++ waeup/trunk/src/waeup/app.py	(revision 4789)
@@ -1,23 +1,31 @@
 import grok
-from grok import index
-from zope.interface import Interface
-from interfaces import IUniversity
-from setup import app_setup
-from viewlets import MainArea
+from zope.app.authentication.authentication import PluggableAuthentication
+from zope.app.security.interfaces import IAuthentication
+from zope.component import createObject
+
+from waeup.interfaces import IUniversity, ICSVDataReceivers
+from waeup.authentication import setup_authentication
+from waeup.datacenter import DataCenter
+from waeup.users import UserContainer
 
 class University(grok.Application, grok.Container):
-    grok.implements(IUniversity)
-
-    def __init__(self, name=u'Unnamed', **kw):
+    """A university.
+    """
+    grok.implements(IUniversity, ICSVDataReceivers)
+    # Setup authentication for this app. Note: this is only
+    # initialized, when a new instance of this app is created.
+    grok.local_utility(
+        PluggableAuthentication, provides = IAuthentication,
+        setup = setup_authentication)
+    
+    def __init__(self, name=u'Sample University', **kw):
         super(University, self).__init__(**kw)
         self.name = name
-        app_setup.setup(self)
+        self.setup()
 
-class Content(grok.Viewlet):
-    grok.viewletmanager(MainArea)
-    grok.context(IUniversity)
-
-
-class Edit(grok.EditForm):
-    grok.context(University)
-    grok.AutoFields(IUniversity)
+    def setup(self):
+        self['students'] = createObject(u'waeup.StudentContainer')
+        self['hostels'] = createObject(u'waeup.HostelContainer')
+        self['faculties'] = createObject(u'waeup.FacultyContainer')
+        self['users'] = UserContainer()
+        self['datacenter'] = DataCenter()
Index: waeup/trunk/src/waeup/app.txt
===================================================================
--- waeup/trunk/src/waeup/app.txt	(revision 4789)
+++ waeup/trunk/src/waeup/app.txt	(revision 4789)
@@ -0,0 +1,63 @@
+:mod:`waeup.app` -- central components for a WAeUP portal
+*********************************************************
+
+:Test-Layer: unit
+
+.. module:: waeup.app
+
+.. class:: University
+
+  The main WAeUP application object is given with
+  :class:`University`. It provides the main containers as faculties,
+  hostels, etc.
+
+  .. attribute:: name
+
+     A string. The name of a university.
+
+  .. attribute:: faculties
+
+     An arbitrary object containing "faculties". In the case of
+     `University` it is a container of type
+     `waeup.interfaces.IFacultyContainer`.
+
+
+Creating `University` instances
+===============================
+
+As :class:`University` 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:
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+Now we can import the :class:`University` class and create an
+instance:
+
+    >>> from waeup.app import University
+    >>> myuniversity = University()
+    >>> myuniversity
+    <waeup.app.University object at 0x...>
+
+Instances of `University` comply with the interface
+`waeup.interfaces.IUniversity`:
+
+    >>> from zope.interface.verify import verifyClass
+    >>> from waeup.interfaces import IUniversity
+    >>> verifyClass(IUniversity, University)
+    True
+
+A freshly created instance provides the attributes promised by the
+interface:
+
+    >>> from waeup.app import University
+    >>> myuniversity = University()
+    >>> myuniversity.name
+    u'Sample University'
+
+    >>> myuniversity['faculties']
+    <waeup.university.facultycontainer.FacultyContainer object at 0x...>
Index: waeup/trunk/src/waeup/authentication.py
===================================================================
--- waeup/trunk/src/waeup/authentication.py	(revision 4789)
+++ waeup/trunk/src/waeup/authentication.py	(revision 4789)
@@ -0,0 +1,143 @@
+"""Authentication for WAeUP portals.
+"""
+import grok
+from zope.app.authentication.session import SessionCredentialsPlugin
+from zope.app.authentication.interfaces import (ICredentialsPlugin,
+                                                IAuthenticatorPlugin,
+                                                IPrincipalInfo,
+                                                IPasswordManager)
+from zope.component import getUtility
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.securitypolicy.principalrole import principalRoleManager
+from waeup.interfaces import IUserAccount
+import waeup.permissions
+
+def setup_authentication(pau):
+    """Set up plugguble authentication utility.
+
+    Sets up an IAuthenticatorPlugin and
+    ICredentialsPlugin (for the authentication mechanism)
+    """
+    pau.credentialsPlugins = ['No Challenge if Authenticated', 'credentials']
+    pau.authenticatorPlugins = ['users']
+
+class WAeUPSessionCredentialsPlugin(grok.GlobalUtility,
+                                    SessionCredentialsPlugin):
+    grok.provides(ICredentialsPlugin)
+    grok.name('credentials')
+
+    loginpagename = 'login'
+    loginfield = 'form.login'
+    passwordfield = 'form.password'
+
+class PrincipalInfo(object):
+    grok.implements(IPrincipalInfo)
+
+    def __init__(self, id, title, description):
+        self.id = id
+        self.title = title
+        self.description = description
+        self.credentialsPlugin = None
+        self.authenticatorPlugin = None
+
+class Account(grok.Model):
+    grok.implements(IUserAccount)
+
+
+    def __init__(self, name, password, title=None, description=None,
+                 roles = []):
+        self.name = name
+        if title is None:
+            title = name
+        if description is None:
+            description = title
+        self.title = title
+        self.description = description
+        self.setPassword(password)
+        self.setRoles(roles)
+
+    def setPassword(self, password):
+        passwordmanager = getUtility(IPasswordManager, 'SHA1')
+        self.password = passwordmanager.encodePassword(password)
+
+    def checkPassword(self, password):
+        passwordmanager = getUtility(IPasswordManager, 'SHA1')
+        return passwordmanager.checkPassword(self.password, password)
+
+    def getRoles(self):
+        prm = self._getPrincipalRoleManager()
+        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
+                 if x[0].startswith('waeup.')]
+        return roles
+    
+    def setRoles(self, roles):
+        prm = self._getPrincipalRoleManager()
+        
+        old_roles = self.getRoles()
+        for role in old_roles:
+            # Remove old roles, not to be set now...
+            if role.startswith('waeup.') and role not in roles:
+                prm.unsetRoleForPrincipal(role, self.name)
+
+        for role in roles:
+            prm.assignRoleToPrincipal(role, self.name)
+
+    roles = property(getRoles, setRoles)
+
+    def _getPrincipalRoleManager(self):
+        portal = grok.getSite()
+        if portal is not None:
+            return IPrincipalRoleManager(portal)
+        return principalRoleManager
+        
+    
+class UserAuthenticatorPlugin(grok.GlobalUtility):
+    grok.provides(IAuthenticatorPlugin)
+    grok.name('users')
+
+    def authenticateCredentials(self, credentials):
+        if not isinstance(credentials, dict):
+            return None
+        if not ('login' in credentials and 'password' in credentials):
+            return None
+        account = self.getAccount(credentials['login'])
+
+        if account is None:
+            return None
+        if not account.checkPassword(credentials['password']):
+            return None
+        return PrincipalInfo(id=account.name,
+                             title=account.title,
+                             description=account.description)
+
+    def principalInfo(self, id):
+        account = self.getAccount(id)
+        if account is None:
+            return None
+        return PrincipalInfo(id=account.name,
+                             title=account.title,
+                             description=account.description)
+
+    def getAccount(self, login):
+        # ... look up the account object and return it ...
+        usercontainer = self.getUserContainer()
+        if usercontainer is None:
+            return
+        return usercontainer.get(login, None)
+
+    def addAccount(self, account):
+        usercontainer = self.getUserContainer()
+        if usercontainer is None:
+            return
+        # XXX: complain if name already exists...
+        usercontainer.addAccount(account)
+
+    def addUser(self, name, password, title=None, description=None):
+        usercontainer = self.getUserContainer()
+        if usercontainer is None:
+            return
+        usercontainer.addUser(name, password, title, description)
+        
+    def getUserContainer(self):
+        site = grok.getSite()
+        return site['users']
Index: waeup/trunk/src/waeup/authentication.txt
===================================================================
--- waeup/trunk/src/waeup/authentication.txt	(revision 4789)
+++ waeup/trunk/src/waeup/authentication.txt	(revision 4789)
@@ -0,0 +1,89 @@
+WAeUP portal authentication
+***************************
+
+:Test-Layer: functional
+
+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 waeup.app import University
+  >>> root = getRootFolder()
+  >>> u = University()
+  >>> root['app'] = u
+
+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.authentication import Account
+  >>> alice = Account('alice', 'alicesecret')
+  >>> root['app']['users'].addAccount(alice)
+
+See ``users.txt`` for details about the UserContainer we use here.
+
+
+Logging in via side bar
+=======================
+
+We can access the front page without restrictions:
+
+  >>> browser.open('http://localhost/app')
+  >>> print browser.headers['Status']
+  200 Ok
+
+There is a login form on the front page sidebar:
+
+  >>> '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 = '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()
+
+Now we are logged out again:
+
+  >>> 'form.login' in browser.contents
+  True
Index: waeup/trunk/src/waeup/basecontainer.py
===================================================================
--- waeup/trunk/src/waeup/basecontainer.py	(revision 3551)
+++ 	(revision )
@@ -1,33 +1,0 @@
-import grok
-from baseitem import BaseItem
-from waeup.viewlets import MainArea
-
-class BaseContainer(BaseItem, grok.Container):
-    """
-    The base container. This is a stub for now. It can have
-    base methods and templates attached to it
-    """
-    
-    name = u"BaseContainer"
-    childClass = None
-    specialActions = []
-    
-class Content(grok.Viewlet):
-    grok.viewletmanager(MainArea)
-    """
-    Base viewlet
-    """
-    
-class Add(grok.AddForm):
-    #form_fields = grok.AutoFields(self.context.childClass)
-#DOESN'T WORK    grok.template("../basecontainer_templates/add.pt")
-
-    def update(self):
-        self.form_fields = grok.AutoFields(self.context.childClass)
-        grok.AddForm.update(self)
-
-    @grok.action('Add Item')
-    def addItem(self, name, *args, **kw):
-        item = self.context.childClass(name=name)
-        self.context[name] = item
-        self.redirect(self.url(item))
Index: waeup/trunk/src/waeup/baseitem.py
===================================================================
--- waeup/trunk/src/waeup/baseitem.py	(revision 3551)
+++ 	(revision )
@@ -1,17 +1,0 @@
-import grok
-from waeup.viewlets import MainArea
-
-class BaseItem(grok.Model):
-    """
-    Base item
-    """
-
-class Edit(grok.EditForm):
-    
-    def update(self):
-        self.form_fields = grok.AutoFields(self.context.__class__)
-        grok.EditForm.update(self)
-
-class Content(grok.Viewlet):
-    grok.viewletmanager(MainArea)
-
Index: waeup/trunk/src/waeup/browser.txt
===================================================================
--- waeup/trunk/src/waeup/browser.txt	(revision 3551)
+++ waeup/trunk/src/waeup/browser.txt	(revision 4789)
@@ -1,4 +1,6 @@
-The waeup package
-********************
+Browsing the WAeUP portal
+*************************
+
+Here we visit all parts of a WAeUP portal using a browser.
 
 :Test-Layer: functional
@@ -34,31 +36,47 @@
   >>> browser.open('http://localhost/myuniversity')
   >>> print browser.contents
-  <html>...
-  ...<h1>Congratulations!</h1>...
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Welcome to the...
   ...
 
 We can also get an edit view of a university::
 
-  >>> browser.open('http://localhost/myuniversity/edit')
-  >>> print browser.contents
-  <html>...
-  ...<form action="http://localhost/myuniversity/edit"
+  >>> browser.open('http://localhost/myuniversity/manage')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<form action="http://localhost/myuniversity/manage"
   ...
 
 The edit form contains the default value for the university name::
 
-  >>> 'Unnamed' in browser.contents
+  >>> 'Sample University' in browser.contents
   True
+
+We can export a university as XML::
+
+  >>> browser.open('http://localhost/myuniversity/export.xml')
+  >>> print browser.contents
+  <?xml version="1.0" encoding="utf-8" ?>
+  <pickle>
+  ...
+  </pickle>
+
+  >>> print browser.headers
+  Status: 200 Ok
+  Content-Length: ...
+  Content-Type: text/xml; charset=UTF-8
+  X-Powered-By: Zope (www.zope.org), Python (www.python.org)
+
 
 Faculties
 =========
 
-Wie can add faculties, by calling and submitting an add form of an
-university. The add form can be retrieved by calling the
-``addfaculty`` view of an university::
-
-  >>> browser.open('http://localhost/myuniversity/addfaculty')
-  >>> print browser.contents
-  <html>...
+Faculties are stored in a special container of `IUniversity`
+instances. The container is called ``faculties`` and provides an
+add-form to add new faculties::
+
+  >>> browser.open('http://localhost/myuniversity/faculties/add')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
   ...<span class="required">*</span><span>Name of Faculty</span>
   ...
@@ -66,25 +84,878 @@
 We fill in a new name for our new faculty::
 
-  >>> ctrl = browser.getControl(name='form.name')
+  >>> ctrl = browser.getControl(name='form.title')
   >>> ctrl.value = 'TestFac'
 
+Furthermore we add a prefix and a code (kind of abbreviation):
+
+  >>> browser.getControl(name='form.code').value = 'TF'
+
 Finally we click on 'Add Faculty' to add the new thing::
 
-  >>> browser.getControl('Add Faculty').click()
-  >>> #print browser.contents
+  >>> browser.getControl('Add faculty').click()
 
 We can view a faculty by browsing a URL like this::
 
-  >>> browser.open('http://localhost/myuniversity/TestFac')
+  >>> browser.open('http://localhost/myuniversity/faculties/TF')
 
 Afterwards, the faculty should be visible:
 
-  >>> browser.open('http://localhost/myuniversity/')
-  >>> print browser.contents
-  <html...
-  ...<h2>Your faculties:</h2>
-  ...<a href="http://localhost/myuniversity/TestFac">TestFac</a>
-  ...
-
-
-
+  >>> browser.open('http://localhost/myuniversity/faculties')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h2>Registered Faculties</h2>
+  ...<td><a href="http://localhost/myuniversity/faculties/TF">TF</a></td><td>Faculty of TestFac</td>
+  ...
+
+We can 'visit' each faculty by clicking on the appropriate link:
+
+  >>> browser.getLink('TF').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h2>Faculty of TestFac (TF)</h2>...
+  ...
+
+Modifying faculties
+-------------------
+
+A faculty can directly be reached by its code:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF')
+
+We can change the settings for a faculty by clicking on the
+provided 'Edit  settings' button:
+
+  >>> browser.getLink('Edit settings').click()
+
+Let's set a new title and save the form:
+
+  >>> browser.getControl(name='form.title').value = "My test faculty"
+  >>> browser.getControl(name='form.actions.save').click()
+
+Clicking 'Save' we will stay on the settings form. So we can change
+the department again. This time we will return to the overview page
+afterwards:
+
+  >>> browser.getControl(name='form.title').value = "My renamed faculty"
+  >>> ctrl = browser.getControl("Save and return")
+  >>> ctrl.click()
+
+If we go to the settings page and click ``Cancel`` nothing will be
+changed:
+
+  >>> browser.getLink('Edit settings').click()
+  >>> browser.getControl(name='form.title').value = "Blah"
+  >>> browser.getControl('Cancel').click()
+
+Our faculty was indeed renamed to ``My renamed faculty`` and not to
+``Blah``:
+
+  >>> browser.open('http://localhost/myuniversity/faculties')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h2>Registered Faculties</h2>
+  ...<td><a href="http://localhost/myuniversity/faculties/TF">TF</a></td><td>Faculty of My renamed faculty</td>
+  ...
+
+
+Departments
+===========
+
+Adding departments
+------------------
+
+Departments are stored in :class:`IFaculty` instances with their code
+as key. Faculties therefore are also department containers. Faculties
+provides an add-form to add new departments:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/add')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<span class="required">*</span><span>Name of Department</span>
+  ...
+
+We fill in a new name for our new department:
+
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'TestDept'
+
+Furthermore we add a code (kind of abbreviation):
+
+  >>> browser.getControl(name='form.code').value = 'TD'
+
+Finally we click on 'Add Department' to add the new thing::
+
+  >>> browser.getControl('Add department').click()
+
+If we try to register a department under the same code twice we will
+get an error:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/add')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another TestDept with same code'
+  >>> browser.getControl(name='form.code').value = 'TD'
+  >>> browser.getControl('Add department').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<div>The name chosen already exists in the database</div>
+  ...
+
+We can view a department by browsing a URL like this:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+
+Afterwards, the department should be visible:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h2>Registered Departments:</h2>
+  ...<td><a href="http://localhost/myuniversity/faculties/TF/TD">TD</a></td><td>Department of TestDept</td>
+  ...
+
+
+Modifying departments
+---------------------
+
+We can change the settings for a department by clicking on the
+provided 'Edit settings' button:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+  >>> browser.getLink('Edit settings').click()
+
+Let's set a new title and save the form:
+
+  >>> browser.getControl(name='form.title').value = "My test dept"
+  >>> browser.getControl(name='form.actions.save').click()
+
+Clicking 'Save' we will stay on the settings form. So we can change
+the department again. This time we will return to the overview page
+afterwards:
+
+  >>> browser.getControl(name='form.title').value = "My renamed dept"
+  >>> ctrl = browser.getControl("Save and return")
+  >>> ctrl.click()
+
+If we go to the settings page and click ``Cancel`` nothing will be
+changed:
+
+  >>> browser.getLink('Edit settings').click()
+  >>> browser.getControl(name='form.title').value = "Blah"
+  >>> browser.getControl('Cancel').click()
+
+Our department was indeed renamed to ``My renamed dept`` and not to
+``Blah``:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h2>Registered Departments:</h2>
+  ...<td><a href="http://localhost/myuniversity/faculties/TF/TD">TD</a></td><td>Department of My renamed dept</td>
+  ...
+
+
+Courses
+=======
+
+Once we have a department, we can add courses.
+
+Adding courses
+--------------
+
+Courses are stored in :class:`ICourseContainer` instances with their
+code as key. CourseContainers are normally availabe as `course`
+attribute of :class:`waeup.university.department.Department`
+instances.
+
+To ease the life of users we do not require to browse the
+coursecontainers (which have a rather flat user interface), but
+provide adding of courses in department views.
+
+Each department provides a ``Add course`` action button near top.
+
+Departments provide an add-form to add new courses:
+
+  >>> dept_url = 'http://localhost/myuniversity/faculties/TF/TD'
+  >>> browser.open(dept_url + '/addcourse')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<span class="required">*</span><span>Title of course</span>
+  ...
+
+We fill in a name for our new course:
+
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'My Course 1'
+
+Furthermore we add a code (kind of abbreviation):
+
+  >>> browser.getControl(name='form.code').value = 'COURSE1'
+
+This course will take place in the the first semester, so we set the
+`semester` value to 1:
+
+  >>> ctrl = browser.getControl(name='form.semester')
+  >>> ctrl.options
+  ['0', '1', '2', '3']
+
+  >>> ctrl.displayOptions
+  ['N/A', 'First Semester', 'Second Semester', 'Combined']
+
+  >>> ctrl.value = ['1']
+
+Finally, we create the course:
+
+  >>> browser.getControl('Add course').click()
+
+If we try to register a course under the same code twice we will
+get an error:
+
+  >>> browser.open(dept_url + '/addcourse')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another course with same code'
+  >>> browser.getControl(name='form.code').value = 'COURSE1'
+  >>> browser.getControl('Add course').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<div>The code chosen already exists in the database</div>
+  ...
+
+Our course will be linked under the code on the department page:
+
+  >>> browser.open(dept_url)
+  >>> browser.getLink('COURSE1').click()
+  >>> browser.url
+  'http://localhost/myuniversity/faculties/TF/TD/courses/COURSE1'
+
+Before we really add a course we can cancel the action and will be
+redirected to the department page:
+
+  >>> browser.open(dept_url + '/addcourse')
+  >>> browser.getControl('Cancel').click()
+  >>> browser.url
+  'http://localhost/myuniversity/faculties/TF/TD'
+
+
+Modifying courses
+-----------------
+
+We can change the settings for a course by clicking on the provided
+'Edit settings' link:
+
+  >>> browser.open(dept_url + '/courses/COURSE1')
+  >>> browser.getLink('Edit settings').click()
+
+When modifying a course, we cannot change the code any more:
+
+  >>> browser.getControl(name='form.code')
+  Traceback (most recent call last):
+  ...
+  LookupError: name 'form.code'
+
+Let's set a new title and save the form:
+
+  >>> browser.getControl(name='form.title').value = "My test course"
+  >>> browser.getControl(name='form.actions.save').click()
+
+Clicking 'Save' we will stay on the settings form. So we can change
+the course again. This time we will return to the overview page
+afterwards:
+
+  >>> browser.getControl(name='form.title').value = "My renamed course" 
+  >>> ctrl = browser.getControl("Save and return")
+  >>> ctrl.click()
+
+If we go to the settings page and click ``Cancel`` nothing will be
+changed:
+
+  >>> browser.getLink('Edit settings').click()
+  >>> browser.getControl(name='form.title').value = "Blah"
+  >>> browser.getControl('Cancel').click()
+
+Our course was indeed renamed to ``My renamed course`` and not to
+``Blah``:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<div name="thecoursetable">
+  ...<td>My renamed course</td>...
+  ...
+
+
+Deleting courses
+----------------
+
+We can delete courses by browsing the containing department and
+clicking on the appropriate 'Delete' button. As we have only one
+course, there is only one 'Delete' button yet:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+  >>> 'My renamed course' in browser.contents
+  True
+
+  >>> browser.getControl('Delete').click()
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+  >>> 'My renamed course' in browser.contents
+  False
+
+
+Certificates
+============
+
+Once we have a department, we can add also certificats.
+
+Adding certificates
+-------------------
+
+Certificates are stored in :class:`ICertificateContainer` instances
+with their code as key. CertificateContainers are normally availabe as
+`certificates` attribute of
+:class:`waeup.university.department.Department` instances.
+
+To ease the life of users we do not require to browse the
+certificatecontainers (which have in fact no user interface), but
+provide adding of certificates in department views.
+
+Each department provides a ``Add certificate`` action button near top.
+
+Departments provide an add-form to add new certificates:
+
+  >>> dept_url = 'http://localhost/myuniversity/faculties/TF/TD'
+  >>> browser.open(dept_url + '/addcertificate')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<span class="required">*</span><span>title</span>
+  ...
+
+We fill in a name for our new cert:
+
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'My Certificate 1'
+
+Furthermore we add a code (kind of abbreviation):
+
+  >>> browser.getControl(name='form.code').value = 'CERT1'
+
+Set the remaining required fields:
+
+  >>> browser.getControl(name='form.category').value = 'UME'
+  >>> browser.getControl(name='form.study_mode').value = 'combined'
+  >>> browser.getControl(name='form.start_level').value = '100'
+  >>> browser.getControl(name='form.end_level').value = '400'
+  >>> browser.getControl(name='form.application_category').value = 'UME'
+  >>> browser.getControl(name='form.m_prefix').value = 'something'
+  >>> browser.getControl(name='form.max_pass').value = '400'
+
+Finally, we create the certificate:
+
+  >>> browser.getControl('Add certificate').click()
+
+If we try to register a certificate under the same code twice we will
+get an error:
+
+  >>> browser.open(dept_url + '/addcertificate')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another cert with same code'
+  >>> browser.getControl(name='form.code').value = 'CERT1'
+  >>> browser.getControl(name='form.category').value = 'UME'
+  >>> browser.getControl(name='form.study_mode').value = 'combined'
+  >>> browser.getControl(name='form.start_level').value = '100'
+  >>> browser.getControl(name='form.end_level').value = '400'
+  >>> browser.getControl(name='form.application_category').value = 'UME'
+  >>> browser.getControl(name='form.m_prefix').value = 'something'
+  >>> browser.getControl(name='form.max_pass').value = '400'
+
+  >>> browser.getControl('Add certificate').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<div>The name chosen already exists in the database</div>
+  ...
+
+Our certificate will be linked under the code on the department page:
+
+  >>> browser.open(dept_url)
+  >>> browser.getLink('CERT1').click()
+  >>> browser.url
+  'http://localhost/myuniversity/faculties/TF/TD/certificates/CERT1'
+
+Before we really add a certificate we can cancel the action and will be
+redirected to the department page:
+
+  >>> browser.open(dept_url + '/addcertificate')
+  >>> browser.getControl('Cancel').click()
+  >>> browser.url
+  'http://localhost/myuniversity/faculties/TF/TD'
+
+
+Modifying certificates
+----------------------
+
+We can change the settings for a cert by clicking on the provided
+'Edit settings' link:
+
+  >>> browser.open(dept_url + '/certificates/CERT1')
+  >>> browser.getLink('Edit settings').click()
+
+When modifying a certificate, we cannot change the code any more:
+
+  >>> browser.getControl(name='form.code')
+  Traceback (most recent call last):
+  ...
+  LookupError: name 'form.code'
+
+Let's set a new title and save the form:
+
+  >>> browser.getControl(name='form.title').value = "My test cert"
+  >>> browser.getControl(name='form.actions.save').click()
+
+Clicking 'Save' we will stay on the settings form. So we can change
+the cert again. This time we will return to the overview page
+afterwards:
+
+  >>> browser.getControl(name='form.title').value = "My renamed cert" 
+  >>> ctrl = browser.getControl("Save and return")
+  >>> ctrl.click()
+
+If we go to the settings page and click ``Cancel`` nothing will be
+changed:
+
+  >>> browser.getLink('Edit settings').click()
+  >>> browser.getControl(name='form.title').value = "Blah"
+  >>> browser.getControl('Cancel').click()
+
+Our certificate was indeed renamed to ``My renamed cert`` and not to
+``Blah``:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<div name="thecerttable">
+  ...<td>My renamed cert</td>...
+  ...
+
+
+Deleting certificates
+---------------------
+
+We can delete certificates by browsing the containing department and
+clicking on the appropriate 'Delete' button. As we have only one
+certificate, there is only one 'Delete' button yet:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+  >>> 'My renamed cert' in browser.contents
+  True
+
+  >>> browser.getControl('Delete').click()
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+  >>> 'My renamed cert' in browser.contents
+  False
+
+
+CertificateCourses
+==================
+
+Once we have a certificate, we can add also certificate courses. These
+are references to courses with some extra data.
+
+Before we can work with certificate courses, we need some certificates
+and courses to be available.
+
+  >>> browser.open(dept_url + '/addcourse')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another course with same code'
+  >>> browser.getControl(name='form.code').value = 'COURSE1'
+  >>> browser.getControl(name='form.title').value = 'Course 1'	
+  >>> browser.getControl('Add course').click()
+
+  >>> browser.open(dept_url + '/addcourse')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another course with same code'
+  >>> browser.getControl(name='form.code').value = 'COURSE2'
+  >>> browser.getControl(name='form.title').value = 'Course 2'
+  >>> browser.getControl('Add course').click()
+
+  >>> browser.open(dept_url + '/addcertificate')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another cert with same code'
+  >>> browser.getControl(name='form.code').value = 'CERT1'
+  >>> browser.getControl(name='form.title').value = 'Certificate 1'
+  >>> browser.getControl(name='form.category').value = 'UME'
+  >>> browser.getControl(name='form.study_mode').value = 'combined'
+  >>> browser.getControl(name='form.start_level').value = '100'
+  >>> browser.getControl(name='form.end_level').value = '400'
+  >>> browser.getControl(name='form.application_category').value = 'UME'
+  >>> browser.getControl(name='form.m_prefix').value = 'something'
+  >>> browser.getControl(name='form.max_pass').value = '400'
+  >>> browser.getControl('Add certificate').click()
+
+  >>> browser.open(dept_url + '/addcertificate')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another cert with same code'
+  >>> browser.getControl(name='form.code').value = 'CERT2'
+  >>> browser.getControl(name='form.title').value = 'Certificate 2'
+  >>> browser.getControl(name='form.category').value = 'UME'
+  >>> browser.getControl(name='form.study_mode').value = 'combined'
+  >>> browser.getControl(name='form.start_level').value = '100'
+  >>> browser.getControl(name='form.end_level').value = '400'
+  >>> browser.getControl(name='form.application_category').value = 'UME'
+  >>> browser.getControl(name='form.m_prefix').value = 'something'
+  >>> browser.getControl(name='form.max_pass').value = '400'
+  >>> browser.getControl('Add certificate').click()
+  
+
+Adding certificatecourses
+-------------------------
+
+Certcourses are stored in :class:`ICertificate` instances
+with their code as key.
+
+Each certificate provides a ``Add course`` action button near top.
+
+Certificates provide an add-form to add new certcourses:
+
+  >>> cert_url = dept_url + '/certificates/CERT1'
+  >>> browser.open(cert_url + '/addcertificatecourse')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<span class="required">*</span><span>Level of this course</span>
+  ...
+
+In the add-form we will get a list of available courses to select
+from. This list will contain all courses stored in the site, not only
+the ones from local department:
+
+  >>> ctrl = browser.getControl(name='form.course')
+  >>> ctrl.displayOptions
+  ['COURSE1 Course 1', 'COURSE2 Course 2']
+
+If we do not select anything else, the first item will be
+selected. That's okay for us. Finally, we create the
+certificatecourse:
+
+  >>> browser.getControl('Add course').click()
+
+Our certificatecourse will be linked on the parent certificate page:
+
+  >>> browser.open(cert_url)
+  >>> browser.getLink('COURSE1').click()
+  >>> browser.url
+  'http://localhost/my...sity/faculties/TF/TD/certificates/CERT1/COURSE1_100'
+
+When we started to add a new certificatecourse, we can also cancel the
+process before submitting. This will bring us back to the certificate
+page:
+
+  >>> browser.open(cert_url + '/addcertificatecourse')
+  >>> browser.getControl('Cancel').click()
+  >>> browser.url
+  'http://localhost/myuniversity/faculties/TF/TD/certificates/CERT1'
+
+
+Modifying certificatecourses
+----------------------------
+
+We can change the settings for a certcourse by clicking on the
+provided 'Edit settings' link:
+
+  >>> browser.open(cert_url + '/COURSE1_100')
+  >>> browser.getLink('Edit settings').click()
+
+Let's set a new level (it was 100 before) and save the form. This will
+bring us to the certificate index page afterwards:
+
+  >>> browser.getControl(name='form.level').value = "200" 
+  >>> ctrl = browser.getControl("Save and return")
+  >>> ctrl.click()
+
+As we changed the level, also the URL will change:
+
+  >>> browser.getLink('COURSE1').click()
+  >>> browser.url
+  'http://localhost/myun.../TF/TD/certificates/CERT1/COURSE1_200'
+
+If we go to the settings page and click ``Cancel`` nothing will be
+changed:
+
+  >>> browser.getLink('Edit settings').click()
+  >>> browser.getControl(name='form.level').value = "666"
+  >>> browser.getControl('Cancel').click()
+
+Our certcourse provides a new level of 200 and not 666:
+
+  >>> browser.open(cert_url + '/COURSE1_200')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Level of this course
+  ...200...
+  ...
+
+
+Deleting certificatecourses
+---------------------------
+
+We can delete certcourses by browsing the containing certificate and
+clicking on the appropriate 'Delete' button. As we have only one
+certcourse, there is only one 'Delete' button yet:
+
+  >>> browser.open(cert_url)
+  >>> 'COURSE1_200' in browser.contents
+  True
+
+  >>> browser.getControl('delete').click()
+
+  >>> browser.open(cert_url)
+  >>> 'COURSE1_200' in browser.contents
+  False
+
+
+Data Center
+===========
+
+The data center helps us uploading files for later import or similar.
+
+  >>> browser.open('http://localhost/myuniversity')
+  >>> 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>/.../waeup/files</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:
+  ...   os.unlink(os.path.join(uploadpath, filename))
+
+The new upload directory is now empty:
+
+  >>> os.listdir(uploadpath)
+  []
+
+
+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:
+
+  >>> os.listdir(uploadpath)
+  ['myfaculties.csv']
+
+We create and upload also a CSV file containing departments:
+
+  >>> open('departments.csv', 'wb').write(
+  ... """code,title,title_prefix,faculty_code
+  ... LIT,Literature,Department,FA
+  ... SOC,Sociology,Department,FA
+  ... PHY,Physics,Department,FS
+  ... INF,Informatics,Department,FS
+  ... MAT,Math,Department,FS
+  ... """)
+
+  >>> browser.open('http://localhost/myuniversity/datacenter/upload')
+  >>> browser.getControl(name='uploadfile:file').add_file(
+  ...   cStringIO.StringIO(open('departments.csv', 'rb').read()),
+  ...   'text/plain', 'mydepartments.csv')
+  >>> browser.getControl(name='SUBMIT').click()
+
+We create and upload also a CSV file containing courses:
+
+  >>> open('courses.csv', 'wb').write(
+  ... """code,level,title,passmark,credits,semester,faculty,department
+  ... LI1,,Introduction to Literature I,40,2,1,FA,LIT
+  ... LI2,,Introduction to Literature II,40,2,2,FA,LIT
+  ... AN1,000,Analysis I,40,2,1,FS,MAT
+  ... AN2,000,Analysis II,40,2,2,FS,MAT
+  ... """)
+
+  >>> browser.open('http://localhost/myuniversity/datacenter/upload')
+  >>> browser.getControl(name='uploadfile:file').add_file(
+  ...   cStringIO.StringIO(open('courses.csv', 'rb').read()),
+  ...   'text/plain', 'mycourses.csv')
+  >>> browser.getControl(name='SUBMIT').click()
+
+We create and upload also a CSV file containing certificates:
+
+  >>> open('certificates.csv', 'wb').write(
+  ... """code,title,faculty_code,department_code,category,study_mode,end_level,m_prefix,max_pass,start_level,application_category,review_state
+  ... LBA,BACHELOR OF LITERATURE,FA,LIT,UG,ug_ft,500,LIT,30,100,basic,checked
+  ... LMA,MASTER OF LITERATURE,FA,LIT,UG,ug_pt,500,LIT,30,100,cest,checked
+  ... DME,DIPLOMA OF MATH,FS,MAT,DP,dp_ft,200,DME,30,100,cest,unchecked
+  ... """)
+
+  >>> browser.open('http://localhost/myuniversity/datacenter/upload')
+  >>> browser.getControl(name='uploadfile:file').add_file(
+  ...   cStringIO.StringIO(open('certificates.csv', 'rb').read()),
+  ...   'text/plain', 'mycertificates.csv')
+  >>> browser.getControl(name='SUBMIT').click()
+
+We create and upload also a CSV file containing certificate courses:
+
+  >>> open('certcourses.csv', 'wb').write(
+  ... """code,faculty_code,department_code,certificate_code,level,core_or_elective
+  ... LI1,FA,LIT,LBA,100,True
+  ... LI2,FA,LIT,LBA,200,True
+  ... """)
+
+  >>> browser.open('http://localhost/myuniversity/datacenter/upload')
+  >>> browser.getControl(name='uploadfile:file').add_file(
+  ...   cStringIO.StringIO(open('certcourses.csv', 'rb').read()),
+  ...   'text/plain', 'mycertcourses.csv')
+  >>> browser.getControl(name='SUBMIT').click()
+
+
+Importing a CSV file
+--------------------
+
+As the uploaded file conforms with what we expect from a faculty-CSV
+file, we can import it. To do this we click the ``Import CSV data``
+link:
+
+  >>> browser.open('http://localhost/myuniversity/datacenter')
+  >>> browser.getLink('Import CSV data').click()
+
+Here we get all importable files listed:
+
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...The following files are available for import:...
+  ...mycertcourses.csv...Certificate Course Importer...
+  ...mycertificates.csv...Certificate Importer...
+  ...mycourses.csv...Course Importer...
+  ...mydepartments.csv...Department Importer...
+  ...myfaculties.csv...Faculty Importer...
+  ...
+
+As we can see, the files are analyzed on-the-fly so the system can
+tell us what kind of importer is available for each.
+
+We do import the faculty file by clicking on ``Import!`` next to the
+appropriate file:
+
+  >>> facimport = browser.getControl('Import!', index=4)
+  >>> facimport.click()
+
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Successfully imported: myfaculties.csv...
+  ...
+
+We just created two new faculties:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h2>Registered Faculties</h2>...
+  ...Arts...
+  ...Sciences...
+  ...
+
+We can also import the department data:
+
+  >>> browser.open('http://localhost/myuniversity/datacenter')
+  >>> browser.getLink('Import CSV data').click()
+  >>> browser.getControl('Import!', index=3).click()
+
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Successfully imported: mydepartments.csv...
+  ...
+
+We can also import the course data:
+
+  >>> browser.open('http://localhost/myuniversity/datacenter')
+  >>> browser.getLink('Import CSV data').click()
+  >>> browser.getControl('Import!', index=2).click()
+
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Successfully imported: mycourses.csv...
+  ...
+
+We can also import the certificate data:
+
+  >>> browser.open('http://localhost/myuniversity/datacenter')
+  >>> browser.getLink('Import CSV data').click()
+  >>> browser.getControl('Import!', index=1).click()
+
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Successfully imported: mycertificates.csv...
+  ...
+
+We can also import certificate courses:
+
+  >>> browser.open('http://localhost/myuniversity/datacenter')
+  >>> browser.getLink('Import CSV data').click()
+  >>> browser.getControl('Import!', index=0).click()
+
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Successfully imported: mycertcourses.csv...
+  ... 
+
+Clean up:
+
+  >>> import os
+  >>> import shutil
+  >>> shutil.rmtree(uploadpath)
Index: waeup/trunk/src/waeup/browser/__init__.py
===================================================================
--- waeup/trunk/src/waeup/browser/__init__.py	(revision 4789)
+++ waeup/trunk/src/waeup/browser/__init__.py	(revision 4789)
@@ -0,0 +1,3 @@
+from waeup.browser.layout import (WAeUPPage, WAeUPLayout, WAeUPDisplayFormPage,
+                                  WAeUPEditFormPage, WAeUPAddFormPage,
+                                  NullValidator)
Index: waeup/trunk/src/waeup/browser/breadcrumbs.py
===================================================================
--- waeup/trunk/src/waeup/browser/breadcrumbs.py	(revision 4789)
+++ waeup/trunk/src/waeup/browser/breadcrumbs.py	(revision 4789)
@@ -0,0 +1,191 @@
+""" Components to get breadcrumbs for any object.
+"""
+import grok
+from grokcore.view.interfaces import IGrokView
+from zope.component import getAdapter
+from zope.publisher.browser import TestRequest
+
+from waeup import interfaces
+from waeup.browser.interfaces import (IBreadcrumb, IBreadcrumbIgnorable,
+                                      IBreadcrumbContainer)
+        
+class Breadcrumb(grok.Adapter):
+    """A most general breadcrumb generator.
+    """
+    grok.provides(IBreadcrumb)
+    grok.context(interfaces.IWAeUPObject)
+    grok.name('index')
+
+    _title = None
+    _parent = 0
+    _request = None
+    parent_viewname = 'index'
+    viewname = 'index'
+    
+    def __init__(self, context):
+        """Turn a context into a breadcrumb.
+        """
+        self.context = context
+
+    @property
+    def title(self):
+        """Get a title for a context.
+        """
+        if self._title is not None:
+            return self._title
+        if hasattr(self.context, 'name'):
+            return self.context.name
+        if hasattr(self.context, 'title'):
+            return self.context.title
+        return None
+
+    @property
+    def parent(self):
+        """Get the contexts parent object and viewname or None.
+        """
+        if self._parent is None:
+            return None
+        if self._parent is not 0:
+            return (self._parent, self.parent_viewname)
+        
+        if self.viewname != 'index':
+            self._parent = self.context
+        else:
+            site = grok.getSite()
+            if self.context is not site:
+                self._parent = getattr(self.context, '__parent__', None)
+
+        if self._parent is not None:
+            return (self._parent, self.parent_viewname)
+        return None
+
+class UniversityBreadcrumb(Breadcrumb):
+    """A breadcrumb for university index pages.
+    """
+    grok.context(interfaces.IUniversity)
+    title = u'Home'
+    parent = None
+
+class FacultyContainerBreadcrumb(Breadcrumb):
+    """A breadcrumb for faculty containers.
+    """
+    grok.context(interfaces.IFacultyContainer)
+    title = u'Faculties'
+
+class AdministrationBreadcrumb(Breadcrumb):
+    """A breadcrumb for administration areas of University instances.
+    """
+    grok.context(interfaces.IUniversity)
+    grok.name('administration')
+    title = u'Administration'
+    viewname = 'administration'
+
+class UserContainerBreadcrumb(Breadcrumb):
+    """A breadcrumb for user containers.
+    """
+    grok.context(interfaces.IUserContainer)
+    title = u'Users'
+    parent_viewname = 'administration'
+
+class DataCenterBreadcrumb(Breadcrumb):
+    """A breadcrumb for data centers.
+    """ 
+    grok.context(interfaces.IDataCenter)
+    title = u'Data Center'
+    parent_viewname = 'administration'
+
+class FacultyBreadcrumb(Breadcrumb):
+    """A breadcrumb for faculties.
+    """
+    grok.context(interfaces.IFaculty)
+    
+    @property
+    def title(self):
+        prefix = self.context.title_prefix
+        prefix = prefix[0].upper() + prefix[1:]
+        return '%s of %s' % (prefix, self.context.title)
+
+class DepartmentBreadcrumb(FacultyBreadcrumb):
+    """A breadcrumb for departments.
+    """
+    grok.context(interfaces.IDepartment)
+
+class CourseContainerBreadcrumb(Breadcrumb):
+    """ We don't want course container breadcrumbs.
+    """
+    grok.context(interfaces.ICourseContainer)
+    grok.implements(IBreadcrumbIgnorable)
+
+class CertificateContainerBreadcrumb(Breadcrumb):
+    """ We don't want course container breadcrumbs.
+    """
+    grok.context(interfaces.ICertificateContainer)
+    grok.implements(IBreadcrumbIgnorable)
+
+class CertificateCourseBreadcrumb(Breadcrumb):
+    """ We don't want course container breadcrumbs.
+    """
+    grok.context(interfaces.ICertificateCourse)
+    @property
+    def title(self):
+        return self.context.course.title
+
+def getBreadcrumb(obj, viewname=None):
+    """ Get a breadcrumb for an object and a viewname.
+
+    If there is no breadcrumb defined for such a combination, a
+    breadcrumb for the ``index`` view will be looked up.
+    """
+    try:
+        return getAdapter(obj, IBreadcrumb, name=viewname)
+    except:
+        pass
+    return getAdapter(obj, IBreadcrumb, name='index')
+
+def getBreadcrumbList(obj, viewname):
+    """Get an ordered list of breadcrumbs for an object and a viewname.
+
+    Ignorables are excluded from the result.
+    """
+    current = getBreadcrumb(obj, viewname)
+    result = [current]
+    while current.parent is not None:
+        context, viewname = current.parent
+        current = getBreadcrumb(context, viewname)
+        if IBreadcrumbIgnorable.providedBy(current):
+            # Ignore empty breadcrumbs...
+            continue
+        result.append(current)
+    result.reverse()
+    return result
+
+def getBreadcrumbListForView(view):
+    """Get an ordered list of breadcrumbs a certain view.
+
+    Ignorables are excluded from the result.
+    """
+    context = getattr(view, 'context')
+    viewname = getattr(view, '__name__')
+    return getBreadcrumbList(context, viewname)
+
+class BreadcrumbContainer(grok.Adapter):
+    """An adapter to adapt grok views to list of breadcrumbs.
+    """
+    grok.context(IGrokView)
+    grok.provides(IBreadcrumbContainer)
+
+    _breadcrumbs = None
+    
+    def __init__(self, context):
+        self.context = context
+        self._breadcrumbs = getBreadcrumbListForView(self.context)
+
+    def __iter__(self):
+        """Allow iteration.
+        """
+        return self._breadcrumbs.__iter__()
+
+    def getList(self):
+        """Get the (ordered) list of breadcrumbs liked to the context view.
+        """
+        return self._breadcrumbs
Index: waeup/trunk/src/waeup/browser/breadcrumbs.txt
===================================================================
--- waeup/trunk/src/waeup/browser/breadcrumbs.txt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/breadcrumbs.txt	(revision 4789)
@@ -0,0 +1,245 @@
+Breadcrumbs
+***********
+
+:test-layer: functional
+
+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 university to check the breadcrumb functionality.
+
+    >>> from waeup.app import University
+    >>> root = getRootFolder()
+    >>> root['app'] = University()
+    >>> 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.browser.breadcrumbs import getBreadcrumbList
+    >>> blist1 = getBreadcrumbList(app, 'index')
+    >>> blist1
+    [<waeup.browser.breadcrumbs.UniversityBreadcrumb object at 0x...>]
+
+A slightly more extensive list for the datacenter:
+
+    >>> blist2 = getBreadcrumbList(app['datacenter'], 'index')
+    >>> from pprint import pprint
+    >>> pprint(blist2)
+    [<waeup.browser.breadcrumbs.UniversityBreadcrumb object at 0x...>,
+     <waeup.browser.breadcrumbs.AdministrationBreadcrumb object at 0x...>,
+     <waeup.browser.breadcrumbs.DataCenterBreadcrumb object at 0x...>]
+
+We get a breadcrumb for university, 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.app.University object at 0x...>, 'index'),
+      (<waeup.app.University object at 0x...>, 'administration'),
+      (<waeup.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 university 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 University
+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.browser.breadcrumbs import getBreadcrumbListForView
+    >>> blist3 = getBreadcrumbListForView(page)
+    >>> [x.title for x in blist3]
+    [u'Home', u'Administration', u'Users']
+
+..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.browser.interfaces import IBreadcrumbContainer
+    >>> mybccontainer = IBreadcrumbContainer(page)
+    >>> [x.title for x in mybccontainer]
+    [u'Home', u'Administration', u'Users']
+
+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.UniversityBreadcrumb object at 0x...>,
+     <...breadcrumbs.AdministrationBreadcrumb object at 0x...>,
+     <...breadcrumbs.UserContainerBreadcrumb 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 University object:
+
+    >>> from zope.component import getAdapter
+    >>> from waeup.browser.interfaces import IBreadcrumb
+    >>> b1 = getAdapter(app, IBreadcrumb, 'index')
+    >>> b1
+    <waeup.browser.breadcrumbs.UniversityBreadcrumb 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.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.app.University object at 0x...>, 'administration')
+
+This result denotes a new context object (the University 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...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
+University 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.app.University 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: waeup/trunk/src/waeup/browser/interfaces.py
===================================================================
--- waeup/trunk/src/waeup/browser/interfaces.py	(revision 4789)
+++ waeup/trunk/src/waeup/browser/interfaces.py	(revision 4789)
@@ -0,0 +1,52 @@
+"""Interfaces for viewing components.
+"""
+from zope import schema
+from zope.interface import Interface, Attribute
+
+
+class IBreadcrumb(Interface):
+    """Provide breadcrumbs.
+    """
+    title = schema.TextLine(
+        title = u'Context description',
+        readonly = True,
+        )
+    viewname = schema.TextLine(
+        title = u'The viewname, for which the breadcrumb is provided.',
+        readonly = True,
+        default = u'index'
+        )
+    parent_viewname = schema.TextLine(
+        title = u'The viewname of the parent to use.',
+        readonly = True,
+        default = u'index'
+        )
+    parent = Attribute("The parent object.")
+    context = Attribute("The context of the breadcrumb.")
+
+
+    def getURL(view=None):
+        """Return a URL to the breadcrumb target.
+        """
+
+class IBreadcrumbIgnorable(Interface):
+    """A marker interface for breadcrumbs that should be skipped in output.
+
+    If a breadcrumb wants to be skipped in real output (for instance,
+    because it is set on a layer in site hierarchy that should not be
+    accessed by users), it can also provide this interface. The
+    getBreadcrumbList() function defined here will exclude IIgnorables
+    by default.
+    """
+    pass
+
+class IBreadcrumbContainer(Interface):
+    """A container of breadcrumbs.
+    """
+    def __iter__():
+        """Allow iteration over the container.
+        """
+
+    def getList():
+        """Get the list of breadcrumbs as real Python list.
+        """
Index: waeup/trunk/src/waeup/browser/layout.py
===================================================================
--- waeup/trunk/src/waeup/browser/layout.py	(revision 4789)
+++ waeup/trunk/src/waeup/browser/layout.py	(revision 4789)
@@ -0,0 +1,107 @@
+"""Basic layout components.
+"""
+import grok
+from hurry import yui
+from megrok.layout import Page, Layout, EditForm, DisplayForm, AddForm
+from waeup.interfaces import IWAeUPObject
+from z3c.flashmessage.interfaces import IMessageSource
+from zope.component import queryUtility, ComponentLookupError
+from zope.interface import Interface
+# Conditional import. getSite moved to zope.site.hooks in recent grok versions.
+try:
+    from zope.site.hooks import getSite
+except ImportError:
+    from zope.app.component.hooks import getSite
+
+grok.templatedir('templates')
+
+def NullValidator(*args, **kw):
+    """A validator that does not validate.
+
+    This is needed especially for cancel buttons. We don't want data
+    to be validated that will be thrown away in the next step.
+
+    You can use it with ``grok.action`` decorator like this::
+
+      @grok.action('Cancel', validator=NullValidator)
+      def cancel(self, **data):
+        self.redirect(<whereever-you-go>)
+    """
+    return dict()
+
+class UtilityView(object):
+    """A view mixin with useful methods.
+
+    The ``pnav`` attribute (a number) tells, to which primary
+    navigation tab a page declares to belong.
+    """
+    title = u'' # What appears in the content title...
+    pnav = 0 # Primary navigation index...
+    
+    def application_url(self, name=None):
+        """Return the URL of the nearest site.
+        """
+        site = getSite()
+        #if not site:
+        #    raise ComponentLookupError("No site found.")
+        return self.url(site, name)
+
+    def flash(self, message, type='message'):
+        """Send a short message to the user.
+        """
+        source = queryUtility(IMessageSource, name='session')
+        if source is None:
+            return None
+        source.send(message, type)
+        return True
+
+class WAeUPLayout(Layout, UtilityView):
+    """A megrok.layout.Layout with additional methods.
+    """
+    grok.baseclass()
+    
+class WAeUPPage(Page, UtilityView):
+    """A megrok.layout page with additional methods.
+    """
+    grok.baseclass()
+
+class WAeUPDisplayFormPage(DisplayForm, UtilityView):
+    """A megrok.layout.DisplayForm with additional methods.
+    """
+    grok.baseclass()
+
+class WAeUPEditFormPage(EditForm, UtilityView):
+    """A megrok.layout.EditForm with additional methods.
+    """
+    grok.baseclass()
+
+class WAeUPAddFormPage(AddForm,  UtilityView):
+    """A megrok.layout.AddForm with additional methods.
+    """
+    grok.baseclass()
+
+class SiteLayout(WAeUPLayout):
+    """ The general site layout.
+    """
+    grok.context(IWAeUPObject)
+
+    def getAppTitle(self):
+        return getattr(grok.getSite(), 'name', u'Sample University')
+    
+    def isAuthenticated(self):
+        """Return True if the calling user is authenticated.
+        """
+        usertitle = self.request.principal.title
+        return usertitle != 'Unauthenticated User'
+    
+    def getUserTitle(self):
+        """Return principal title of current user.
+        """
+        usertitle = self.request.principal.title
+        if usertitle == 'Unauthenticated User':
+            return u''
+        return usertitle
+
+    def update(self):
+        yui.reset_fonts_grids.need()
+        yui.sam.need()
Index: waeup/trunk/src/waeup/browser/pages.py
===================================================================
--- waeup/trunk/src/waeup/browser/pages.py	(revision 4789)
+++ waeup/trunk/src/waeup/browser/pages.py	(revision 4789)
@@ -0,0 +1,882 @@
+# -*- coding: utf-8 -*-
+""" Viewing components for WAeUP objects.
+"""
+import copy
+import grok
+import os
+import sys
+from hurry import yui
+from hurry.query import Eq
+from hurry.query.query import Query, Text
+from hurry.workflow.interfaces import NoTransitionAvailableError
+from waeup.browser import (WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage,
+                           WAeUPDisplayFormPage, NullValidator)
+from waeup.catalog import search, search_context
+from waeup.datacenter import Import
+from waeup.interfaces import (IUniversity, IWAeUPObject, IUserContainer,
+                              IUserAccount, IFacultyContainer, IDataCenter,
+                              IFaculty, IDepartment, ICourse, ICertificate,
+                              ICertificateCourse, IWAeUPXMLImporter,
+                              IWAeUPXMLExporter)
+from waeup.widgets.interfaces import ITableProvider
+from waeup.utils.helpers import getName
+from zope import schema
+from zope.app.security.interfaces import (IAuthentication,
+                                          IUnauthenticatedPrincipal,
+                                          ILogout)
+from zope.component import getUtility, createObject
+from zope.component.interfaces import Invalid
+from zope.exceptions import DuplicationError
+
+grok.context(IWAeUPObject)
+grok.templatedir('templates')
+
+#
+# Login/logout pages...
+#
+
+class LoginPage(WAeUPPage):
+    """A login page, available for all objects.
+    """
+    grok.name('login')
+    grok.context(IWAeUPObject)
+
+    title = u'Login'
+    camefrom = None
+
+    def update(self, SUBMIT=None, camefrom=None):
+        self.camefrom = camefrom
+        if SUBMIT is not None:
+            if self.request.principal.id != 'zope.anybody':
+                self.flash('You logged in.')
+                if not self.camefrom:
+                    # User might have entered the URL directly. Let's beam
+                    # him back to our context.
+                    self.redirect(self.url(self.context))
+                    return
+                self.redirect(self.camefrom)
+                return
+            self.flash('You entered wrong credentials!')
+
+
+class LoginStaffPage(LoginPage):
+    """A login page for staff members.
+    """
+    grok.name('loginstaff')
+    grok.template('loginstaffpage')
+    title = u'Staff Login'
+
+
+class LoginStudentPage(LoginPage):
+    """A login page for students.
+    """
+    grok.name('loginstudent')
+    grok.template('loginstudentpage')
+    title = u'Student Login'
+
+class LogoutPage(WAeUPPage):
+    """A logout page. Calling this page will log the current user out.
+    """
+    grok.context(IWAeUPObject)
+    grok.name('logout')
+    
+    def update(self):
+        if not IUnauthenticatedPrincipal.providedBy(self.request.principal):
+            auth = getUtility(IAuthentication)
+            ILogout(auth).logout(self.request)
+            self.flash("You have been logged out. Thanks for using WAeUP SRP!")
+        self.redirect(self.url(self.context))
+
+
+#
+# University related pages...
+#
+
+class UniversityPage(WAeUPPage):
+    """ The main university page.
+    """
+    grok.name('index')
+    grok.context(IUniversity)
+    title = u'Welcome to SRP'
+    pnav = 0
+
+class Manage(WAeUPEditFormPage):
+    """Manage the basic properties of a `University` instance.
+    """
+    form_fields = grok.AutoFields(IUniversity)
+    grok.context(IUniversity)
+    grok.require('waeup.manageUniversity')
+    title = u'Edit settings'
+    pnav = 0
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        self.flash('Settings have been saved.')
+        return
+    
+    @grok.action('Save and return')
+    def saveAndReturn(self, **data):
+        self.applyData(self.context, **data)
+        self.redirect(self.url(self.context))
+        self.flash('Settings have been saved.')
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.flash('Action cancelled.')
+        self.redirect(self.url(self.context))
+        return
+
+#
+# User container pages...
+#
+    
+class UserContainerPage(WAeUPPage):
+    """Overview page for all local users.
+    """
+    grok.require('waeup.manageUsers')
+    grok.context(IUserContainer)
+    grok.name('index')
+    title = 'Users'
+    
+    def update(self, userid=None, adduser=None, edit=None, delete=None):
+        if edit is not None and userid is not None:
+            self.redirect(self.url(userid))
+        if delete is not None and userid is not None:
+            self.context.delUser(userid)
+            self.flash('User %s successfully deleted.' % userid)
+
+class AddUserFormPage(WAeUPAddFormPage):
+    grok.require('waeup.manageUsers')
+    grok.context(IUserContainer)
+    grok.name('add')
+    form_fields = grok.AutoFields(IUserAccount)
+    label = 'Add user'
+    title = 'Add user'
+
+    @grok.action('Add user')
+    def addUser(self, **data):
+        name = data['name']
+        title = data['title']
+        description = data['description']
+        password = data['password']
+        roles = data['roles']
+        try:
+            self.context.addUser(name, password, title=title,
+                                 description=description, roles=roles)
+        except DuplicationError:
+            self.status = Invalid('The userid chosen already exists '
+                                  'in the database')
+            return
+        self.redirect(self.url(self.context))
+
+class UserEditFormPage(WAeUPEditFormPage):
+    """Edit a user account.
+    """
+    grok.context(IUserAccount)
+    grok.name('index')
+    
+    form_fields = grok.AutoFields(IUserAccount)
+    grok.require('waeup.manageUsers')
+
+    title = "Edit user"
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        self.flash('User settings have been saved.')
+        return
+    
+    @grok.action('Save and return')
+    def saveAndReturn(self, **data):
+        self.applyData(self.context, **data)
+        self.flash('User settings have been saved.')
+        self.redirect(self.url(self.context.__parent__))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context.__parent__))
+        return
+
+#
+# Search pages...
+#
+class SearchStudentPage(WAeUPPage):
+    grok.context(IUniversity)
+    grok.name('searchstudent')
+    grok.template('searchstudentpage')
+    grok.require('waeup.View')
+    title = u"Student Search"
+    pnav = 2
+
+    def update(self, query=None):
+        self.search_result = []
+        if query is not None:
+            self.search_result = search_context(query)
+            print "RESULT: ", list(self.search_result)
+
+
+class SearchPage(WAeUPPage):
+     grok.context(IUniversity)
+     grok.name('search')
+     grok.template('searchpage')
+     grok.require('waeup.View')
+     title = u"Site Search"
+     pnav = 2
+
+     def update(self, *args, **kw):
+          form = self.request.form
+          self.hitlist = []
+          self.query = ''
+          if not 'query' in form:
+               return
+          query = form['query']
+          self.query = query
+          self.hitlist = search(query=self.query, view=self)
+          return
+
+#
+# Datacenter pages...
+#
+
+class DatacenterPage(WAeUPPage):
+    grok.context(IDataCenter)
+    grok.name('index')
+    title = u'Data Center'
+    pnav = 0
+
+class DatacenterUploadPage(WAeUPPage):
+    grok.context(IDataCenter)
+    grok.name('upload')
+    title = u'Data Center Upload'
+    pnav = 0
+    
+    def update(self, uploadfile=None, CANCEL=None, SUBMIT=None):
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        if not uploadfile:
+            return
+        try:
+            filename = uploadfile.filename
+            target = os.path.join(self.context.storage, filename)
+            open(target, 'wb').write(uploadfile.read())
+        except IOError:
+            self.flash('Error while uploading file. Please retry.')
+            self.flash('I/O error: %s' % sys.exc_info()[1])
+            return
+        self.redirect(self.url(self.context))
+
+
+class DataCenterImportCSVPage(WAeUPPage):
+    """A view for importing things.
+
+    """
+    grok.require('waeup.manageUniversity')
+    grok.name('importcsv')
+    grok.template('datacenterimportcsvpage')
+    grok.context(IDataCenter)
+    title = u'Import CSV'
+    pnav = 0
+
+    def getImports(self):
+        result = []
+        imports = self.context.getPossibleImports()
+        result = [Import(x, y) for x, y in imports]
+        return result
+
+    def update(self, csvfile=None, clear=None, overwrite=None,
+               receiverid=None, CANCEL=None, SUBMIT=None):
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        if not csvfile:
+            return
+        if not SUBMIT:
+            return
+        try:
+            self.context.doImport(csvfile, receiverid, clear=clear)
+        except ValueError:
+            self.flash('Could not import: %s' % os.path.basename(csvfile))
+            return
+        except:
+            self.flash('Import failed: %s %s' % (sys.exc_info()[0],
+                                                 sys.exc_info()[1]))
+            return
+        self.flash('Successfully imported: %s' % os.path.basename(csvfile))
+        self.redirect(self.url(self.context))
+        return
+
+class DatacenterSettings(WAeUPPage):
+    grok.context(IDataCenter)
+    grok.name('manage')
+    grok.template('datacentermanagepage')
+    grok.require('waeup.manageUniversity')
+    title = u'Data Center Settings'
+    pnav = 0
+    #grok.template('master')
+
+    def update(self, newpath=None, move=False, overwrite=False,
+               save=None, cancel=None):
+        if move:
+            move = True
+        if overwrite:
+            overwrite = True
+        if newpath is None:
+            return
+        if cancel is not None:
+            self.redirect(self.url(self.context))
+            return
+        try:
+            not_copied = self.context.setStoragePath(newpath, move=move)
+            for name in not_copied:
+                self.flash('File already existed (not copied): %s' % name)
+        except ValueError:
+            self.flash('Given storage path cannot be used.')
+            self.flash('Error: %s' %sys.exc_info()[1])
+            return
+        if newpath:
+            self.flash('New storage path succefully set.')
+            self.redirect(self.url(self.context))
+        return
+
+class ExportXMLPage(grok.View):
+    """Deliver an XML representation of the context.
+    """
+    grok.name('export.xml')
+    grok.require('waeup.manageUniversity')
+    
+    def render(self):
+        exporter = IWAeUPXMLExporter(self.context)
+        xml = exporter.export().read()
+        self.response.setHeader(
+            'Content-Type', 'text/xml; charset=UTF-8')
+        return xml
+
+class ImportXMLPage(WAeUPPage):
+    """Replace the context object by an object created from an XML
+       representation.
+
+       XXX: This does not work for ISite objects, i.e. for instance
+            for complete University objects.
+    """
+    grok.name('importxml')
+    grok.require('waeup.manageUniversity')
+
+    def update(self, xmlfile=None, CANCEL=None, SUBMIT=None):
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        if not xmlfile:
+            return
+        importer = IWAeUPXMLImporter(self.context)
+        obj = importer.doImport(xmlfile)
+        if type(obj) != type(self.context):
+            return
+        parent = self.context.__parent__
+        name = self.context.__name__
+        self.context = obj
+        if hasattr(parent, name):
+            setattr(parent, name, obj)
+        else:
+            del parent[name]
+            parent[name] = obj
+            pass
+        return
+
+
+
+#
+# FacultyContainer pages...
+#
+
+class FacultyContainerPage(WAeUPPage):
+    """ Index page for faculty containers.
+    """
+    grok.context(IFacultyContainer)
+    grok.require('waeup.View')
+    grok.name('index')
+    title = 'Faculties'
+    pnav = 1
+
+    def update(self):
+        self.table = ITableProvider(self.context).getTables(self)[0]
+        self.table.need()
+    
+    def getFaculties(self):
+        """Convinience method to create a sorted list of faculties.
+
+        It provides a list of dicts with entries for all data needed by
+        usual list templates.
+        """
+        result = []
+        for key, val in self.context.items():
+            result.append(dict(id=key, name=val.title))
+        return result
+
+class AddFacultyFormPage(WAeUPAddFormPage):
+    """ Page form to add a new faculty to a faculty container.
+    """
+    grok.context(IFacultyContainer)
+    grok.require('waeup.manageUniversity')
+    grok.name('add')
+    form_fields = grok.AutoFields(IFaculty)
+    label = 'Add a faculty'
+    title = 'Add a faculty'
+    pnav = 1
+
+    @grok.action('Add faculty')
+    def addFaculty(self, **data):
+        faculty = createObject(u'waeup.Faculty')
+        self.applyData(faculty, **data)
+        try:
+            self.context.addFaculty(faculty)
+        except DuplicationError:
+            self.status = Invalid('The name chosen already exists '
+                                  'in the database')
+            return
+        self.redirect(self.url(self.context))
+
+class ManageFacultyContainerPage(WAeUPPage):
+    """ Manage Page 
+    """
+    grok.context(IFacultyContainer)
+    grok.require('waeup.manageUniversity')
+    grok.template('facultycontainermanagepage')
+    grok.name('manage')
+    title = "Manage Faculties"
+    pnav = 1
+
+    def update(self):
+        form = self.request.form
+        if 'CANCEL' in form.keys():
+            self.redirect(self.url(self.context))
+        if not 'DELETE' in form.keys():
+            return
+        fac_id = form['fac_id']
+        if not isinstance(fac_id, list):
+            fac_id = [fac_id]
+        deleted = []
+        for id in fac_id:
+            try:
+                del self.context[id]
+                deleted.append(id)
+            except:
+                self.flash('Could not delete %s: %s: %s' % (
+                        id, sys.exc_info()[0], sys.exc_info()[1]))
+        if len(deleted):
+            self.flash('Successfully deleted: %s' % ', '.join(deleted))
+        # We have to redirect to let flash messages appear immediately...
+        self.redirect(self.url())
+        return
+
+#
+# Faculty pages
+#
+class FacultyPage(WAeUPPage):
+    """Index page of faculties.
+    """
+    grok.context(IFaculty)
+    grok.require('waeup.View')
+    grok.name('index')
+    pnav = 1
+    @property
+    def title(self):
+        return "%s (%s)" % (getName(self.context), self.context.code)
+
+    def update(self):
+        self.table = ITableProvider(self.context).getTables(view=self)[0]
+        self.table.need()
+
+class AddDepartmentFormPage(WAeUPAddFormPage):
+    """Add a department to a faculty.
+    """
+    grok.context(IFaculty)
+    grok.name('add')
+    grok.require('waeup.manageUniversity')
+    form_fields = grok.AutoFields(IDepartment)
+    title = 'Add a department'
+    pnav = 1
+    
+    @grok.action('Add department')
+    def addDepartment(self, **data):
+        department = createObject(u'waeup.Department')
+        self.applyData(department, **data)
+        try:
+            self.context.addDepartment(department)
+        except DuplicationError:
+            self.status = Invalid('The name chosen already exists '
+                                  'in the database')
+            return
+        self.redirect(self.url(self.context))
+
+class ManageFacultyFormPage(WAeUPEditFormPage):
+    """Manage the basic properties of a `Faculty` instance.
+    """
+    grok.context(IFaculty)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    form_fields = grok.AutoFields(IFaculty)
+    title = 'Edit faculty'
+    pnav = 1
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        return
+    
+    @grok.action('Save and return')
+    def saveAndReturn(self, **data):
+        self.applyData(self.context, **data)
+        self.redirect(self.url(self.context))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+#
+# Department pages
+#
+class DepartmentPage(WAeUPPage):
+    """Department index page.
+    """
+    grok.context(IDepartment)
+    grok.require('waeup.View')
+    grok.name('index')
+    pnav = 1
+
+    @property
+    def title(self):
+        return "%s (%s)" % (getName(self.context), self.context.code)
+    
+    def update(self):
+        yui.tabview.need()
+
+        if 'delcourse' in self.request.form:
+            code = self.request.form['code']
+            del self.context.courses[code]
+        if 'delcert' in self.request.form:
+            code = self.request.form['code']
+            del self.context.certificates[code]
+        self.tables = ITableProvider(self.context).getTables(view=self)
+        self.coursetable = self.tables[0]
+        self.certificatetable = self.tables[1]
+        self.coursetable.need()
+
+class DepartmentManageFormPage(WAeUPEditFormPage):
+    """Manage the basic properties of a `Department` instance.
+    """
+    grok.context(IDepartment)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    title = u'Edit department settings'
+    pnav = 1
+
+    form_fields = grok.AutoFields(IDepartment)
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        return
+    
+    @grok.action('Save and return')
+    def saveAndReturn(self, **data):
+        self.applyData(self.context, **data)
+        self.redirect(self.url(self.context))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+class DepartmentAddCourseFormPage(WAeUPAddFormPage):
+    """Add-form to add course to a department.
+    """
+    grok.context(IDepartment)
+    grok.name('addcourse')
+    grok.require('waeup.manageUniversity')
+    title = u'Add course'
+    pnav = 1
+    
+    # We need a deepcopy here, because otherwise also all other
+    # ICourse forms would be affected.
+    form_fields = copy.deepcopy(grok.AutoFields(ICourse))
+    form_fields.get('code').field.readonly = False
+
+    @grok.action('Add course')
+    def addCourse(self, **data):
+        course = createObject(u'waeup.Course')
+        self.applyData(course, **data)
+        try:
+            self.context.courses.addCourse(course)
+        except DuplicationError:
+            self.status = Invalid('The code chosen already exists '
+                                  'in the database')
+            return
+        self.redirect(self.url(self.context))
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+class DepartmentAddCertificateFormPage(WAeUPAddFormPage):
+    """Add-form to add certificate to a department.
+    """
+    grok.context(IDepartment)
+    grok.name('addcertificate')
+    grok.require('waeup.manageUniversity')
+    title = u'Add certificate'
+    pnav = 1
+    form_fields = grok.AutoFields(ICertificate)
+    
+    @grok.action('Add certificate')
+    def addCertificate(self, **data):
+        certificate = createObject(u'waeup.Certificate')
+        try:
+            self.applyData(certificate, **data)
+        except NoTransitionAvailableError:
+            self.status = Invalid('Review state cannot be changed to '
+                                  'requested state: no such transition '
+                                  'available in workflow.')
+            return
+        try:
+            self.context.certificates.addCertificate(certificate)
+        except DuplicationError:
+            self.status = Invalid('The name chosen already exists '
+                                  'in the database')
+            return
+        self.redirect(self.url(self.context))
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self): #, **data):
+        self.redirect(self.url(self.context))
+        return
+
+#
+# Courses pages
+#
+class CoursePage(WAeUPDisplayFormPage):
+    """Course index page.
+    """
+    grok.context(ICourse)
+    grok.name('index')
+    grok.require('waeup.View')
+    pnav = 1
+    form_fields = grok.AutoFields(ICourse)
+
+    @property
+    def title(self):
+        return 'Course: %s (%s)' % (self.context.title, self.context.code)
+
+class CourseManageFormPage(WAeUPEditFormPage):
+    """Edit form page for courses.
+    """
+    grok.context(ICourse)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    title = u'Edit course settings'
+    pnav = 1
+
+    form_fields = grok.AutoFields(ICourse)
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        return
+    
+    @grok.action('Save and return')
+    def saveAndReturn(self, **data):
+        self.applyData(self.context, **data)
+        self.redirect(self.url(self.context))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+#
+# Certificate pages
+#
+class CertificatePage(WAeUPPage):
+    """Index page for certificates.
+    """
+    grok.context(ICertificate)
+    grok.name('index')
+    grok.require('waeup.View')
+    pnav = 1
+
+    @property
+    def title(self):
+        return "%s (%s)" % (self.context.title, self.context.code)
+
+    def update(self):
+        if 'delcourse' in self.request.form:
+            code = self.request.form['code']
+            level = self.request.form['level']
+            self.context.delCourseRef(code, level=level)
+        return
+        
+    def getCourseList(self):
+        def cmpCourses(a, b):
+            if a['level'] < b['level']:
+                return -1
+            if a['level'] > b['level']:
+                return 1
+            if a['mandatory'] != b['mandatory']:
+                if a['mandatory'] == False and b['mandatory'] == True:
+                    return -1
+                return 1
+            if a['code'] < b['code']:
+                return -1
+            if a['code'] > b['code']:
+                return 1
+            return 0
+
+        entries = []
+        levels = dict()
+        for key, course in self.context.items():
+            certcourse = self.context[key]
+            course = certcourse.course
+            level = certcourse.level
+            mandatory = certcourse.core_or_elective
+            title = course.title
+            code = course.code
+            course_url = self.url(self.context[key])
+            semester = course.semester
+
+            new_entry = dict(
+                level=level, mandatory=mandatory, url=course_url,
+                title=title, code=code, semester=semester)
+            if str(level) not in levels.keys():
+                levels[str(level)] = dict()
+            if str(semester) not in levels[str(level)].keys():
+                levels[str(level)][str(semester)] = []
+            levels[str(level)][str(semester)].append(new_entry)
+        return levels
+
+class CertificateManageForm(WAeUPEditFormPage):
+    """Manage the basic properties of a `Certificate` instance.
+    """
+    grok.context(ICertificate)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    # We omit 'code' field from original schema and set prepend a new
+    # one with 'readonly' set to True.
+    form_fields = grok.Fields(
+        code = schema.TextLine(
+            title = u'Code', readonly=True
+            )
+        ) + grok.AutoFields(ICertificate).omit('code')
+    pnav = 1
+
+    @property
+    def title(self):
+        # Set certificate name as form label
+        return self.form_fields['title'].field.get(self.context)
+    
+    @grok.action('Save')
+    def save(self, **data):
+        try:
+            self.applyData(self.context, **data)
+        except NoTransitionAvailableError:
+            self.status = Invalid('Review state cannot be changed to '
+                                  'requested state: no such transition '
+                                  'available in workflow.')
+        return
+    
+    @grok.action('Save and return')
+    def saveAndReturn(self, **data):
+        try:
+            self.applyData(self.context, **data)
+        except NoTransitionAvailableError:
+            self.status = Invalid('Review state cannot be changed to '
+                                  'requested state: no such transition '
+                                  'available in workflow.')
+            return
+        self.redirect(self.url(self.context))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+#
+# Certificate course pages...
+#
+class CertificateCoursePage(WAeUPDisplayFormPage):
+    """Course index page.
+    """
+    grok.context(ICertificateCourse)
+    grok.name('index')
+    grok.require('waeup.View')
+    pnav = 1
+    form_fields = grok.AutoFields(ICertificateCourse)
+
+    @property
+    def title(self):
+        return self.context.course.title
+
+class CertificateCourseFormPage(WAeUPEditFormPage):
+    """Manage the basic properties of a `CertificateCourse` instance.
+    """
+    grok.context(ICertificateCourse)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    form_fields = grok.AutoFields(ICertificateCourse)
+    pnav = 1
+    
+    @property
+    def title(self):
+        return 'Edit certificate course %s' % self.context.course.code
+    
+    @grok.action('Save and return')
+    def saveAndReturn(self, **data):
+        parent = self.context.__parent__
+        if self.context.level == data['level']:
+            self.applyData(self.context, **data)
+        else:
+            # The level changed. We have to create a new certcourse as
+            # the key of the entry will change as well...
+            old_level = self.context.level
+            parent.addCourseRef(**data)
+            parent.delCourseRef(data['course'].code, level=old_level)
+        self.redirect(self.url(parent))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+class CertificateCourseAddFormPage(WAeUPAddFormPage):
+    """Add-page to add a course ref to a certificate
+    """
+    grok.context(ICertificate)
+    grok.name('addcertificatecourse')
+    grok.require('waeup.manageUniversity')
+    form_fields = grok.AutoFields(ICertificateCourse)
+    pnav = 1
+    title = 'Add a course'
+
+    @grok.action('Add course')
+    def addCertcourse(self, **data):
+        try:
+            self.context.addCourseRef(**data)
+        except DuplicationError:
+            self.status = Invalid('The chosen course is already part of'
+                                  'this certificate')
+            return
+        self.redirect(self.url(self.context))
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
Index: waeup/trunk/src/waeup/browser/static/purple.css
===================================================================
--- waeup/trunk/src/waeup/browser/static/purple.css	(revision 4789)
+++ waeup/trunk/src/waeup/browser/static/purple.css	(revision 4789)
@@ -0,0 +1,301 @@
+/* This is the bare minimum needed to create your own theme.
+In this example, all we're doing is overriding the default
+color scheme with our own. In the real world, you'd probably
+want to do more complex operations. */
+
+#hd { background-color:#5D0265; }
+.block .hd { background-color:#5D0265; color:#fff; border-bottom:10px solid #262626; }
+.block .bd h2 { color:#5D0265; }
+.block .bd h3 { color:#5D0265; }
+.spaces .hd { background-color:transparent; }
+.spaces .hd ul li { background-color:#5D0265; }
+a { color:#5D0265; }
+.tabs .hd ul li a:hover { background-color:#8d0399; }
+
+/***** Uli Stuff *****/
+
+/* Fix logo image height: as image is taller than h1 text, header area
+   explodes otherwise */
+#logo img {
+  margin-top: -45px;
+  position: relative;
+  top: 12px;
+}
+
+.actionbar { 
+  margin-top: -18px;
+  margin-bottom: 18px;
+}
+
+
+/* Fixes to let tables appear correctly... */
+table, tr, th, td {font-family: arial,sans-serif;}
+
+/* Odd and even table rows */
+tr.even,
+.yui-skin-sam tr.yui-dt-even {
+  background-color: #fff;
+}
+tr.odd,
+.yui-skin-sam tr.yui-dt-odd {
+  background-color: #eee;
+}
+
+/* unselected paginator page numbers */
+.yui-skin-sam .yui-pg-pages a.yui-pg-page {
+  background-color: #fff;
+  color: #000;
+  text-decoration: none;
+  bo/rder: 1px solid #000;
+  padding-left: 5px;
+}
+
+/* selected paginator page, hover for unselected pages */
+.yui-skin-sam .yui-pg-pages a.yui-pg-page:hover,
+.yui-skin-sam span.yui-pg-page {
+  background-color: #000;
+  color: #fff;
+}
+
+/* paginator prev, next, etc. links */
+.yui-skin-sam .yui-dt .yui-dt-paginator a.yui-pg-first,
+.yui-skin-sam .yui-dt .yui-dt-paginator a.yui-pg-previous,
+.yui-skin-sam .yui-dt .yui-dt-paginator a.yui-pg-last,
+.yui-skin-sam .yui-dt .yui-dt-paginator a.yui-pg-next {
+  color: #000;
+  text-decoration: none;
+  background-color: #fff;
+}
+
+/* Header row */
+th .yui-dt-liner {
+  color: #fff;
+  background: #000 url();
+}
+
+/* Header cells text, when not selected */
+.yui-skin-sam .yui-dt th, .yui-skin-sam .yui-dt th a {
+  font-weight: bold;
+  color: #fff;
+}
+
+/* Sortable header cells, when selected */
+.yui-skin-sam th.yui-dt-desc .yui-dt-liner .yui-dt-sortable,
+.yui-skin-sam th.yui-dt-asc .yui-dt-liner .yui-dt-sortable {
+  color: #000;
+}
+
+
+
+/* The follwing is stolen from jquery... */
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; 
+           text-indent: -99999px; overflow: hidden;
+           background-repeat: no-repeat; }
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(ui-icons_222222_256x240.png); }
+.ui-widget-content .ui-icon {background-image: url(ui-icons_222222_256x240.png); }
+.ui-widget-header .ui-icon {background-image: url(ui-icons_ffffff_256x240.png); }
+.ui-state-default .ui-icon { background-image: url(ui-icons_ef8c08_256x240.png); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(ui-icons_ef8c08_256x240.png); }
+.ui-state-active .ui-icon {background-image: url(ui-icons_ef8c08_256x240.png); }
+.ui-state-highlight .ui-icon {background-image: url(ui-icons_228ef1_256x240.png); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(ui-icons_e73434_256x240.png); }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-off { background-position: -96px -144px; }
+.ui-icon-radio-on { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+span.ui-icon {
+               position: relative;
+               float: left;
+               margin-right: 4px;
+               margin-bottom: 0pt;
+               margin-left: -0pt;
+               left: 0.2em;
+               top: 50%;
+}
+a span.ui-icon { 
+                 margin-top: 4px;
+                 }
Index: waeup/trunk/src/waeup/browser/static/waeuptheme.css
===================================================================
--- waeup/trunk/src/waeup/browser/static/waeuptheme.css	(revision 4789)
+++ waeup/trunk/src/waeup/browser/static/waeuptheme.css	(revision 4789)
@@ -0,0 +1,13 @@
+/* This is the bare minimum needed to create your own theme.
+In this example, all we're doing is overriding the default
+color scheme with our own. In the real world, you'd probably
+want to do more complex operations. */
+ 
+#hd { background-color:#840000; }
+.block .hd { background-color:#840000; color:#fff; border-bottom:10px solid #262626; }
+.block .bd h2 { color:#840000; }
+.block .bd h3 { color:#840000; }
+.spaces .hd { background-color:transparent; }
+.spaces .hd ul li { background-color:#840000; }
+a { color:#840000; }
+.tabs .hd ul li a:hover { background-color:#cc0000; }
Index: waeup/trunk/src/waeup/browser/static/yuiapp.css
===================================================================
--- waeup/trunk/src/waeup/browser/static/yuiapp.css	(revision 4789)
+++ waeup/trunk/src/waeup/browser/static/yuiapp.css	(revision 4789)
@@ -0,0 +1,126 @@
+html, body { background-color:#f0f0ee; color:#222; }
+ 
+/* Make the page full bleed */
+#doc3 { margin:0; }
+ 
+/* Header */
+#hd { padding:1.2em 1.5em 0 1.5em; background-color:#7a1818; }
+h1 { font-size:230%; color:#fff; }
+ 
+#navigation { margin-top:1.5em; background-color:#262626; height:1%; }
+#navigation li { float:left; display:inline; }
+#navigation li a { display:block; color:#fff; text-decoration:none; }
+#navigation li a:hover { text-decoration:underline; }
+ 
+#primary-navigation li a { font-size:116%; padding:0.4em 1em; }
+#primary-navigation li.active a { background-color:#f0f0ee; color:#000; }
+ 
+#user-navigation { float:right; }
+#user-navigation li a { font-size:93%; padding:0.5em 1em; }
+ 
+/* Body */
+#bd { padding:1.5em; }
+h2 { font-size:153.9%; margin-bottom:0.5em; }
+h3 { font-size:153.9%; margin-bottom:0.5em; }
+ 
+/* Basic block */
+.block { margin-bottom:1.5em; }
+.block .hd { background-color:#7a1818; padding:0.7em 1em; color:#fff; border-bottom:10px solid #262626; }
+.block .hd h2, .block .hd h3 { font-size:100%; margin-bottom:0; }
+.block .bd { padding:1em; background-color:#fff; }
+.block .bd h2 { color:#7a1818; }
+.block .bd h3 { color:#7a1818; }
+ 
+/* Extend block with tabs */
+.tabs .hd { padding:0; }
+.tabs .hd ul { height:1%; }
+.tabs .hd ul li { float:left; }
+.tabs .hd ul li a { display:block; color:#fff; text-decoration:none; padding:0.5em 1em; }
+.tabs .hd ul li a:hover { background-color:#470E0E; }
+.tabs .hd ul li.active a { background-color:#262626 !important; color:#fff; text-decoration:none; } /* important! fixes IE cascade issue */
+.tabs .hd h2, .tabs .hd h3 { position:absolute; margin-left:-5000px; } /* Hidden SEO Header if needed */
+ 
+/* Extend tab block with spaced tabs */
+.spaces .hd { background-color:transparent; }
+.spaces .hd ul li { background-color:#7a1818; margin-right:0.1em; }
+ 
+/* Extend block and header to have rounded corners */
+.rounded .hd,
+.rounded .hd ul li,
+.rounded .hd ul li a,
+.rounded #navigation,
+.rounded #navigation ul li,
+.rounded #navigation ul li a {
+    -moz-border-radius-topright:7px;
+    -moz-border-radius-topleft:7px;
+    -webkit-border-top-right-radius:7px;
+    -webkit-border-top-left-radius:7px;
+}
+ 
+/* Style the horizontal rules inside .block */
+.block hr { background-color:#f0f0ee; color:#f0f0ee; height:1px; border:0; }
+ 
+/* Tables */
+table { width:100%; margin-bottom:2em; }
+table td, table th { padding:0.5em; }
+table thead { background-color:#262626; color:#fff; font-weight:bold; }
+table thead a { text-decoration:none; color:#fff; }
+table thead a:hover { text-decoration:underline; }
+table tbody a { text-decoration:underline; }
+ 
+table.zebra tbody tr:nth-child(even) {background: #eee}
+table.zebra tbody tr:nth-child(odd) {background: #fff}
+ 
+ 
+ 
+/* Pager control*/
+ul.pager { text-align:right; }
+ul.pager li { display:inline; }
+ul.pager li a { padding:0.1em 0.3em; margin-right:0.3em; border:1px solid #000; text-decoration:none; color:#000; }
+ul.pager li.active a, ul.pager li a:hover { background-color:#000; color:#fff; }
+ 
+ 
+ 
+/* Forms */
+form label { font-weight:bold; }
+form label span { font-weight:normal; color:#f00; font-size:85%; }
+form span.info { font-style:italic; color:#aaa; font-size:85%; }
+form .text { display:block; width:99%; border:1px solid #aaa; padding:0.3em; }
+form .column { width:48%; }
+form .left { float:left; }
+form .right { float:right; }
+ 
+/* Messages */
+.alert { padding:0.5em; text-align:center; }
+.error { border:2px solid #fbb; background-color:#fdd; }
+.warning { border:2px solid #fffaaa; background-color:#ffc; }
+.notice { border:2px solid #1fdf00; background-color:#bbffb6; }
+ 
+/* Lists */
+ol.list, ul.list, dl.list { margin-left:2em; margin-bottom:1em; }
+ol.list li { list-style:decimal outside; }
+ul.list li { list-style:disc outside; }
+dl.list dd { margin-left:1em; }
+ 
+ul.biglist { margin-bottom:1em; }
+ul.biglist li { margin-top:-1px; border:1px solid #f0f0ee; border-width:1px 0 1px 0; }
+ul.biglist li a { display:block; padding:0.5em 0; text-decoration:none; color:#000; }
+ul.biglist li a:hover { text-decoration:underline; }
+ 
+/* Footer */
+#ft { text-align:center; font-size:85%; padding:0.5em 1em; color:#262626; }
+ 
+/* Generics */
+p { margin-bottom:1em; }
+a { color:#7a1818; }
+strong { font-weight:bold; }
+em { font-style:italic; }
+abbr, acronym { border-bottom:1px dotted #000; cursor:help; }
+ 
+.small { font-size:85%; }
+.gray { color:#999; }
+.highlight { background-color:#ffffcc; }
+.clear { clear:both; }
+.text-left { text-align:left; }
+.text-right { text-align:right; }
+.text-center { text-align:center; }
Index: waeup/trunk/src/waeup/browser/templates/actionbutton.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/actionbutton.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/actionbutton.pt	(revision 4789)
@@ -0,0 +1,10 @@
+<span class="yui-button yui-link-button">
+  <span class="first-child">
+    <a href=""
+       tal:attributes="href viewlet/target_url">
+      <img src="" alt="edit"
+	   tal:attributes="src viewlet/icon_url; 
+			   alt viewlet/alt"/>
+    <span tal:content="viewlet/text">TEXT</span></a>
+  </span>
+</span>
Index: waeup/trunk/src/waeup/browser/templates/addaction.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/addaction.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/addaction.pt	(revision 4789)
@@ -0,0 +1,11 @@
+<span class="yui-button yui-link-button">
+  <span class="first-child">
+
+    <a href=""
+       tal:attributes="href viewlet/getTargetURL"
+       id="linkbutton1">
+      <img alt="edit"
+           src="http://localhost:8080/app/@@/waeup/actionicon_addfavorite.png" />
+    <span tal:content="viewlet/addlink_text">Add something</span></a>
+  </span>
+</span>
Index: waeup/trunk/src/waeup/browser/templates/breadcrumbs.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/breadcrumbs.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/breadcrumbs.pt	(revision 4789)
@@ -0,0 +1,14 @@
+<div style="background-color: #fff; padding: 3px ">
+  <span tal:repeat="item viewlet/getEntries">
+    <a href="#"
+       tal:attributes="href item/url">
+      <span tal:content="item/title">
+	Entry
+      </span>
+    </a>
+    <span tal:condition="not: repeat/item/end">
+      >
+    </span>
+  </span>
+</div>
+<p />
Index: waeup/trunk/src/waeup/browser/templates/certificatepage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/certificatepage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/certificatepage.pt	(revision 4789)
@@ -0,0 +1,82 @@
+<h2 tal:content="context/title">TITLE</h2>
+
+<table class="listing">
+  <thead>
+  </thead>
+  <tbody>
+    <tr class="even">
+      <td>Title:</td>
+      <td tal:content="context/title">THE TITLE</td>
+    </tr>
+    <tr class="odd">
+      <td>Start Level:</td>
+      <td tal:content="context/start_level">START_LEVEL</td>
+    </tr>
+    <tr class="even">
+      <td>End Level:</td>
+      <td tal:content="context/end_level">END_LEVEL</td>
+    </tr>
+    <tr class="odd">
+      <td>Application Category:</td>
+      <td tal:content="context/category">CATEGORY</td>
+    </tr>
+    <tr class="even">
+      <td>Study Mode:</td>
+      <td tal:content="context/study_mode">STUDY_MODE</td>
+    </tr>
+    <tr class="odd">
+      <td>School Fee Prefix:</td>
+      <td tal:content="context/m_prefix">SCHOOL_FEE</td>
+    </tr>
+  </tbody>
+</table>
+
+<h3>Courses</h3>
+
+
+<div tal:repeat="level python: sorted(view.getCourseList().items())">
+  <b>
+    Level <span tal:replace="python: level[0]">100</span>
+  </b>
+
+  <div tal:repeat="semester python: sorted(level[1].items())">
+    <div tal:condition="python: semester[0] == '0'">
+      &nbsp;&nbsp;Untimed courses
+    </div>
+    <div tal:condition="python: semester[0] == '1'">
+      &nbsp;&nbsp;First semester
+    </div>
+    <div tal:condition="python: semester[0] == '2'">
+      &nbsp;&nbsp;Second semester
+    </div>
+    <div tal:condition="python: semester[0] == '3'">
+      &nbsp;&nbsp;Combined
+    </div>
+    
+    <div tal:repeat="course python:semester[1]">
+      &nbsp;&nbsp;&nbsp;&nbsp;
+	<form method="POST">
+	  <input type="hidden" name="code" 
+		 value="" tal:attributes="value course/code" />
+	  <input type="hidden" name="level" 
+		 value="" tal:attributes="value python:level[0]" />
+	  <input type="submit" name="delcourse" value="delete" />
+
+      <a href="" 
+	 tal:attributes="href course/url"
+	 ><span tal:content="course/code">code</span>
+      </a>
+      <span tal:content="course/title">title</span>
+      <span tal:condition="course/mandatory">
+	[mandatory]
+      </span>
+      <span tal:condition="not: course/mandatory">
+	[optional]
+      </span>
+	</form>
+    </div>
+  </div>
+
+</div>
+
+
Index: waeup/trunk/src/waeup/browser/templates/datacenterimportcsvpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/datacenterimportcsvpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/datacenterimportcsvpage.pt	(revision 4789)
@@ -0,0 +1,56 @@
+<h2>Import data from CSV file</h2>
+
+<div tal:define="imports view/getImports">
+  <div tal:condition="python: len(imports)==0">
+    Currently no files are available for import.
+  </div>
+  <div tal:condition="python: len(imports)>0">
+
+    <p>
+      The following files are available for import:
+    </p>
+
+    <table>
+      <thead>
+	<th>File</th><th>Importer</th><th>Erase old content?</th>
+      </thead>
+      <tbody>
+
+	<tr tal:repeat="import imports">
+	  <form method="POST">
+	    <td tal:content="import/file/name">filename</td>
+	    <td>
+	      <select name='receiverid'>
+		<option tal:repeat="importer import/importers"
+			tal:attributes="value importer/receiverid"
+			tal:content="importer/datatype">
+		  datatype
+		</option>
+	      </select>
+	    </td>
+	    <td>
+	      <input name="clear" type="checkbox" checked="checked" />
+	    </td>
+	    <td>
+	      <input type="hidden" name="csvfile"
+		     tal:attributes="value import/file/context" />
+	      <input type="submit" name="SUBMIT" value="Import!" />
+	      
+	    </td>
+	  </form>
+	</tr>
+   
+      </tbody>
+    </table>
+
+    <div>
+      Note, that importing will most probably destroy existing data!
+    </div>
+
+
+  </div>
+
+  <form method="POST">
+    <input type="submit" name="CANCEL" value="Cancel" />
+  </form>
+</div>
Index: waeup/trunk/src/waeup/browser/templates/datacentermanagepage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/datacentermanagepage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/datacentermanagepage.pt	(revision 4789)
@@ -0,0 +1,22 @@
+<h1>Data Center Settings</h1>
+
+<form method="POST">
+  <div>
+    <label for="newpath">Storage path:</label>
+    <input type="text" name="newpath"
+	   tal:attributes="value context/storage" />
+    <br />
+    <div>
+      <input type="checkbox" name="move" checked="checked" />
+      <label for="move">Copy existing data to new storage?</label>
+    </div>
+    <div>
+      <input type="checkbox" name="overwrite" checked="no" />
+      <label for="overwrite">Overwrite contents in new location?</label>
+    </div>
+
+    <input type="submit" name="save" value="Save" />
+    <input type="reset" name="reset" value="Reset" />
+    <input type="submit" name="cancel" value="Cancel" />
+  </div>
+</form>
Index: waeup/trunk/src/waeup/browser/templates/datacenterpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/datacenterpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/datacenterpage.pt	(revision 4789)
@@ -0,0 +1,33 @@
+<h2>Data Center</h2>
+
+<p>
+The data center helps you to manage portal data. You can upload CSV
+files here, which will be available for import afterwards.
+</p>
+
+<p>
+  <b>Storage path:</b> <span tal:content="context/storage">/foo/bar</span>
+</p>
+
+<table>
+  <thead>
+    <tr>
+      <th>File</th>
+      <th>Size</th>
+      <th>Uploaded</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="file context/getFiles"
+	tal:attributes="class python: repeat['file'].odd() and 'odd' or 'even'">
+      <td tal:content="file/name">thefilename</td>
+      <td tal:content="file/size">12 k</td>
+      <td tal:content="file/uploaddate">Mar 12, 2008</td>
+    </tr>
+  </tbody>
+</table>
+
+<div>
+  <a href='@@upload'>Upload data</a>
+  <a href='@@importcsv'>Import CSV data</a>
+</div>
Index: waeup/trunk/src/waeup/browser/templates/datacenteruploadpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/datacenteruploadpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/datacenteruploadpage.pt	(revision 4789)
@@ -0,0 +1,9 @@
+<h1>Upload a file</h1>
+
+<form method="post" enctype="multipart/form-data">
+  <input type="file" name="uploadfile:file" />
+  <div>
+    <input type="submit" name="SUBMIT" value="Upload" />
+    <input type="submit" name="CANCEL" value="Cancel" />
+  </div>
+</form>
Index: waeup/trunk/src/waeup/browser/templates/departmentpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/departmentpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/departmentpage.pt	(revision 4789)
@@ -0,0 +1,26 @@
+<script type="text/javascript"> 
+  var tabView = new YAHOO.widget.TabView('table-tabs'); 
+</script> 
+<div id="table-tabs" class="yui-navset"> 
+  <ul class="yui-nav"> 
+    <li class="selected"><a href="#tab1"><em>Courses</em></a></li> 
+    <li><a href="#tab2"><em>Certificates</em></a></li> 
+  </ul>             
+  <div class="yui-content"> 
+    <div id="tab1">
+	
+      <div name="thecoursetable"
+	   tal:condition="python: len(context.courses.keys())">
+	<div tal:replace="structure view/coursetable/renderHTML" />
+      </div>
+
+    </div> 
+    <div id="tab2">
+
+      <div name="thecerttable">
+	<div tal:replace="structure view/certificatetable/renderHTML" />
+      </div>
+
+    </div> 
+  </div> 
+</div>
Index: waeup/trunk/src/waeup/browser/templates/exportxmlaction.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/exportxmlaction.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/exportxmlaction.pt	(revision 4789)
@@ -0,0 +1,8 @@
+<span class="yui-button yui-link-button" noclass="actionbarlink">
+  <span class="first-child">
+    <a href="" id="linkbutton1"
+       tal:attributes="href python: view.url(context) + '/export.xml'">
+      <img alt="export as XML"
+	   tal:attributes="src static/actionicon_unpublish.png"/>
+  <span>Export as XML</span></a></span>
+</span>
Index: waeup/trunk/src/waeup/browser/templates/facultycontainermanagepage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/facultycontainermanagepage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/facultycontainermanagepage.pt	(revision 4789)
@@ -0,0 +1,41 @@
+  <h2>Registered faculties:</h2>
+
+
+  <div tal:condition="python: not len(context.keys())">
+    There are no faculties registered yet.
+  </div>
+  <form method="POST" tal:condition="python: len(context.keys())">
+    <table>
+      <tr tal:repeat="fac context/values"
+	  tal:attributes="class python: repeat['fac'].odd() and 'odd' or 'even'">
+	<td>
+	  <input type="checkbox"
+		 name="fac_id"
+		 tal:attributes="value fac/code" />
+	</td>
+	<td>
+	  <a href="."
+	     tal:content="fac/code"
+	     tal:attributes="href python:view.url(fac)">
+	    FacultyCode
+	  </a>
+	</td>
+	<td>
+	  <a href="."
+	     tal:content="fac/title"
+	     tal:attributes="href python:view.url(fac)">
+	    Faculty Name
+	  </a>
+	</td>
+      </tr>
+    </table>
+    <input type="submit" name="DELETE" value="Delete selected" />
+    <input type="submit" name="CANCEL" value="Cancel" />
+  </form>
+
+  <div>
+    <form method="POST" 
+	  tal:attributes="action string:add">
+      <input type="submit" name="addfaculty" value="Add" />
+    </form>
+  </div>
Index: waeup/trunk/src/waeup/browser/templates/facultycontainerpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/facultycontainerpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/facultycontainerpage.pt	(revision 4789)
@@ -0,0 +1,9 @@
+  <h2>Registered Faculties</h2>
+
+  <div tal:condition="python: not len(context.keys())">
+    There are no faculties registered yet.
+  </div>
+
+  <div name="thefacultytable" tal:condition="python: len(context.keys())">
+    <div tal:replace="structure view/table/renderHTML" />
+  </div>
Index: waeup/trunk/src/waeup/browser/templates/facultypage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/facultypage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/facultypage.pt	(revision 4789)
@@ -0,0 +1,10 @@
+  <h2>Registered Departments:</h2>
+
+
+  <div tal:condition="python: not len(context.keys())">
+    There are no departments registered yet.
+  </div>
+
+  <div name="thedepartmenttable" tal:condition="python: len(context.keys())">
+    <div tal:replace="structure view/table/renderHTML" />
+  </div>
Index: waeup/trunk/src/waeup/browser/templates/gotofacultiesaction.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/gotofacultiesaction.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/gotofacultiesaction.pt	(revision 4789)
@@ -0,0 +1,10 @@
+<span class="yui-button yui-link-button" noclass="actionbarlink">
+  <span class="first-child">
+    <a href="" id="linkbutton1"
+       tal:attributes="href python: view.url(
+		       context, 'faculties')">
+      <img alt="edit"
+	   tal:attributes="src static/actionicon_manage.png"/>
+    <span>Browse faculties</span></a>
+  </span>
+</span>
Index: waeup/trunk/src/waeup/browser/templates/importxmlaction.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/importxmlaction.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/importxmlaction.pt	(revision 4789)
@@ -0,0 +1,9 @@
+<span tal:condition="viewlet/can_import"
+      class="yui-button yui-link-button" noclass="actionbarlink">
+  <span class="first-child">
+    <a href="" id="linkbutton1"
+       tal:attributes="href python: view.url(context) + '/importxml'">
+      <img alt="import from XML"
+	   tal:attributes="src static/actionicon_submit.png"/>
+  <span>Import from XML</span></a></span>
+</span>
Index: waeup/trunk/src/waeup/browser/templates/importxmlpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/importxmlpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/importxmlpage.pt	(revision 4789)
@@ -0,0 +1,17 @@
+<div>
+  <h2>Import from XML source</h2>
+
+  <div>
+    Please specify an XML source file
+  </div>
+
+  <div>
+    <form method="post" enctype="multipart/form-data">
+      <input type="file" name="xmlfile:file" />
+      <div>
+	<input type="submit" name="SUBMIT" value="Upload" />
+	<input type="submit" name="CANCEL" value="Cancel" />
+      </div>
+    </form>
+  </div>
+</div>
Index: waeup/trunk/src/waeup/browser/templates/login.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/login.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/login.pt	(revision 4789)
@@ -0,0 +1,13 @@
+<div class="portlet" 
+     tal:condition="python: request.principal.id == 'zope.anybody'">
+    <h3>Login</h3>
+    <form method="post">
+      User<br /><input type="text" name="form.login" size="20" /><br />
+      Password<br /><input type="password" name="form.password" size="20" />
+      <br />
+      <input type="submit" name="SUBMIT" value="Login" />
+    </form>
+    <a href="@@loginstaff">
+      <img tal:attributes="src static/login.png" /> Staff Login
+    </a>
+</div>
Index: waeup/trunk/src/waeup/browser/templates/loginpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/loginpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/loginpage.pt	(revision 4789)
@@ -0,0 +1,41 @@
+    <form method="post">
+
+      <table id="login" class="form" 
+	     summary="Table for entering login information">
+	<tbody>
+	  <tr>
+	    <th>
+	      <label for="form.login">User Name</label>
+	    </th>
+	    <td>
+	      <input type="text" id="login" name="form.login" size="20" />
+	    </td>
+	  </tr>
+
+	  <tr>
+	    <th>
+	      <label for="form.password">Password
+	    </label>
+	    </th>
+	    <td>
+	      <input type="password" id="password" name="form.password"
+		     size="20" value="" />
+	      &nbsp;&nbsp; 
+	      <font color="red">
+		Notice: Passwords are case sensitive.
+	      </font>   
+	    </td>
+	  </tr>
+	</tbody>
+      </table>
+
+      <input type="hidden" name="camefrom" 
+	     tal:attributes="value view/camefrom" />
+
+      <input type="submit" name="SUBMIT" value="Login" />
+
+      <p>Don't forget to logout or exit your browser when you're done.</p>
+      <p>If you are having trouble logging in, make sure to enable
+      cookies in your web browser.</p>
+
+    </form>
Index: waeup/trunk/src/waeup/browser/templates/loginstaffpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/loginstaffpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/loginstaffpage.pt	(revision 4789)
@@ -0,0 +1,50 @@
+<div class="portlet" 
+     tal:condition="python: request.principal.id == 'zope.anybody'">
+    <h3>Staff Login</h3>
+    <div>
+      You've chosen the the portal's 'back entrance' to log in. This
+      entrance is dedicated to university staff members to maintain
+      the portal's database.
+    </div>
+    <form method="post">
+
+      <table id="login" class="form" 
+	     summary="Table for entering login information">
+	<tbody>
+	  <tr>
+	    <th>
+	      <label for="form.login">User Name</label>
+	    </th>
+	    <td>
+	      <input type="text" id="login" name="form.login" size="20" />
+	    </td>
+	  </tr>
+
+	  <tr>
+	    <th>
+	      <label for="form.password">Password
+	    </label>
+	    </th>
+	    <td>
+	      <input type="password" id="password" name="form.password"
+		     size="20" value="" />
+	      &nbsp;&nbsp; 
+	      <font color="red">
+		Notice: Passwords are case sensitive.
+	      </font>   
+	    </td>
+	  </tr>
+	</tbody>
+      </table>
+
+      <!-- input type="hidden" name="camefrom" 
+	     tal:attributes="value view/camefrom or nothing" / -->
+
+      <input type="submit" name="SUBMIT" value="Login" />
+
+      <p>Don't forget to logout or exit your browser when you're done.</p>
+      <p>If you are having trouble logging in, make sure to enable
+      cookies in your web browser.</p>
+
+    </form>
+</div>
Index: waeup/trunk/src/waeup/browser/templates/loginstudentpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/loginstudentpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/loginstudentpage.pt	(revision 4789)
@@ -0,0 +1,67 @@
+<div class="portlet" 
+     tal:condition="python: request.principal.id == 'zope.anybody'">
+    <h3>Student Login</h3>
+    <p>
+      To access the registration portal enter your Student Id and
+      Password.
+    </p>
+    <p>
+      The <strong>Student Id</strong> (initial capital letter followed
+      by 6 numbers) has been automatically assigned by the portal
+      after creation of the student record. Do not confuse with your
+      Registration or Matriculation Number!
+    </p>
+  
+    <p>
+      During the admission and clearance process the
+      <strong>Password</strong> corresponds with the last 10 digits of
+      your Admission Checking PIN (prefix APP).
+    </p>
+
+    <div>
+      You've chosen the the portal's 'back entrance' to log in. This
+      entrance is dedicated to university staff members to maintain
+      the portal's database.
+    </div>
+    <form method="post">
+
+      <table id="login" class="form" 
+	     summary="Table for entering login information">
+	<tbody>
+	  <tr>
+	    <th>
+	      <label for="form.login">User Name</label>
+	    </th>
+	    <td>
+	      <input type="text" id="login" name="form.login" size="20" />
+	    </td>
+	  </tr>
+
+	  <tr>
+	    <th>
+	      <label for="form.password">Password
+	    </label>
+	    </th>
+	    <td>
+	      <input type="password" id="password" name="form.password"
+		     size="20" value="" />
+	      &nbsp;&nbsp; 
+	      <font color="red">
+		Notice: Passwords are case sensitive.
+	      </font>   
+	    </td>
+	  </tr>
+	</tbody>
+      </table>
+
+      <!-- input type="hidden" name="camefrom" 
+	     tal:attributes="value view/camefrom or nothing" / -->
+
+      <input type="submit" name="SUBMIT" value="Login" />
+
+      <p>Don't forget to logout or exit your browser when you're done.</p>
+      <p>If you are having trouble logging in, make sure to enable
+      cookies in your web browser.</p>
+
+    </form>
+</div>
Index: waeup/trunk/src/waeup/browser/templates/logoutpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/logoutpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/logoutpage.pt	(revision 4789)
@@ -0,0 +1,2 @@
+<div>
+</div>
Index: waeup/trunk/src/waeup/browser/templates/manageaction.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/manageaction.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/manageaction.pt	(revision 4789)
@@ -0,0 +1,9 @@
+<span class="yui-button yui-link-button" noclass="actionbarlink">
+  <span class="first-child">
+    <a href="" id="linkbutton1"
+       tal:attributes="href python: view.url(context) + '/@@manage'">
+      <img alt="edit"
+	   tal:attributes="src static/actionicon_modify.png"/>
+    <span>Edit settings</span></a>
+  </span>
+</span>
Index: waeup/trunk/src/waeup/browser/templates/primarynavtab.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/primarynavtab.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/primarynavtab.pt	(revision 4789)
@@ -0,0 +1,7 @@
+<li tal:attributes="class viewlet/active">
+  <a href="#"
+     tal:attributes="href viewlet/link_target"
+     tal:content="viewlet/tab_title">
+    Primary Tab Title
+  </a>
+</li>
Index: waeup/trunk/src/waeup/browser/templates/searchpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/searchpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/searchpage.pt	(revision 4789)
@@ -0,0 +1,17 @@
+<h3>Search the site</h3>
+<form method="post">
+  <input type="text" name="query"
+	 tal:attributes="value view/query" />
+    <input type="submit" name="SUBMIT" value="search..." />
+</form>
+<div tal:condition="view/hitlist">
+  <h4>Results (<span tal:replace="python: len(view.hitlist)" />):</h4>
+  <div tal:repeat="item view/hitlist">
+    <a href=""
+       tal:attributes="href item/url"
+       tal:content="item/title">Search Item Title</a>
+    <div tal:content="item/description">
+      Longer description of item...
+    </div>
+  </div>
+</div>
Index: waeup/trunk/src/waeup/browser/templates/searchstudentpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/searchstudentpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/searchstudentpage.pt	(revision 4789)
@@ -0,0 +1,14 @@
+<html>
+<body>
+  <form>
+    <div>
+      Search: 
+      <input type='text' name='query' />
+    </div>
+    <input type="submit" value="Search" />
+  </form>
+  <div tal:repeat="item view/search_result"
+       tal:content="item/name" >
+  </div>
+</body>
+</html>
Index: waeup/trunk/src/waeup/browser/templates/sitelayout.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/sitelayout.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/sitelayout.pt	(revision 4789)
@@ -0,0 +1,116 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+	  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+  <head>
+    <title>WAeUP - your way up</title>
+    <!-- base href="http://localhost:8080/app/@@page" / -->
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+    <meta name="robots" content="index, follow" />
+    <!-- tal:head content="structure provider:head" / -->
+
+    <link rel="shortcut icon" tal:attributes="href static/favicon.ico" 
+	  type="image/x-icon" />
+
+    <script type="text/javascript" src=""
+	    tal:attributes="src python: view.url(context, '@@yuistatictables.js')">
+    </script>
+
+    <link rel="stylesheet" type="text/css" href=""
+	  tal:attributes="href static/yuiapp.css" />
+    <link rel="stylesheet" type="text/css" href=""
+	  tal:attributes="href static/purple.css" />
+    <link rel="stylesheet" type="text/css" href=""
+	  tal:attributes="href static/waeuptheme.css" />
+
+  </head>
+  <body class="rounded yui-skin-sam">
+    <div id="doc3" class="yui-t2">
+      <div id="hd" role="banner">
+	<div id="hd">
+
+	  <h1 id="logo">
+	    <a href=""
+	       tal:attributes="href view/application_url">
+	      <img tal:attributes="src static/logo_waeup.gif" 
+		   alt="WAeUp logo" />
+	    </a>
+	    <span id="uni-title" tal:content="layout/getAppTitle">
+	      Sample University
+	    </span>
+	  </h1>
+
+	  <div id="navigation" class="yui-navset" style="text-align:right;">
+
+	    <ul id="primary-navigation">
+	      <tal:primary_nav content="structure provider:primary_nav" />
+	    </ul>
+
+	    <ul id="user-navigation"
+		tal:condition="layout/isAuthenticated">
+	      <li><a href="#">Profile</a></li>
+	      <li><a href="#">Settings</a></li>
+	      <li>
+		<a href="#"
+		   tal:attributes="href python: view.url(context,
+		   '@@logout')">Logout</a>
+	      </li>
+	    </ul>
+	    <div class="clear"></div>
+	    
+	  </div>
+	</div>
+	
+      </div>
+      <div id="bd" role="main">
+	<div id="yui-main">
+	  <div class="yui-b"><div class="yui-g">
+	      
+	      <tal:breadcrumbs content="structure provider:breadcrumbs" />
+
+	      <div class="block">
+		<div class="hd">
+		  <h2 tal:content="view/title"> 
+		    THE CONTENT TITLE
+		  </h2>
+		</div>
+		<div class="bd">
+		  <div class="actionbar">
+		    <tal:actionbar content="structure provider:actionbar" />
+		  </div>
+
+		  <div class="alert warning ui-state-error"
+		       tal:define="message context/@@messages"
+		       tal:condition="python: len(message)>11">
+		    <span class="ui-icon ui-icon-alert">A</span>
+		    <span tal:content="structure message"></span>
+		  </div>
+
+		  <div tal:replace="structure view/content">
+		    THE CONTENT
+		  </div>
+
+		</div>
+	      </div>
+
+	    </div>
+	  </div>
+	</div>
+	<div class="yui-b">
+	  <div class="block">
+	    <div class="hd">
+	      <h2 tal:content="layout/getUserTitle">Username</h2>
+	    </div>
+	    <div class="bd">
+	      <tal:left content="structure provider:left" />
+	    </div>
+	  </div>
+	</div>
+      </div>
+
+      <div id="ft" role="contentinfo">
+	<p>powered by WestAfrican eUniversity Portal, 
+	copyright &copy; 2008-2010 WAeUP Germany.</p>
+      </div>
+    </div>
+  </body>
+</html>
Index: waeup/trunk/src/waeup/browser/templates/universitypage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/universitypage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/universitypage.pt	(revision 4789)
@@ -0,0 +1,30 @@
+
+<h2> 
+  Welcome to the WAeUP SRP of <span tal:replace="context/name">NAME</span>
+</h2>
+
+<p>
+  This is the default frontpage of the Student Registration Portal
+  (SRP) which is part of the West African e-University Project
+  (<a href="http://www.waeup.org/">WAeUP</a>).
+</p>
+
+<p>
+  First thing to do after installation is to set a new location
+  for data files (imports/exports) in the <a
+  href="datacenter">DataCenter</a>.
+</p>
+
+<p>
+  You can tweak the layout starting with the page template
+  <code>universitypage.pt</code> in
+  <code>waeup/browser/templates/</code>.
+</p>
+
+<!--
+    <div name="tableexample" class="yui-skin-sam">
+    <div tal:replace="structure view/table/renderHTML" />
+    </div>
+-->
+
+
Index: waeup/trunk/src/waeup/browser/templates/usercontainerpage.pt
===================================================================
--- waeup/trunk/src/waeup/browser/templates/usercontainerpage.pt	(revision 4789)
+++ waeup/trunk/src/waeup/browser/templates/usercontainerpage.pt	(revision 4789)
@@ -0,0 +1,31 @@
+<h2>Registered Users</h2>
+
+<table class="zebra">
+  <thead>
+    <tr>
+      <th>Username</th>
+      <th>Title</th>
+      <th>Description</th>
+      <th>Actions</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="account context/values"
+	tal:attributes="class python: repeat['account'].odd() and 'odd' or 'even'">
+      <td tal:content="account/name">username</td>
+      <td tal:content="account/title">Title</td>
+      <td tal:content="account/description">Description</td>
+      <td class="text-right">
+	<form method="post">
+	  <input type="hidden" name="userid" 
+		 tal:attributes="value account/name"/>
+	  <input type="submit" name="edit" value="edit" />
+	  <input type="submit" name="delete" value="delete" />
+	</form>
+      </td>
+    </tr>
+  </tbody>
+</table>
+<form method="post" action="add">
+  <input type="submit" name="adduser" value="Add user" />
+</form>
Index: waeup/trunk/src/waeup/browser/viewlets.py
===================================================================
--- waeup/trunk/src/waeup/browser/viewlets.py	(revision 4789)
+++ waeup/trunk/src/waeup/browser/viewlets.py	(revision 4789)
@@ -0,0 +1,447 @@
+# -*- coding: utf-8 -*-
+
+import grok
+
+from hurry import yui
+from zope.component import getMultiAdapter
+from zope.location.interfaces import ISite
+from waeup.browser.pages import (UniversityPage, FacultyContainerPage,
+                                 DatacenterPage, FacultyPage, DepartmentPage,
+                                 CoursePage, CertificatePage,
+                                 CertificateCoursePage)
+from waeup.browser.interfaces import IBreadcrumbContainer
+from waeup.interfaces import (IUniversity, IWAeUPObject, IWAeUPXMLExporter,
+                              IWAeUPXMLImporter, IFacultyContainer, IFaculty,
+                              IDepartment, ICourse, ICertificate,
+                              ICertificateCourse, IDataCenter)
+
+grok.templatedir('templates')
+grok.context(IWAeUPObject) # Make IWAeUPObject the default context
+
+class LeftSidebar(grok.ViewletManager):
+    grok.name('left')
+
+class BreadCrumbManager(grok.ViewletManager):
+    grok.name('breadcrumbs')
+
+class ActionBar(grok.ViewletManager):
+    grok.name('actionbar')
+    pass
+
+#
+# Baseclasses that give some defaults for really used viewlets.
+#
+class ActionButton(grok.Viewlet):
+    """A base for action buttons.
+
+    An action button provides an icon, some text and links to a
+    target.  If you want to set a different text, icon or target name
+    for some active button below, just override the approriate
+    attribute in the concerned viewlet.
+
+    Action buttons provide by default dynamic attributes
+
+     * ``alt``
+          An alternative text for the icon. By default the same as
+          the text.
+
+     * ``icon_url``
+          The URL of the icon.
+
+     * ``target_url``
+          The URL of the link target.
+
+    """
+    grok.baseclass()
+    grok.context(IWAeUPObject)
+    grok.template('actionbutton')
+    grok.viewletmanager(ActionBar)
+    icon = 'actionicon_modify.png' # File must exist in static/
+    target = '@@manage' # link to this viewname.
+    text = 'Edit settings' # Text to display on the button
+
+    @property
+    def alt(self):
+        """Alternative text for icon.
+        """
+        return self.text
+
+    @property
+    def icon_url(self):
+        """Get the icon URL.
+        """
+        return self.view.static[self.icon]()
+
+    @property
+    def target_url(self):
+        """Get a URL to the target...
+        """
+        return self.view.url(self.view.context, self.target)
+
+    
+class ManageActionButton(ActionButton):
+    """A base for 'edit' buttons
+    """
+    grok.baseclass()
+    grok.order(2)
+    grok.require('waeup.manageUniversity')
+    icon = 'actionicon_modify.png'
+    target = '@@manage'
+    text = 'Edit settings'
+
+class AddActionButton(ActionButton):
+    """A base for 'add' buttons.
+    """
+    grok.baseclass()
+    grok.order(4)
+    grok.require('waeup.manageUniversity')
+    icon = 'actionicon_addfavorite.png'
+    target = 'add'
+    text = 'Add'
+
+
+#
+# General viewlets (for more than one page/context)
+#
+
+class BreadCrumbs(grok.Viewlet):
+    grok.context(IWAeUPObject)
+    grok.viewletmanager(BreadCrumbManager)
+    grok.order(1)
+
+    def getEntries(self):
+        result = []
+        site = grok.getSite()
+        context = self.context
+        breadcrumbs = IBreadcrumbContainer(self.view)
+        for breadcrumb in breadcrumbs:
+            yield dict(
+                title = breadcrumb.title,
+                url = self.view.url(breadcrumb.context, breadcrumb.viewname)
+                )
+
+
+# Problem with circular references. Disabled for now...
+# class ExportXMLAction(grok.Viewlet):
+#    grok.viewletmanager(ActionBar)
+#     #grok.view(Index)
+#     grok.order(98)
+#     grok.require('waeup.manageUniversity')
+
+#class ImportXMLAction(grok.Viewlet):
+#    grok.viewletmanager(ActionBar)
+#    #grok.view(Index)
+#    grok.order(99)
+#    grok.require('waeup.manageUniversity')
+#
+#    def update(self):
+#        # We cannot simply replace local sites.
+#        self.can_import = not ISite.providedBy(self.context)
+
+class LeftSidebarLink(grok.Viewlet):
+    """ An entry on left sidebar.
+
+    This is only a baseclass that won't be rendered actually. Deriving
+    viewlets can override certain values and will be rendered with the
+    values set here as default.
+    """
+    grok.baseclass()
+    grok.viewletmanager(LeftSidebar)
+    grok.context(IWAeUPObject)
+    grok.order(5)
+    grok.require('waeup.manageUniversity')
+
+    title = 'Text of link'
+    @property
+    def url(self):
+        return 'URL to link to'
+    
+    def render(self):
+        return u'<div class="portlet"><a href="%s">%s</a></div>' % (
+            self.url, self.title)
+
+
+            
+#
+# waeup.app.University viewlets...
+#
+class Login(grok.Viewlet):
+    """This viewlet allows to login in the sidebar.
+    """
+    grok.viewletmanager(LeftSidebar)
+    grok.context(IUniversity)
+    grok.view(UniversityPage)
+    grok.order(2)
+
+    def isAuthenticated(self):
+        """Is the calling user authenticated?
+        """
+        return self.request.principal.id != 'zope.anybody'
+
+class ManageLink(grok.Viewlet):
+    grok.viewletmanager(LeftSidebar)
+    grok.context(IUniversity)
+    grok.view(UniversityPage)
+    grok.order(5)
+    # This link is only displayed, if the user is
+    # allowed to use it!
+    grok.require('waeup.manageUniversity')
+    
+    def render(self):
+        return u'<div class="portlet"><a href="manage">Settings</a></div>'
+
+class ManageFacultiesLink(grok.Viewlet):
+    grok.viewletmanager(LeftSidebar)
+    grok.context(IUniversity)
+    grok.view(UniversityPage)
+    grok.order(6)
+    # This is so cool! This link is only displayed, when the user is
+    # allowed to use it!
+    grok.require('waeup.manageUniversity')
+    
+    def render(self):
+        return u'<div class="portlet"><a href="faculties">Manage faculties</a></div>'
+
+class ManageUsersLink(grok.Viewlet):
+    grok.viewletmanager(LeftSidebar)
+    grok.context(IUniversity)
+    grok.view(UniversityPage)
+    grok.order(6)
+    grok.require('waeup.manageUsers')
+
+    def render(self):
+        return u'<div class="portlet"><a href="users">Manage users</a></div>'
+
+class ManageDataCenter(grok.Viewlet):
+    grok.viewletmanager(LeftSidebar)
+    grok.context(IUniversity)
+    grok.view(UniversityPage)
+    grok.order(6)
+    grok.require('waeup.manageUniversity')
+
+    def render(self):
+        return u'<div class="portlet"><a href="datacenter">Data Center</a></div>'
+
+#
+# Manage ("Edit settings") actions...
+#
+class ManageUniversityActionButton(ManageActionButton):
+    """ 'Edit settings' button for University instances.
+    """
+    grok.context(IUniversity)
+    grok.view(UniversityPage)
+    
+class ManageFacultiesActionButton(ManageActionButton):
+    """ 'Edit settings' button for faculty container.
+    """
+    grok.context(IFacultyContainer)
+    grok.view(FacultyContainerPage)
+
+class ManageDataCenterActionButton(ManageActionButton):
+    """ 'Edit settings' button for datacenter.
+    """
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    
+class ManageFacultyActionButton(ManageActionButton):
+    """ 'Edit settings' button for faculties.
+    """
+    grok.context(IFaculty)
+    grok.view(FacultyPage)
+
+class ManageDepartmentActionButton(ManageActionButton):
+    """ 'Edit settings' button for departments.
+    """
+    grok.context(IDepartment)
+    grok.view(DepartmentPage)
+
+class ManageCourseActionButton(ManageActionButton):
+    """ 'Edit settings' button for courses.
+    """
+    grok.context(ICourse)
+    grok.view(CoursePage)
+    
+class ManageCertificateActionButton(ManageActionButton):
+    """ 'Edit settings' button for certificates.
+    """
+    grok.context(ICertificate)
+    grok.view(CertificatePage)
+
+class ManageCertificateCourseActionButton(ManageActionButton):
+    """ 'Edit settings' button for certificate courses.
+    """
+    grok.context(ICertificateCourse)
+    grok.view(CertificateCoursePage)
+
+#
+# Add actions...
+#
+class AddAction(grok.Viewlet):
+    """Base class for 'adding actions'.
+
+    AddActions are viewlets that render a button linked to an adding
+    page. To create such a button in one of your pages, follow the
+    following steps:
+
+    - Create a new class derived from AddAction. In this class:
+    
+    - Set a grok.context() for the context type of your page. So, if
+      your page sets grok.context(IMyFunnyType), do the same in the
+      viewlet class.
+
+    - Set the Page on which the button should appear using
+      grok.view(). So, if your page class is MyFunnyPage, do
+      grok.view(MyFunnyPage).
+
+    - Set the template to use to 'addaction'. This means to use the
+      'addaction.pt' template in the templates/ dir. Don't fiddle
+      around with this template file, as it is used also by other
+      viewlets.
+
+    - If you want only certain authorized users to see the button, use
+      grok.require(<Permission>). By default only people with
+      waeup.manageUniversity permission are able to see your new
+      button. Other users, however, will still see your page, only
+      without the button rendered.
+
+    - As one can define several buttons for a certain page, you can
+      influence the order of buttons by setting grok.order(<num>). The
+      lower the number, the more left the button will be displayed and
+      vice versa. Default is 4.
+      
+    - Set the page name as ``addview_name`` attribute. This means, if
+      the page, the button is linked to can be reached by calling
+      http://localhost:8080/<current_location>/@@addfoo, then you should set::
+
+        addview_name = 'addfoo'
+
+      By default the target page name 'add' is assumed. You should
+      have good reasons not to name your AddFormPage 'add'.
+
+    - Set the button text by setting the ``addlink_text``
+      attribute. If you set it to 'Add foobar', then on the page will
+      appear a button with a small 'add stuff' icon and the text 'Add
+      foobar'.
+    """
+    grok.viewletmanager(ActionBar)
+    grok.view(FacultyContainerPage)
+    grok.context(IFacultyContainer)
+    grok.order(4)
+    grok.template('addaction')
+    grok.require('waeup.manageUniversity')
+    grok.baseclass()
+
+    addview_name = 'add'
+    addlink_text = 'Add department'
+
+    def getTargetURL(self):
+        return '%s/@@%s' % (self.view.url(self.view.context), self.addview_name)
+
+class AddFacultyActionButton(AddActionButton):
+    grok.context(IFacultyContainer)
+    grok.view(FacultyContainerPage)
+    text = 'Add faculty'
+    
+class AddDepartmentActionButton(AddActionButton):
+    grok.context(IFaculty)
+    grok.view(FacultyPage)
+    text = 'Add department'
+
+class AddCertificateActionButton(AddActionButton):
+    grok.context(IDepartment)
+    grok.view(DepartmentPage)
+    grok.order(3)
+    text = 'Add certificate'
+    target = 'addcertificate'
+    
+class AddCourseActionButton(AddActionButton):
+    grok.context(IDepartment)
+    grok.view(DepartmentPage)
+    grok.order(4)
+    text = 'Add course'
+    target = 'addcourse'
+
+class AddCertificateActionButton(AddActionButton):
+    grok.context(ICertificate)
+    grok.view(CertificatePage)
+    grok.order(4)
+    text = 'Add course reference'
+    target = 'addcertificatecourse'
+
+
+#
+# Primary navigation tabs (in upper left navigation bar)...
+#
+class PrimaryNavManager(grok.ViewletManager):
+    """Viewlet manager for the primary navigation tab.
+    """
+    grok.name('primary_nav')
+
+class PrimaryNavTab(grok.Viewlet):
+    """Base for primary nav tabs.
+    """
+    grok.baseclass()
+    grok.viewletmanager(PrimaryNavManager)
+    grok.order(1)
+    grok.require('waeup.manageUniversity')
+
+    pnav = 0 # This is a kind of id of a tab. If some page provides
+             # also a 'pnav' attribute with the same value (here: 0),
+             # then the tab will be rendered as 'active' when the page
+             # gets rendered.
+             #
+             # This way you can assign certain pages to certain
+             # primary nav tabs. Each primary tab should therefore set
+             # the 'pnav' attribute to a different value (or several
+             # tabs might be rendered as active simultanously when the
+             # page gets rendered.
+    tab_title = u'Some Text'
+    
+    @property
+    def link_target(self):
+        return self.view.application_url()
+
+    @property
+    def active(self):
+        view_pnav = getattr(self.view, 'pnav', 0)
+        if view_pnav == self.pnav:
+            return 'active'
+        return ''
+
+class HomeTab(PrimaryNavTab):
+    """Home-tab in primary navigation.
+    """
+    grok.order(1)
+    grok.require('waeup.Public')
+    grok.template('primarynavtab')
+
+    pnav = 0
+    tab_title = u'Home'
+
+class FacultiesTab(PrimaryNavTab):
+    """Faculties-tab in primary navigation.
+    """
+    grok.order(2)
+    grok.require('waeup.manageUniversity')
+    grok.template('primarynavtab')
+
+    pnav = 1
+    tab_title = u'Faculties'
+
+    @property
+    def link_target(self):
+        return self.view.application_url('faculties')
+
+class SearchTab(PrimaryNavTab):
+    """Search tab in primary navigation.
+    """
+    grok.order(3)
+    grok.require('waeup.manageUniversity')
+    grok.template('primarynavtab')
+
+    pnav = 2
+    tab_title = u'Search'
+
+    @property
+    def link_target(self):
+        return self.view.application_url('@@search')
Index: waeup/trunk/src/waeup/catalog.py
===================================================================
--- waeup/trunk/src/waeup/catalog.py	(revision 3551)
+++ waeup/trunk/src/waeup/catalog.py	(revision 4789)
@@ -1,6 +1,2 @@
-import sys
-import csv
-import random
-from datetime import datetime
 import os
 import grok
@@ -8,7 +4,12 @@
 from hurry.query.query import Query, Text
 from hurry.query import Eq
-from app import University
-from students.interfaces import IStudent
-from students.student import Student
+from waeup.app import University
+from waeup.interfaces import ICourse, ICertificateCourse
+from waeup.student.interfaces import IStudent
+from zope.app.catalog.interfaces import ICatalog
+from zope.app.intid.interfaces import IIntIds
+from zope.component import getUtility
+from zope.component.interfaces import ComponentLookupError
+
 
 class StudentIndexes(grok.Indexes):
@@ -20,17 +21,96 @@
 
 
-class StudentSearch(grok.View):
-    grok.context(University)
+class CourseIndexes(grok.Indexes):
+     grok.site(University)
+     grok.name('courses_catalog')
+     grok.context(ICourse)
 
-    def update(self, query=None):
-        self.search_result = []
-        if query is not None:
-            self.search_result = self.search_context(query)
-            print "RESULT: ", list(self.search_result)
+     code = index.Field(attribute='code')
+     title = index.Text(attribute='title')
 
-    def search_context(self, query):
-        result = Query().searchResults(
-            Eq(('students_catalog', 'name'), query)
-            )
-        return result
-    
+class CourseCertificatesIndexes(grok.Indexes):
+     grok.site(University)
+     grok.name('certcourses_catalog')
+     grok.context(ICertificateCourse)
+
+     course_code = index.Field(attribute='getCourseCode')
+
+@grok.subscribe(ICourse, grok.IObjectAddedEvent)
+def handleCourseAdd(obj, event):
+     """Index an added course with the local catalog.
+
+     Courses are not indexed automatically, as they are not a
+     dictionary subitem of the accompanied site object
+     (`IUniversity`). I.e. one cannot get them by asking for
+     ``app['FACCODE']['DEPTCODE']['COURSECODE']`` but one has to ask for
+     ``app.faculties['FACCODE']['DEPTCODE'].courses['COURSECODE']``.
+
+     Once, a course is indexed we can leave the further handling to
+     the default component architechture. At least removals will
+     be handled correctly then (and the course unindexed).
+     """
+     try:
+          cat = getUtility(ICatalog, name='courses_catalog')
+     except ComponentLookupError:
+          # catalog not available. This might happen during tests.
+          return
+     intids = getUtility(IIntIds)
+     index = cat['code']
+     index.index_doc(intids.getId(obj), obj)
+
+from zope.interface import Interface
+from zope import schema
+
+class IQueryResultItem(Interface):
+     url = schema.TextLine(
+          title = u'URL that links to the found item')
+     title = schema.TextLine(
+          title = u'Title displayed in search results.')
+     description = schema.Text(
+          title = u'Longer description of the item found.')
+     
+     
+class QueryResultItem(object):
+     grok.implements(IQueryResultItem)
+     url = None
+     title = None
+     description = None
+     
+     def __init__(self, context, view):
+          self.context = context
+          self.url = view.url(context)
+          self.title = context.title
+          self.description = ''
+     
+class CourseQueryResultItem(QueryResultItem):
+     def __init__(self, context, view):
+          self.context = context
+          self.url = view.url(context)
+          self.title = "COURSE: " + context.title
+          self.description = 'code: %s' % context.code
+
+def search(query=None, view=None):
+     if not query:
+          return []
+     cat = getUtility(ICatalog, name='courses_catalog')
+     results = list(cat.searchResults(code=(query, query)))
+          
+     hitlist = []
+     results = Query().searchResults(
+          Eq(('courses_catalog', 'code'), query))
+     for result in results:
+          hitlist.append(CourseQueryResultItem(result, view=view))
+
+     results = Query().searchResults(
+          Text(('courses_catalog', 'title'), query))
+          
+     for result in results:
+          hitlist.append(CourseQueryResultItem(result, view=view))
+
+     return hitlist
+
+def search_context(query):
+     result = Query().searchResults(
+          Eq(('students_catalog', 'name'), query)
+          )
+     return result
Index: waeup/trunk/src/waeup/csvfile/README.txt
===================================================================
--- waeup/trunk/src/waeup/csvfile/README.txt	(revision 4789)
+++ waeup/trunk/src/waeup/csvfile/README.txt	(revision 4789)
@@ -0,0 +1,653 @@
+:mod:`waeup.csvfile` -- generic support for handling CSV files
+**************************************************************
+
+:Test-Layer: unit
+
+.. module:: waeup.csvfile
+   :synopsis: generic support for handling CSV files.
+
+
+.. note::
+
+   This version of the :mod:`waeup.csvfile` module doesn't support
+   Unicode input.  Also, there are currently some issues regarding
+   ASCII NUL characters.  Accordingly, all input should be UTF-8 or
+   printable ASCII to be safe. These restrictions will be removed in
+   the future.
+
+
+Module Contents
+================
+
+:class:`CSVFile`
+----------------
+
+.. class:: CSVFile(filepath)
+
+   Wrapper around the path to a real CSV file. :class:`CSVFile` is an
+   adapter that adapts basestring objects (aka regular and unicode
+   strings).
+
+   :class:`CSVFile` is designed as a base for derived, more
+   specialized types of CSV files wrappers, although it can serve as a
+   basic wrapper for simple, unspecified CSV files.
+
+   .. method:: grok.context(basestring)
+      :noindex:
+
+      We bind to basestring objects.
+
+   .. method:: grok.implements(ICSVFile)
+      :noindex:
+
+   .. attribute:: required_fields=[]
+
+      A list of header fields (strings) required for this kind of
+      CSVFile. Using the default constructor will fail with paths to
+      files that do *not* provide those fields.
+
+      The defaul value (empty list) means: no special fields required
+      at all.
+
+      Deriving classes can override this attribute to accept only
+      files that provide the appropriate header fields. The default
+      constructor already checks this.
+
+   .. attribute:: path
+
+      A string describing the path to the associated CSV file.
+
+   .. method:: getData()
+
+      Returns a generator delivering one data row of the denoted file
+      at a time.
+
+      Each data row is delivered as a dictionary mapping from header
+      field names to the values. Therefore a source line like this::
+
+        field1,field2
+        ...
+        data 1,data 2
+
+      will become a dictionary like this::
+
+        {'field1' : 'data 1', 
+         'field2' : 'data 2'}
+
+      The :meth:`getData` method does not evaluate file
+      values for correct data types or so.
+
+   .. method:: getHeaderFields()
+
+      Get a sorted list of header fields in the wrapped CSV file.
+
+
+:func:`getCSVFile`
+------------------
+
+.. function:: getCSVFile(filepath)
+
+   `filepath`
+      String with a filepath to an existing CSV file.
+
+   Get a CSV file wrapper for the given `filepath`. :func:`getCSVFile`
+   knows about all registered :class:`CSVFile` wrappers registered and
+   searches them for the most appropriate one.
+
+   If none can be found ``None`` is returned.
+
+   .. seealso:: 
+
+      :ref:`getcsvfilewrapper`,
+
+      :ref:`getcsvfiledecision`
+
+Helpers
+=======
+
+Some helper functions provide convenience methods for handling CSV
+data.
+
+:func:`toBool`
+--------------
+
+.. function:: toBool(string)
+
+   `string`
+      String containing some CSV data.
+
+   Turn a string into a boolean value.
+
+   If the string contains one of the values ``'true'``, ``'yes'``,
+   ``'y'``, ``'on'`` or ``'checked'`` then ``True`` is returned,
+   ``False`` otherwise.
+
+   The string can be uppercase, lowercase or mixed:
+
+     >>> from waeup.csvfile import toBool
+     >>> toBool('y')
+     True
+
+     >>> toBool('Yes')
+     True
+
+     >>> toBool('TRUE')
+     True
+
+     >>> toBool('no')
+     False
+
+   If we pass in a boolean then this will be returned unchanged:
+
+     >>> toBool(True)
+     True
+
+     >>> toBool(False)
+     False
+
+Basic example
+=============
+
+To initialize the whole framework we have to grok the :mod:`waeup`
+package first:
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+Create a file:
+
+    >>> path = 'mycsvfile.csv'
+    >>> open(path, 'wb').write(
+    ... """col1,col2
+    ... dataitem1,dataitem2
+    ... item3,item4
+    ... """)
+
+A regular file path is difficult to handle in terms of a component
+framework. Therefore we get a wrapper for it, that makes a CSV file
+object out of a path string:
+
+    >>> from waeup.csvfile.interfaces import ICSVFile
+    >>> src = ICSVFile(path)
+    >>> src
+    <waeup.csvfile.csvfile.CSVFile object at 0x...>
+
+Create a receiver:
+
+    >>> from waeup.csvfile.interfaces import ICSVDataReceiver
+    >>> class Receiver(object):
+    ...   grok.implements(ICSVDataReceiver)
+    ...   def receive(self, data):
+    ...     print "RECEIVED: ", data
+
+    >>> recv = Receiver()
+
+Find a connector:
+
+    >>> from zope.component import getMultiAdapter
+    >>> from waeup.csvfile.interfaces import ICSVDataConnector
+    >>> conn = getMultiAdapter((src, recv), ICSVDataConnector)
+    Traceback (most recent call last):
+    ...
+    ComponentLookupError: ((<waeup.csvfile.csvfile.CSVFile object at 0x...>, 
+                            <Receiver object at 0x...>),
+        <InterfaceClass waeup.csvfile.interfaces.ICSVDataConnector>, u'')
+
+Okay, create a connector:
+
+    >>> class Connector1(grok.MultiAdapter):
+    ...   grok.adapts(ICSVFile, ICSVDataReceiver)
+    ...   grok.provides(ICSVDataConnector)
+    ...   def __init__(self, source, receiver):
+    ...     self.source = source
+    ...     self.receiver = receiver
+    ...   def doImport(self):
+    ...     self.receiver.receive(
+    ...        self.source.getData())
+
+    >>> grok.testing.grok_component('Connector1', Connector1)
+    True
+
+Try again...
+
+    >>> conn = getMultiAdapter((src, recv), ICSVDataConnector)
+    >>> conn
+    <Connector1 object at 0x...>
+
+    >>> conn.doImport()
+    RECEIVED: <generator object at 0x...>
+
+Clean up:
+
+    >>> import os
+    >>> os.unlink(path)
+
+
+CSV file wrappers
+=================
+
+CSV file wrappers can extract data from CSV files denoted by a path.
+
+We create a CSV file:
+
+    >>> path = 'mycsvfile.csv'
+    >>> open(path, 'wb').write(
+    ... """col1,col2
+    ... dataitem1,dataitem2
+    ... item3,item4
+    ... """)
+
+Now we get a CSV file wrapper for it. This is simply done by asking
+for an adapter to the path string:
+
+    >>> from waeup.csvfile.interfaces import ICSVFile
+    >>> wrapper = ICSVFile(path)
+    >>> wrapper
+    <waeup.csvfile.csvfile.CSVFile object at 0x...>
+
+This wrapper can return the CSV data as a sequence of dicts:
+
+    >>> wrapper.getData()
+    <generator object at 0x...>
+
+As we see, the single dicts (each representing a row) are returned as
+a generator. We can list them:
+
+    >>> list(wrapper.getData())
+    [{'col2': 'dataitem2', 'col1': 'dataitem1'}, 
+     {'col2': 'item4', 'col1': 'item3'}]
+
+We can get a list of headerfields found in the file:
+
+    >>> wrapper.getHeaderFields()
+    ['col1', 'col2']
+
+.. _getcsvfilewrapper:
+
+Getting a wrapper
+=================
+
+If we want to get a wrapper best suited for our purposes, we can also
+use the :func:`getCSVFile` function:
+
+    >>> from waeup.csvfile.csvfile import getCSVFile
+    >>> wrapper = getCSVFile(path)
+    >>> wrapper
+    <waeup.csvfile.csvfile.CSVFile object at 0x...>
+
+As we currently have only one type of wrapper, we get this. Let's
+create another wrapper, that requires a column 'col1':
+
+    >>> from waeup.csvfile.interfaces import ICSVFile
+    >>> from waeup.csvfile import CSVFile
+    >>> class ICSVFileWithCol1(ICSVFile):
+    ...   """A CSV file that contains a 'col1' column.
+    ...   """
+
+    >>> class CSVFileWithCol1(CSVFile):
+    ...   required_fields = ['col1']
+    ...   grok.implements(ICSVFileWithCol1)
+    ...   grok.provides(ICSVFileWithCol1)
+
+We have to grok:
+
+    >>> grok.testing.grok_component('CSVFileWithCol1', CSVFileWithCol1)
+    True
+
+Now we can ask for a wrapper again, but this time we will get a
+CSVFileWithCol12 instance:
+
+    >>> getCSVFile(path)
+    <CSVFileWithCol1 object at 0x...>
+
+If we cannot get a wrapper at all, ``None`` is returned:
+
+    >>> getCSVFile('not-existent-file') is None
+    True
+
+.. _getcsvfiledecision:
+
+
+How :func:`getCSVFile` decides which wrapper to use
+---------------------------------------------------
+
+Apparently, :func:`getCSVFile` performes some magic: given a certain
+CSV file, it decides which one of all registered wrappers suits the
+file best.
+
+This decision is based on a score, which is computed as shown below
+for each registered wrapper.
+
+Before we can show this, we create some more CSV files.
+
+One file that does not contain valid CSV data:
+
+     >>> nocsvpath = 'nocsvfile.csv'
+     >>> open(nocsvpath, 'wb').write(
+     ... """blah blah blah.
+     ... blubb blubb.
+     ... """)
+
+One file that contains a 'col1' and a 'col3' column:
+
+    >>> path2 = 'mycsvfile2.csv'
+    >>> open(path2, 'wb').write(
+    ... """col1,col3
+    ... dataitem1b,dataitem2b
+    ... item3b,item4b
+    ... """)
+
+We create a wrapper that requires 'col1' and 'col2' but does not check
+in constructor, whether this requirement is met:
+
+    >>> from waeup.csvfile.interfaces import ICSVFile
+    >>> from waeup.csvfile import CSVFile
+    >>> class ICSVFile13(ICSVFile):
+    ...   """A CSV file that contains a 'special_col' column.
+    ...   """
+
+    >>> class CSVFile13(CSVFile):
+    ...   required_fields = ['col1', 'col2']
+    ...   grok.context(basestring)
+    ...   grok.implements(ICSVFile13)
+    ...   grok.provides(ICSVFile13)
+    ...   def __init__(self, context):
+    ...     self.path = context
+
+    >>> grok.testing.grok_component('CSVFile13',  CSVFile13)
+    True
+
+.. warn:: This is bad design as :class:`ICSVFile` instances should
+          always raise an exception in their constructor if a file
+          does not meet the basic requirements.
+
+          The base constructor will check for the correct values. So,
+          if you do not overwrite the base constructor, instances will
+          check on creation time, whether they can handle the desired
+          file.
+
+
+We create a wrapper that requires 'col1' and 'col2':
+
+    >>> from waeup.csvfile.interfaces import ICSVFile
+    >>> from waeup.csvfile import CSVFile
+    >>> class ICSVFile12(ICSVFile):
+    ...   """A CSV file that contains a 'special_col' column.
+    ...   """
+
+    >>> class CSVFile12(CSVFile):
+    ...   required_fields = ['col1', 'col2']
+    ...   grok.context(basestring)
+    ...   grok.implements(ICSVFile12)
+    ...   grok.provides(ICSVFile12)
+
+    >>> grok.testing.grok_component('CSVFile12',  CSVFile12)
+    True
+
+Now the rules applied to all wrappers are:
+
+* If no instance of a certain wrapper can be created from the given
+  path (i.e. __init__ raises some kind of exception): score is -1:
+
+    >>> from waeup.csvfile.csvfile import getScore
+    >>> getScore('nonexistant', CSVFile)
+    -1
+
+* If a wrapper requires at least one header_field and the given file
+  does not provide all of the required fields: score is -1:
+
+    >>> getScore(path2, CSVFile12)
+    -1
+
+* If a wrapper requires no header fields at all
+  (i.e. `required_fields` equals empty list): score is 0 (zero):
+
+    >>> getScore(path, CSVFile)
+    0
+
+* If a wrapper requires at least one header_field and all header
+  fields do also appear in the file: score is number of required
+  fields.
+
+    >>> getScore(path, CSVFileWithCol1)
+    1
+
+    >>> getScore(path, CSVFile12)
+    2
+
+If several wrappers get the same score for a certain file, the result
+is not determined.
+
+
+How to build custom CSV file wrappers
+=====================================
+
+A typical CSV file wrapper can be built like this:
+
+    >>> import grok
+    >>> from waeup.csvfile.interfaces import ICSVFile
+    >>> from waeup.csvfile import CSVFile
+
+    >>> class ICustomCSVFile(ICSVFile):
+    ...   """A marker for custom CSV files."""
+
+    >>> class CustomCSVFile(CSVFile):
+    ...   required_fields = ['somecol', 'othercol']
+    ...   grok.implements(ICustomCSVFile)
+    ...   grok.provides(ICustomCSVFile)
+
+    >>> grok.testing.grok_component('CustomCSVFile',  CustomCSVFile)
+    True
+
+The special things here are:
+
+* Derive from :class:`CSVFile`
+
+  :func:`getCSVFile` looks only for classes that are derived from
+  :class:`CSVFile`. So if you want your wrapper to be found by this function,
+  derive from :class:`CSVFile`.
+
+  As :class:`CSVFile` is an adapter, also our custom wrapper will
+  become one (adapting strings):
+
+     >>> ICustomCSVFile(path)
+     Traceback (most recent call last):
+     ...
+     TypeError: Missing columns in CSV file: ['somecol', 'othercol']
+
+  If our input file provides the correct columns, it will work:
+
+     >>> custompath = 'mycustom.csv'
+     >>> open(custompath, 'wb').write(
+     ... """somecol,othercol,thirdcol
+     ... dataitem1,dataitem2,dataitem3
+     ... """)
+
+     >>> ICustomCSVFile(custompath)
+     <CustomCSVFile object at 0x...>
+
+* Provide and implement a custom interface
+
+  A custom CSV file wrapper should provide and implement an own
+  interface. Otherwise it could not be required by other components
+  explicitly.
+
+  We have to provide *and* implement the custom interface because
+  otherwise instances would not implement the required interface
+  (maybe due to a flaw in `grok`/`martian`).
+
+
+Common Use Cases
+================
+
+Get a wrapper for a certain type of CSV file
+--------------------------------------------
+
+The type of a :class:`CSVFile` is determined by the interfaces it
+provides.
+
+If we want to get a wrapper that also guarantees to support certain
+fields (or None), then we already know about the wanted type.
+
+We create a file that does not have a 'special_col' field:
+
+    >>> path = 'mycsvfile.csv'
+    >>> open(path, 'wb').write(
+    ... """col1,col2
+    ... dataitem1,dataitem2
+    ... item3,item4
+    ... """)
+
+Now we create a wrapper, that requires that field:
+
+    >>> from waeup.csvfile.interfaces import ICSVFile
+    >>> from waeup.csvfile import CSVFile
+    >>> class ICSVFileWithSpecialCol(ICSVFile):
+    ...   """A CSV file that contains a 'special_col' column.
+    ...   """
+
+    >>> class CSVFileWithSpecialCol(CSVFile):
+    ...   required_fields = ['special_col']
+    ...   grok.provides(ICSVFileWithSpecialCol)
+
+    >>> grok.testing.grok_component('CSVFileWithSpecialCol', 
+    ...                             CSVFileWithSpecialCol)
+    True
+
+If we want to get a wrapper for that kind of file:
+
+    >>> ICSVFileWithSpecialCol(path)
+    Traceback (most recent call last):
+    ...
+    TypeError: Missing columns in CSV file: ['special_col']
+
+If the required col is available, however:
+
+    >>> path2 = 'mycsvfile2.csv'
+    >>> open(path2, 'wb').write(
+    ... """col1,col2,special_col
+    ... dataitem1,dataitem2,dataitem3
+    ... item4,item5,item6
+    ... """)
+    >>> ICSVFileWithSpecialCol(path2)
+    <CSVFileWithSpecialCol object at 0x...>
+
+Build an importer framework
+---------------------------
+
+We can also build an importer framework with CSV file support using
+the components described above.
+
+To model this, we start with two files, we will import lateron:
+
+    >>> path = 'mycsvfile.csv'
+    >>> open(path, 'wb').write(
+    ... """col1,col2
+    ... dataitem1a,dataitem2a
+    ... item3a,item4a
+    ... """)
+
+    >>> path2 = 'mycsvfile2.csv'
+    >>> open(path2, 'wb').write(
+    ... """col1,col3
+    ... dataitem1b,dataitem2b
+    ... item3b,item4b
+    ... """)
+
+Then we create two receivers for CSV file data:
+
+    >>> from zope.interface import Interface
+    >>> class IReceiver1(Interface):
+    ...   """A CSV data receiver."""
+
+    >>> class IReceiver2(Interface):
+    ...   """Another CSV data receiver."""
+
+    >>> class Receiver1(object):
+    ...   grok.implements(IReceiver1)
+    ...   def receive(self, data):
+    ...     print "Receiver1 received: ", data
+
+    >>> class Receiver2(object):
+    ...   grok.implements(IReceiver2)
+    ...   def receive(self, data):
+    ...     print "Receiver2 received: ", data
+    
+
+If we want to be sure, that a wrapper requires these fields, we ask
+for ICSVFile12:
+
+    >>> wrapper1 = ICSVFile12(path)
+    >>> wrapper1
+    <CSVFile12 object at 0x...>
+
+We could not use this interface (adapter) with the other CSV file:
+
+    >>> wrapper2 = ICSVFile12(path2)
+    Traceback (most recent call last):
+    ...
+    TypeError: Missing columns in CSV file: ['col2']
+
+The last step is to build a bridge between the receivers and the
+sources. We call it connector or importer here:
+
+    >>> class IImporter(Interface):
+    ...   """Import sources to receivers."""
+
+    >>> class IImporter12(IImporter):
+    ...   """Imports ICSVFile12 data into IReceiver1 objects."""
+
+    >>> class Importer12(grok.MultiAdapter):
+    ...   grok.adapts(ICSVFile12, IReceiver1)
+    ...   grok.implements(IImporter12)
+    ...   def __init__(self, csvfile, receiver):
+    ...     self.csvfile = csvfile
+    ...     self.receiver = receiver
+    ...   def doImport(self):
+    ...     self.receiver.receive(self.csvfile.getData())
+
+    >>> grok.testing.grok_component('Importer12',  Importer12)
+    True
+
+We can create an importer if we know the type of CSV file:
+
+    >>> myrecv = Receiver1()
+    >>> myfile = ICSVFile12(path)
+    >>> myfile
+    <CSVFile12 object at 0x...>
+
+    >>> ICSVFile12.providedBy(myfile)
+    True
+
+    >>> from zope.component import getMultiAdapter
+    >>> myimporter = getMultiAdapter((myfile, myrecv), IImporter12)
+    >>> myimporter
+    <Importer12 object at 0x...>
+
+
+We can also create an importer without knowing the type of CSV file
+before, using the :func:`getCSVFile` function:
+
+    >>> from waeup.csvfile import getCSVFile
+    >>> myfile = getCSVFile(path)
+    >>> ICSVFile12.providedBy(myfile)
+    True
+
+Apparently, the CSVFileWrapper getter knows, that a CSVFile12 suits
+the contents of our file best. We did not specify what type of file
+wrapper we want.
+
+Getting an importer now is easy:
+
+    >>> myimporter = getMultiAdapter((myfile, myrecv), IImporter12)
+    >>> myimporter
+    <Importer12 object at 0x...>
+
+
+Clean up:
+
+    >>> import os
+    >>> os.unlink(path)
+    >>> os.unlink(path2)
+    >>> os.unlink(nocsvpath)
+    >>> os.unlink(custompath)
Index: waeup/trunk/src/waeup/csvfile/__init__.py
===================================================================
--- waeup/trunk/src/waeup/csvfile/__init__.py	(revision 4789)
+++ waeup/trunk/src/waeup/csvfile/__init__.py	(revision 4789)
@@ -0,0 +1,3 @@
+# Make this a package.
+from waeup.csvfile.csvfile import getCSVFile, CSVFile
+from waeup.csvfile.util import toBool
Index: waeup/trunk/src/waeup/csvfile/csvfile.py
===================================================================
--- waeup/trunk/src/waeup/csvfile/csvfile.py	(revision 4789)
+++ waeup/trunk/src/waeup/csvfile/csvfile.py	(revision 4789)
@@ -0,0 +1,87 @@
+"""Support for CSV files.
+
+See README.txt and interfaces.py to learn more.
+"""
+
+import csv
+import grok
+from waeup.csvfile.interfaces import ICSVFile
+
+class CSVFile(grok.Adapter):
+    """A CSV file abstraction.
+    """
+    grok.context(basestring)
+    grok.implements(ICSVFile)
+
+    required_fields = []
+    path = None
+    
+    def __init__(self, context):
+        self.path = context
+        self.checkFields()
+
+    def checkFields(self):
+        headers = self.getHeaderFields()
+        missing = [x for x in self.required_fields
+                   if x not in headers]
+        if len(missing):
+            raise TypeError('Missing columns in CSV file: %s' % missing)
+        return
+            
+    def getDictReader(self):
+        return csv.DictReader(open(self.path, 'rb'))
+    
+    def getData(self):
+        reader = self.getDictReader()
+        for row in reader:
+            row = self.processRow(row)
+            yield row
+
+    def processRow(self, row):
+        return row
+
+    def getHeaderFields(self):
+        path = self.path
+        reader = self.getDictReader()
+        reader.next()
+        fields = reader.fieldnames
+        return sorted(reader.fieldnames)
+
+    
+# The registry for CSV file wrappers...
+csvwrappers = [CSVFile,]
+
+def getScore(path, wrapperclass):
+    """Compute a score for a certain wrapper related to a certain CSV
+    file.
+    """
+    wrapper = None
+    try:
+        wrapper = wrapperclass(path)
+    except:
+        return -1
+    required = wrapper.required_fields
+    if required == []:
+        return 0
+    available_headers = wrapper.getHeaderFields()
+    score = len([x for x in required if x in available_headers])
+    if score < len(required):
+        return -1
+    return score
+    
+    
+def getCSVFile(path):
+    """Get a CSVFile instance for a path.
+    """
+    best_score = -1
+    best_wrapper = None
+    
+    for wrapperclass in csvwrappers:
+        score = getScore(path, wrapperclass)
+        if score < best_score or score == -1:
+            continue
+        best_score, best_wrapper = score, wrapperclass
+    if best_wrapper is not None:
+        return best_wrapper(path)
+    return None
+
Index: waeup/trunk/src/waeup/csvfile/interfaces.py
===================================================================
--- waeup/trunk/src/waeup/csvfile/interfaces.py	(revision 4789)
+++ waeup/trunk/src/waeup/csvfile/interfaces.py	(revision 4789)
@@ -0,0 +1,37 @@
+"""Interfaces for waeup.csvfile.
+"""
+from zope.interface import Interface
+from zope import schema
+
+class ICSVFile(Interface):
+    """A wrapper for a CSV file.
+    """
+    required_fields = schema.List(
+        title = u'Required header fields.',
+        value_type = schema.TextLine(
+            title = u'Field name',
+            )
+        )
+    
+    path = schema.TextLine(
+        title = u'Filesystem path to wrapped file.'
+        )
+
+    def getData():
+        """Get a dict of data contained in the CSV file.
+        """
+        
+    def getHeaderFields():
+        """Get a list of (supported) header fields.
+        """
+    
+class ICSVDataReceiver(Interface):
+    """A receiver for CSV data.
+    """
+    def setData(data):
+        """Set data.
+        """
+
+class ICSVDataConnector(Interface):
+    """A connector brings together a CSVDataSource and a CSVDataReceiver.
+    """
Index: waeup/trunk/src/waeup/csvfile/meta.py
===================================================================
--- waeup/trunk/src/waeup/csvfile/meta.py	(revision 4789)
+++ waeup/trunk/src/waeup/csvfile/meta.py	(revision 4789)
@@ -0,0 +1,13 @@
+"""Meta configuration for waeup.csvfile components.
+"""
+import martian
+import waeup.csvfile.csvfile
+from waeup.csvfile import CSVFile
+
+# This grokker fills the registry on startup, with any class that is
+# derived from `CSVFileWrapper`...
+class CSVFileWrapperGrokker(martian.ClassGrokker):
+    martian.component(CSVFile)
+    def execute(self, klass, **kw):
+        waeup.csvfile.csvfile.csvwrappers.append(klass)
+        return True
Index: waeup/trunk/src/waeup/csvfile/util.py
===================================================================
--- waeup/trunk/src/waeup/csvfile/util.py	(revision 4789)
+++ waeup/trunk/src/waeup/csvfile/util.py	(revision 4789)
@@ -0,0 +1,12 @@
+"""Helpers for CSV import/export.
+"""
+
+## type casts
+
+def toBool(string):
+    if not isinstance(string, basestring):
+        return string
+    string = string.lower()
+    if string in ['true', 'yes', 'y', 'on', 'checked']:
+        return True
+    return False
Index: waeup/trunk/src/waeup/datacenter.py
===================================================================
--- waeup/trunk/src/waeup/datacenter.py	(revision 4789)
+++ waeup/trunk/src/waeup/datacenter.py	(revision 4789)
@@ -0,0 +1,219 @@
+"""WAeUP data center.
+
+The waeup data center cares for management of upload data and provides
+tools for importing/exporting CSV data.
+"""
+import os
+import struct
+import grok
+from datetime import datetime
+from zope.component import getMultiAdapter
+from waeup.csvfile import getCSVFile
+from waeup.interfaces import (IDataCenter, IWAeUPCSVImporter,
+                              ICSVDataReceivers, IDataCenterFile)
+from waeup.utils.helpers import copyFileSystemTree
+
+class DataCenter(grok.Container):
+    """A data center contains CSV files.
+    """
+    grok.implements(IDataCenter)
+    storage = os.path.join(os.path.dirname(__file__), 'files')
+
+    def getReceivers(self):
+        receivers = []
+        curr_obj = getattr(self, '__parent__', None)
+        while curr_obj is not None:
+            if ICSVDataReceivers.providedBy(curr_obj):
+                receivers = self.getCSVDataReceivers(curr_obj)
+                break
+            curr_obj = getattr(curr_obj, '__parent__', None)
+        return receivers
+
+    def getReceiverIds(self):
+        """Get a dict of available receivers.
+
+        The keys of the result are the receiver ids.
+        """
+        receivers = self.getReceivers()
+        return dict([(self.getReceiverId(x), x) for x in receivers])
+    
+    def getImporters(self):
+        """Get a list of all importers available.
+
+        The search for available importers is done in two steps:
+
+        1) Look for a utility providing ICSVDataReceiver,
+
+        2) For every item of that utility: try to get an adapter
+           providing IWAeUPCSVImporter.
+        """
+        result = []
+        receivers = self.getReceivers()
+        files = self.getFiles()
+        for receiver in receivers:
+            for file in files:
+                wrapped_file = getCSVFile(file.context)
+                try:
+                    importer = getMultiAdapter((wrapped_file, receiver),
+                                           IWAeUPCSVImporter)
+                    result.append(importer)
+                except:
+                    # No multi-adapter for this combination available...
+                    pass
+        return result
+    
+    def getFiles(self):
+        """Get a list of files stored in `storage`.
+
+        Files are sorted by basename.
+        """
+        result = []
+        if not os.path.exists(self.storage):
+            return result
+        for filename in sorted(os.listdir(self.storage)):
+            fullpath = os.path.join(self.storage, filename)
+            if not os.path.isfile(fullpath):
+                continue
+            result.append(DataCenterFile(fullpath))
+        return result
+
+    def setStoragePath(self, path, move=False, overwrite=False):
+        """Set the path where to store files.
+        """
+        path = os.path.abspath(path)
+        not_copied = []
+        if not os.path.exists(path):
+            raise ValueError('The path given does not exist: %s' % path)
+        if move is True:
+
+            not_copied = copyFileSystemTree(self.storage, path,
+                                            overwrite=overwrite)
+        
+        self.storage = path
+        return not_copied
+
+    def getCSVDataReceivers(self, obj):
+        """Get a list of attributes, that can receive CSV data.
+
+        We also get a list of values, if obj is a container that
+        contains value items.
+        """
+        result = []
+        for attr_name in dir(obj):
+            if attr_name.startswith('_'):
+                continue
+            try:
+                attr = getattr(obj, attr_name)
+                # This might fail...
+                #IWAeUPCSVImporter(attr)
+                result.append(attr)
+            except:
+                pass
+        if hasattr(obj, 'values'):
+            try:
+                result.extend(obj.values())
+            except:
+                pass
+        return result
+
+    def getPossibleImports(self):
+        """Get list of possible imports.
+
+        A single import is defined as a tuple
+        
+          ( <file-descr>, <importers> )
+        
+        where ``<file-descr>`` is an `IDataCenterFile` object and
+        ``<importers>`` is a list of `IWAeUPCSVImporter` objects.
+        """
+        result = []
+        importers = self.getImporters()
+        for filedescr in self.getFiles():
+            possible_importers = []
+            for importer in importers:
+                if importer.csvfile.path != filedescr.context:
+                    continue
+                importer_context = (
+                    importer, self.getReceiverId(importer.receiver))
+                possible_importers.append(importer_context)
+            if len(possible_importers) == 0:
+                continue
+            result.append((filedescr, possible_importers))
+        return result
+
+    def getReceiverId(self, obj):
+        """Get a unique id for an object.
+
+        If the object is stored in ZODB it should contain a `_p_oid`
+        attribute, which is guaranteed to be unique over the ZODB.
+
+        If the object has no such attribute, then it will be held in
+        memory only and will vanish as soon as the request is over. In
+        this case we can use the memory address of it.
+
+        Both, the id() result and the ZODB oid, are actually integers
+        which we return as strings. To make the difference chrystal
+        clear, we prepend ``z`` to ids taken from ZODB oids.
+        """
+        if not hasattr(obj, '_p_oid'):
+            return str(id(obj))
+        oid = getattr(obj, '_p_oid')
+        if oid is None:
+            # The persistent object is not stored in the ZODB
+            return str(id(obj))
+        return 'z%s' % struct.unpack('>Q', oid)[0]
+
+    def doImport(self, path, receiverid, clear=None):
+        receivers = self.getReceiverIds()
+        if receiverid not in receivers.keys():
+            raise ValueError('The requested data receiver cannot be found.')
+        receiver = receivers[receiverid]
+        filewrapper = getCSVFile(path)
+        if filewrapper is None:
+            raise ValueError('Format of CSV file not supported.')
+        importer = getMultiAdapter((filewrapper, receiver), IWAeUPCSVImporter)
+        if clear is not None:
+            importer.doImport(clear_old_data=clear)
+        else:
+            importer.doImport()
+        return
+
+class DataCenterFile(object):
+    """A description of a file stored in data center.
+    """
+    grok.implements(IDataCenterFile)
+    
+    def __init__(self, context):
+        self.context = context
+        self.name = os.path.basename(self.context)
+        self.size = self.getSize()
+        self.uploaddate = self.getDate()
+
+    def getDate(self):
+        """Get a human readable datetime representation.
+        """
+        date = datetime.fromtimestamp(os.path.getctime(self.context))
+        return date.strftime('%c')
+
+    def getSize(self):
+        """Get a human readable file size.
+        """
+        bytesize = os.path.getsize(self.context)
+        size = "%s bytes" % bytesize
+        units = ['kb', 'MB', 'GB']
+        for power, unit in reversed(list(enumerate(units))):
+            power += 1
+            if bytesize >= 1024 ** power:
+                size = "%.2f %s" % (bytesize/(1024.0**power), unit)
+                break
+        return size
+
+class Import(object):
+    """Helper class to aggregate imports and their data.
+    """
+    def __init__(self, filedescr, importers):
+        self.file = filedescr
+        self.importers = []
+        for importer, receiverid in importers:
+            importer.receiverid = receiverid
+            self.importers.append(importer)
Index: waeup/trunk/src/waeup/datacenter.txt
===================================================================
--- waeup/trunk/src/waeup/datacenter.txt	(revision 4789)
+++ waeup/trunk/src/waeup/datacenter.txt	(revision 4789)
@@ -0,0 +1,288 @@
+WAeUP Data Center
+*****************
+
+The WAeUP data center cares for managing CSV files and importing then.
+
+:Test-Layer: unit
+
+Creating a data center
+======================
+
+A data center can be created easily:
+
+    >>> from waeup.datacenter import DataCenter
+    >>> mydatacenter = DataCenter()
+    >>> mydatacenter
+    <waeup.datacenter.DataCenter object at 0x...>
+
+Each data center has a location in file system where files are stored:
+
+    >>> storagepath = mydatacenter.storage
+    >>> storagepath
+    '/.../src/waeup/files'
+
+
+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.getFiles()
+    []
+
+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.getFiles()
+    [<waeup.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.getFiles()[0]
+    >>> datafile.getSize()
+    '13 bytes'
+
+    >>> datafile.getDate() # Nearly current datetime...
+    '...'
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(newpath)
+    >>> shutil.rmtree(verynewpath)
+
+
+Handling imports
+================
+
+Data centers can find objects ready for CSV imports and associate
+appropriate importers with them.
+
+Getting importers
+-----------------
+
+To do so, data centers look up their parents for the nearest ancestor,
+that implements `ICSVDataReceivers` and grab all attributes, that
+provide some importer.
+
+We therefore have to setup a proper scenario first.
+
+We start by creating a simple thing that is ready for receiving CSV
+data:
+
+    >>> class MyCSVReceiver(object):
+    ...   pass
+
+Then we create a container for such a CSV receiver:
+
+    >>> import grok
+    >>> from waeup.interfaces import ICSVDataReceivers
+    >>> from waeup.datacenter import DataCenter
+    >>> class SomeContainer(grok.Container):
+    ...   grok.implements(ICSVDataReceivers)
+    ...   def __init__(self):
+    ...     self.some_receiver = MyCSVReceiver()
+    ...     self.other_receiver = MyCSVReceiver()
+    ...     self.datacenter = DataCenter()
+
+By implementing `ICSVDataReceivers`, a pure marker interface, we
+indicate, that we want instances of this class to be searched for CSV
+receivers.
+
+This root container has two CSV receivers.
+
+The datacenter is also an attribute of our root container.
+
+Before we can go into action, we also need an importer, that is able
+to import data into instances of MyCSVReceiver:
+
+    >>> from waeup.csvfile.interfaces import ICSVFile
+    >>> from waeup.interfaces import IWAeUPCSVImporter
+    >>> from waeup.utils.importexport import CSVImporter
+    >>> class MyCSVImporter(CSVImporter):
+    ...   grok.adapts(ICSVFile, MyCSVReceiver)
+    ...   grok.provides(IWAeUPCSVImporter)
+    ...   datatype = u'My Stuff'
+    ...   def doImport(self, filepath, clear_old_data=True,
+    ...                                overwrite=True):
+    ...     print "Data imported!"
+
+We grok the components to get the importer (which is actually an
+adapter) registered with the component architechture:
+
+    >>> grok.testing.grok('waeup')
+    >>> grok.testing.grok_component('MyCSVImporter', MyCSVImporter)
+    True
+
+Now we can create an instance of `SomeContainer`:
+
+    >>> mycontainer = SomeContainer()
+
+As we are not creating real sites and the objects are 'placeless' from
+the ZODB point of view, we fake a location by telling the datacenter,
+that its parent is the container:
+
+    >>> mycontainer.datacenter.__parent__ = mycontainer
+    >>> datacenter = mycontainer.datacenter
+
+When a datacenter is stored in the ZODB, this step will happen
+automatically.
+
+Before we can go on, we have to set a usable path where we can store
+files without doing harm:
+
+    >>> os.mkdir('filestore')
+    >>> filestore = os.path.abspath('filestore')
+    >>> datacenter.setStoragePath(filestore)
+    []
+
+Furthermore we must create a file for possible import, as we will get
+only importers, for which also an importable file is available:
+
+    >>> import os
+    >>> filepath = os.path.join(datacenter.storage, 'mydata.csv')
+    >>> open(filepath, 'wb').write("""col1,col2
+    ... 'ATerm','Something'
+    ... """)
+
+The datacenter is now able to find the CSV receivers in its parents:
+
+    >>> datacenter.getImporters()
+    [<MyCSVImporter object at 0x...>, <MyCSVImporter object at 0x...>]
+
+
+Imports with the WAeUP portal
+-----------------------------
+
+The examples above looks complicated, but this is the price for
+modularity. If you create a new container type, you can define an
+importer and it will be used automatically by other components.
+
+In the WAeUP portal the only component that actually provides CSV data
+importables is the `University` object.
+
+
+Getting imports (not: importers)
+--------------------------------
+
+We can get 'imports':
+
+    >>> datacenter.getPossibleImports()
+    [(<...DataCenterFile object at 0x...>, 
+      [(<MyCSVImporter object at 0x...>, '...'),
+       (<MyCSVImporter object at 0x...>, '...')])]
+
+As we can see, an import is defined here as a tuple of a
+DataCenterFile and a list of available importers with an associated
+data receiver (the thing where the data should go to).
+
+The data receiver is given as an ZODB object id (if the data receiver
+is persistent) or a simple id (if it is not).
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(filestore)
+
+
+Data center helpers
+===================
+
+Data centers provide several helper methods to make their usage more
+convenient.
+
+
+Receivers and receiver ids
+--------------------------
+
+As already mentioned above, imports are defined as triples containing
+
+* a file to import, 
+
+* an importer to do the import and 
+
+* an object, which should be updated by the data file.
+
+The latter normally is some kind of container, like a faculty
+container or similar. This is what we call a ``receiver`` as it
+receives the data from the file via the importer.
+
+The datacenter finds receivers by looking up its parents for a
+component, that implements `ICSVDataReceivers` and scanning that
+component for attributes, that can be adapted to `ICSVImporter`.
+
+I.e., once found an `ICSVDataReceiver` parent, the datacenter gets all
+importers that can be applied to attributes of this component. For
+each attribute there can be at most one importer.
+
+When building the importer list for a certain file, we also check,
+that the headers of the file comply with what the respective importers
+expect. So, if a file contains broken headers, the file won't be
+offered for import at all.
+
+The contexts of the found importers then build our list of available
+receivers. This means also, that for each receiver provided by the
+datacenter, there is also an importer available.
+
+If for a potential receiver no importer can be found, this receiver
+will be skipped.
+
+As one type of importer might be able to serve several receivers, we
+also have to provide a unique id for each receiver. This is, where
+``receiver ids`` come into play.
+
+Receiver ids of objects are determined as
+
+* the ZODB oid of the object if the object is persistent
+
+* the result of id(obj) otherwise.
+
+The value won this way is a long integer which we turn into a
+string. If the value was get from the ZODB oid, we also prepend it
+with a ``z`` to avoid any clash with non-ZODB objects (they might
+deliver the same id, although this is *very* unlikely).
Index: waeup/trunk/src/waeup/doc/developernotes.txt
===================================================================
--- waeup/trunk/src/waeup/doc/developernotes.txt	(revision 4789)
+++ waeup/trunk/src/waeup/doc/developernotes.txt	(revision 4789)
@@ -0,0 +1,307 @@
+Developer Notes
+***************
+
+How to setup a developer instance of the WAeUP SRP, handle tests, docs
+etc.
+
+The new WAeUP SRP is based on `Grok <http://grok.zope.org/>`_.
+
+Installing a developer copy
+===========================
+
+The installation is described for Linux-based computers.
+
+Preparing the system
+--------------------
+
+To create a working copy of the WAeUP SRP we recommend use of
+`virtualenv`. You, however, need also some basic libraries, a C
+compiler and some things more.
+
+What you need (Debian/Ubuntu package names in brackets):
+
+* Python 2.5 (python2.5)
+
+  Currently, also Python2.4 is supported but we want to make use of
+  some of the 2.5 goodies in the future.
+
+
+* Python 2.5 development files (python2.5-dev)
+
+* A C-Compiler (gcc)
+
+* The C library development files (libc6-dev)
+
+* A subversion client (svn)
+
+* enscript (enscript) [optional]
+
+  This is only needed if you want test coverage reports.
+
+All these packages can be installed on Debian systems like this::
+
+  # apt-get install python2.5 python2.5-dev python2.5-dbg \
+                    gcc libc6-dev svn enscript
+
+Afterwards you should be able to enter::
+
+  $ python2.5
+
+at the commandline and get a Python prompt. Quit the interpreter
+pressing <CTRL-D>.
+
+Installing `virtualenv`
+-----------------------
+
+We recommend use of `virtualenv` to create Python sandboxes where you
+can run your code without touching any other installations.
+
+If you don't already have ``easy_install`` available, you can find the
+script to set it up on the `PEAK EasyInstall page`_.
+
+.. _`PEAK EasyInstall page`: http://peak.telecommunity.com/DevCenter/EasyInstall#installing-easy-install
+
+You need to download `ez_setup.py`_. Then, you run it like this to
+install ``easy_install`` into your system Python::
+
+  $ sudo python2.5 ez_setup.py
+
+.. _`ez_setup.py`: http://peak.telecommunity.com/dist/ez_setup.py
+
+This will make ``easy_install`` available to you.
+
+.. note:: Sometimes you have ``easy_install`` installed but you need a
+          newer version of the underlying setuptools infrastructure to
+          make Grok work. You can upgrade setuptools with::
+
+            $ sudo easy_install -U setuptools
+
+Now you can install `virtualenv` by doing (as root)::
+
+  # easy_install-2.5 virtualenv
+
+This step will fetch all needed sources from the internet and install
+`virtualenv` locally in your Python2.5 installation.
+
+
+Creating a sandbox
+------------------
+
+This step is only necessary (and recommended) if you installed
+`virtualenv` before.
+
+As a normal user you now can create a sandbox for your upcoming work
+by::
+
+  $ virtualenv --no-site-packages mysandbox
+
+where ``mysandbox`` is a directory in the filesystem where your
+sandbox will be created. `virtualenv` will also create this directory
+for you.
+
+By passing the ``no-site-packages`` switch we tell `virtualenv` to
+provide us a clean environment without any extra-packages installed
+systemwide.
+
+If you have a look into the freshly created sandbox, you will notice
+that in the ``bin/`` directory there is also 
+
+You now can activate the sandbox by doing::
+
+  $ source mysandbox/bin/activate
+
+You will notice that the input prompt changes.
+
+To deactivate the sandbox at any time, enter::
+
+  $ deactivate
+
+and the prompt will be the same as before the activation.
+
+For the following steps make sure the sandbox is active.
+
+
+Creating a working place
+------------------------
+
+In the sandbox (or anywhere else) we now create our real working
+environment. To do this, we change to the sandbox and checkout the
+sources of the WAeUP SRP from the subversion server::
+
+  $ cd mysandbox/
+  $ svn co https://svn.waeup.org/repos/waeup/tunk waeup-trunk
+
+where ``waeup-trunk`` is only a name we've chosen here to make clear
+where the sources come from.
+
+This command should fetch the sources of the WAeUP sources for you and
+put it in the directory ``waeup-trunk/``.
+
+Now enter the new directory::
+
+  $ cd waeup-trunk
+
+
+Preparing the build
+-------------------
+
+In the sources directory (``waeup-trunk/``) you have to prepare the
+project to fetch needed components (eggs), compile C-code parts,
+etc. This steip will not touch any external projects::
+
+  $ python2.5 bootstrap.py
+
+This will generate some directories and the ``buildout`` script in
+``bin/`` for us. This step must be executed only once for each
+instance.
+
+Now we can do the real build by triggering::
+
+  $ bin/buildout
+
+If this is your first install of some Grok-related project, this step
+will need some time as lots of sources have to be fetched, many
+components must be compiled, etc.
+
+This step must be redone whenever you change something in
+``buildout.cfg`` or ``setup.py``.
+
+Afterwards we are ready to go.
+
+
+Start the instance
+------------------
+
+You should be able now to start the created instance by doing::
+
+   $ bin/zopectl fg
+
+If you now point a browser to::
+
+   localhost:8080
+
+you should get a login pop-up, where you can login as superuser with
+``grok`` and ``grok`` as username/password.
+
+If you want to change the default credentials, have a look into
+``buildout.cfg`` where the superuser password is determined.
+
+You can stop the instance by pressing <CTRL-C>.
+
+
+
+Documentation
+=============
+
+With the :mod:`waeup` package we try to reach high standards in both,
+documentation and testing.
+
+:mod:`waeup` makes extensive use of doctests, which this way also
+become both: executable (i.e. testable) examples and documentation.
+
+Generating documentation
+------------------------
+
+We use the excellent `Sphinx <http://sphinx.pocoo.org/>`_ Python
+documentation generator to generate the docs as HTML pages.
+
+The documentation of the :mod:`waeup` project can easily be created
+doing::
+
+  $ bin/waeupdocs
+
+This will create a tree of HTML pages in
+``parts/waeupdocs/waeup/build/waeup/`` which you can for instance
+browse by pointing your browser to this location.
+
+An 'official' place in internet for the whole docs is about to come
+but not yet available.
+
+
+Writing documentation
+---------------------
+
+means explaining to other developers what your code does and test it
+at the same time. See the many .txt files in the :mod:`waeup` package
+for examples.
+
+
+Testing
+=======
+
+Tests are most important to the reliability of the :mod:`waeup`
+package. We don't tell someone that our code works, if we cannot prove
+it. And we prove it by testing.
+
+Running tests
+-------------
+
+You can run all the tests for the package by doing::
+
+  $ bin/test
+
+If you like colored output (can improve readability of failure
+messages), use the ``-c`` switch::
+
+  $ bin/test -c
+
+We have many tests in the :mod:`waeup` package so that sometimes you
+only want the functional *or* the unit tests to run. This can be done
+like this::
+
+  $ bin/test -c -u
+
+if you want only unit tests or like this::
+
+  $ bin/test -c -f
+
+if you only want functional tests.
+
+
+Writing tests
+-------------
+
+Is a wiiide topic. For now, please see the numerous .txt files in the
+source. Most of the **are** tests.
+
+Code coverage
+-------------
+
+We want to make sure, that all aspects of our software are
+tested. This means, that all parts of the codes should be tested
+somewhere.
+
+To tell how good our test coverage is, we can also use the testrunner
+(``bin/test``)::
+
+  $ bin/test --coverage=coverage
+
+will run the tests but also look, which parts of code were touched by
+them. For releases we want 100% coverage. Beware: running tests with
+the ``--coverage`` switch slows down tests by factor 10 or more.
+
+The command above will output a table with percentages. Furthermore,
+in ``/parts/test/coverage`` you will (after the testrun) find your
+sources preceeded by markers which tell, how often (or none) a certain
+line was used in tests.
+
+To have a more convenient cmdline interface, we also provide some
+shortcuts::
+
+  $ bin/coverage-detect
+
+will run all the tests as shown above and put the results in
+``parts/coverage-detect/coverage``.
+
+After that you can run::
+
+  $ bin/coverage
+
+to get a browsable HTML representation of test coverage in
+``parts/coverage-detect/coverage/report``.
+
+Both, the coverage reports and HTML documentation generated by sphinx
+can be packed and put onto a website as-is.
+
+It is also possible to generate the docs and reports nightly by a
+buildbot or something like this.
Index: waeup/trunk/src/waeup/ftesting.zcml
===================================================================
--- waeup/trunk/src/waeup/ftesting.zcml	(revision 3551)
+++ waeup/trunk/src/waeup/ftesting.zcml	(revision 4789)
@@ -10,5 +10,5 @@
   <!-- Typical functional testing security setup -->
   <securityPolicy
-      component="zope.app.securitypolicy.zopepolicy.ZopeSecurityPolicy"
+      component="zope.securitypolicy.zopepolicy.ZopeSecurityPolicy"
       />
 
@@ -17,7 +17,29 @@
       title="Unauthenticated User"
       />
+  <unauthenticatedGroup id="zope.Anybody"
+      title="Unauthenticated Users" />
+  <authenticatedGroup id="zope.Authenticated"
+      title="Authenticated Users" />
+  <everybodyGroup id="zope.Everybody"
+      title="All Users" />
+
   <grant
       permission="zope.View"
-      principal="zope.anybody"
+      principal="zope.Anybody"
+      />
+  <grant
+      permission="zope.app.dublincore.view"
+      principal="zope.Anybody" />
+  <grant
+      permission="zope.View"
+      principal="zope.Authenticated"
+      />
+  <grant
+      permission="zope.app.dublincore.view"
+      principal="zope.Authenticated"
+      />
+  <grant
+      permission="waeup.Public"
+      principal="zope.Everybody"
       />
 
Index: waeup/trunk/src/waeup/hostel/hostel.py
===================================================================
--- waeup/trunk/src/waeup/hostel/hostel.py	(revision 3551)
+++ waeup/trunk/src/waeup/hostel/hostel.py	(revision 4789)
@@ -1,8 +1,6 @@
 import grok
-from waeup.baseitem import BaseItem
-from interfaces import IHostel
+from waeup.interfaces import IHostel
 
-
-class Hostel(BaseItem):
+class Hostel(grok.Container):
     """This is a hostel record.
     """    
@@ -12,2 +10,4 @@
         super(Hostel, self).__init__(**kw)
         self.name = name
+
+grok.global_utility(Hostel, provides=IHostel)
Index: waeup/trunk/src/waeup/hostel/hostelcontainer.py
===================================================================
--- waeup/trunk/src/waeup/hostel/hostelcontainer.py	(revision 3551)
+++ waeup/trunk/src/waeup/hostel/hostelcontainer.py	(revision 4789)
@@ -1,6 +1,36 @@
-from waeup.basecontainer import BaseContainer
-from hostel import Hostel
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.interfaces import IHostelContainer, IHostel
 
-class HostelContainer(BaseContainer):
-    name = u"Hostels"
-    childClass = Hostel
+class HostelContainer(grok.Container):
+
+    grok.implements(IHostelContainer)
+    grok.require('waeup.manageUniversity')
+
+    # A simple counter for ids.
+    next_id = 0L
+
+    def addHostel(self, hostel):
+        if not IHostel.providedBy(hostel):
+            raise TypeError('HostelContainers contain only IHostel instances')
+        id = str(self.next_id)
+        self[id] = hostel
+        while self.next_id in self.keys():
+            # Look for next unused int...
+            self.next_id += 1
+        return id
+
+class HostelContainerFactory(grok.GlobalUtility):
+    """A factory for faculty containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.HostelContainer')
+    title = u"Create a new hostel container.",
+    description = u"This factory instantiates new hostel containers."
+
+    def __call__(self):
+        return HostelContainer()
+
+    def getInterfaces(self):
+        return implementedBy(HostelContainer)
Index: waeup/trunk/src/waeup/hostel/hostelcontainer.txt
===================================================================
--- waeup/trunk/src/waeup/hostel/hostelcontainer.txt	(revision 4789)
+++ waeup/trunk/src/waeup/hostel/hostelcontainer.txt	(revision 4789)
@@ -0,0 +1,70 @@
+waeup.hostel.HostelContainer
+****************************
+
+Containers for hostels.
+
+:Test-Layer: unit
+
+Getting a hostel container
+==========================
+
+We can easily create `HostelContainers`::
+
+    >>> from waeup.hostel.hostelcontainer import HostelContainer
+    >>> mycontainer = HostelContainer()
+
+Hostel containers provide ``IHostelContainer``:
+
+    >>> from waeup.interfaces import IHostelContainer
+    >>> IHostelContainer.providedBy(mycontainer)
+    True
+
+Another way to get a hostel container -- without importing the class
+-- is via factories. We registered a factory for hostel containers
+under the name ``waeup.HostelContainer``::
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+Now we can ask for an object by calling the appropriate factory:
+
+    >>> from zope.component import createObject
+    >>> createObject(u'waeup.HostelContainer')
+    <waeup.hostel.hostelcontainer.HostelContainer object at 0x...>
+
+This way we get a thing that implements IHostelContainer without
+imports or similar.
+
+We can be sure, that the full interface is supported by the
+HostelContainer class::
+
+    >>> from zope.interface.verify import verifyClass
+    >>> verifyClass(IHostelContainer, HostelContainer)
+    True
+
+
+Storing things in hostel containers
+====================================
+
+We can, of course, store things in a hostel container. But when we
+really store an object, then it must be a hostel::
+
+    >>> mycontainer.addHostel(42)
+    Traceback (most recent call last):
+    ...
+    TypeError: HostelContainers contain only IHostel instances
+
+Okay, so we have to get a hostel first::
+
+    >>> from waeup.hostel.hostel import Hostel
+    >>> myhostel = Hostel()
+
+We can add this hostel to our container::
+
+    >>> mycontainer.addHostel(myhostel)
+    '0'
+
+We get back the key, under which the hostel was stored. It will be
+some string, but there is no guarantee at all, how this key looks
+like. It might be a string of integers, a name or whatever; you cannot
+know before.
Index: waeup/trunk/src/waeup/hostel/interfaces.py
===================================================================
--- waeup/trunk/src/waeup/hostel/interfaces.py	(revision 3551)
+++ 	(revision )
@@ -1,14 +1,0 @@
-##
-## interfaces.py
-from zope.interface import Interface
-from zope import schema
-
-class IHostel(Interface):
-    """Representation of a student.
-    """
-    name = schema.TextLine(
-        title = u'Name of Hostel',
-        default = u'Nobody',
-        required = True,
-        )
-
Index: waeup/trunk/src/waeup/index.txt
===================================================================
--- waeup/trunk/src/waeup/index.txt	(revision 4789)
+++ waeup/trunk/src/waeup/index.txt	(revision 4789)
@@ -0,0 +1,29 @@
+Welcome to waeup documentation!
+*******************************
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+   README
+   browser
+   doc/developernotes
+
+API documentation
+=================
+
+.. toctree::
+   :maxdepth: 1
+
+   app
+   csvfile/README.txt
+   university/api.txt
+   utils/api.txt
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
Index: waeup/trunk/src/waeup/interfaces.py
===================================================================
--- waeup/trunk/src/waeup/interfaces.py	(revision 3551)
+++ waeup/trunk/src/waeup/interfaces.py	(revision 4789)
@@ -1,8 +1,39 @@
 ##
 ## interfaces.py
-from zope.interface import Interface
+from zc.sourcefactory.basic import BasicSourceFactory
+from zope.component import getUtility
+from zope.app.catalog.interfaces import ICatalog
+from zope.interface import Interface, Attribute
 from zope import schema
-
-class IUniversity(Interface):
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+from waeup.permissions import RoleSource
+
+def SimpleWAeUPVocabulary(*terms):
+    """A well-buildt vocabulary provides terms with a value, token and
+       title for each term
+    """
+    return SimpleVocabulary([
+            SimpleTerm(value, value, title) for title, value in terms])
+
+
+class CourseSource(BasicSourceFactory):
+    """A course source delivers all courses inside the portal by looking
+       up a catalog.
+    """
+    catalog = None
+    def getValues(self):
+        if self.catalog is None:
+            self.catalog = getUtility(ICatalog, name='courses_catalog')
+        return list(self.catalog.searchResults(code=('', 'z*')))
+        
+    def getTitle(self, value):
+        return "%s %s" % (value.code, value.title[:32])
+
+
+class IWAeUPObject(Interface):
+    """A WAeUP object.
+    """
+
+class IUniversity(IWAeUPObject):
     """Representation of a university.
     """
@@ -12,2 +43,369 @@
         required = True,
         )
+
+    faculties = Attribute("A container for faculties.")
+    students = Attribute("A container for students.")
+    hostels = Attribute("A container for hostels.")
+    
+class IWAeUPContainer(IWAeUPObject):
+    """A container for WAeUP objects.
+    """
+
+class IWAeUPContained(IWAeUPObject):
+    """An item contained in an IWAeUPContainer.
+    """
+    
+class IStudentContainer(IWAeUPContainer):
+    """A container for StudentObjects.
+    """
+
+class IFaculty(IWAeUPContainer):
+    """Representation of a university faculty.
+    """
+    title = schema.TextLine(
+        title = u'Name of Faculty',
+        default = u'Unnamed',
+        required = True,
+        )
+
+    title_prefix = schema.TextLine(
+        title = u'Title prefix',
+        default = u'faculty',
+        required = True,
+        )
+    
+    code = schema.TextLine(
+        title = u'Code',
+        description = u'Abbreviated code of the faculty',
+        default = u'NA',
+        required = True,
+        )
+
+class IFacultyContainer(IWAeUPContainer):
+    """A container for faculties.
+    """
+    def addFaculty(faculty):
+        """Add an IFactulty object.
+
+        Returns the key, under which the object was stored.
+        """
+
+class IHostelContainer(IWAeUPContainer):
+    """A container for hostels.
+    """
+    def addHostel(hostel):
+        """Add an IHostel object.
+
+        Returns the key, under which the object was stored.
+        """
+
+class IHostel(IWAeUPObject):
+    """Representation of a hostel.
+    """
+    name = schema.TextLine(
+        title = u'Name of Hostel',
+        default = u'Nobody',
+        required = True,
+        )
+
+class IDepartment(IWAeUPObject):
+    """Representation of a department.
+    """
+    title = schema.TextLine(
+        title = u'Name of Department',
+        default = u'Unnamed',
+        required = True,
+        )
+
+    title_prefix = schema.TextLine(
+        title = u'Title prefix',
+        default = u'department',
+        required = True,
+        )
+    
+    code = schema.TextLine(
+        title = u'Code',
+        default = u'NA',
+        description = u'Abbreviated code of the department',
+        required = True,
+        )
+
+    courses = Attribute("A container for courses.")
+    certificates = Attribute("A container for certificates.")
+
+
+class ICourseContainer(IWAeUPContainer):
+    """A container for faculties.
+    """
+    def addCourse(faculty):
+        """Add an ICourse object.
+
+        Returns the key, under which the object was stored.
+        """
+
+class ICourse(IWAeUPObject):
+    """Representation of a course.
+    """
+    code = schema.TextLine(
+        title = u'Code',
+        default = u'NA',
+        description = u'Abbreviated code of the course',
+        required = True,
+        readonly = True,
+        )
+
+    title = schema.TextLine(
+        title = u'Title of course',
+        default = u'Unnamed',
+        required = True,
+        )
+
+    level = schema.TextLine(
+        title = u'Level',
+        default = u'100',
+        required = False,
+        )
+
+    credits = schema.Int(
+        title = u'Credits',
+        default = 0,
+        required = False,
+        )
+    
+    passmark = schema.Int(
+        title = u'Passmark',
+        default = 40,
+        required = False,
+        )
+
+    semester = schema.Choice(
+        title = u'Semester/Term',
+        default = 0,
+        vocabulary = SimpleWAeUPVocabulary(
+            ('N/A', 0), ('First Semester', 1),
+            ('Second Semester', 2), ('Combined', 3)),
+        required = True,
+        )
+
+
+class ICertificate(IWAeUPObject):
+    """Representation of a certificate.
+    """
+    code = schema.TextLine(
+        title = u'Code',
+        default = u'NA',
+        description = u'Abbreviated code of the certificate.',
+        required = True,
+        )
+
+    review_state = schema.Choice(
+        title = u'review state',
+        values = ['unchecked', 'checked']
+        )
+
+    title = schema.TextLine(
+        title = u'title',
+        required = True,
+        )
+
+    category = schema.TextLine(
+        title = u'category',
+        required = True,
+        )
+
+    study_mode = schema.TextLine(
+        title = u'study mode',
+        required = True,
+        )
+
+    start_level = schema.TextLine(
+        title = u'start level',
+        required = True,
+        )
+    
+    end_level = schema.TextLine(
+        title = u'end level',
+        required = True,
+        )
+    
+    application_category = schema.TextLine(
+        title = u'application category',
+        required = True,
+        )
+    
+    m_prefix = schema.TextLine(
+        title = u'prefix',
+        required = True,
+        )
+        
+    max_pass = schema.TextLine(
+        title = u'maximum pass',
+        required = True,
+        )
+    
+
+    
+class ICertificateContainer(IWAeUPContainer):
+    """A container for certificates.
+    """
+    def addCertificate(faculty):
+        """Add an ICertificate object.
+
+        Returns the key, under which the object was stored.
+        """
+
+class ICertificateCourse(IWAeUPObject):
+    """A certificatecourse is a course referenced by a certificate, which
+       provides some own attributes.
+    """
+    course = schema.Choice(
+        title = u'Course',
+        source = CourseSource(),
+        )
+    
+    level = schema.Int(
+        title = u'Level of this course',
+        required = True,
+        default = 100
+        )
+
+    core_or_elective = schema.Bool(
+        title = u'Is mandatory course (not elective)',
+        required = True,
+        default = True
+        )
+
+    def getCourseCode():
+        """Return the code of the referenced course.
+
+        This is needed for cataloging.
+        """
+        
+class IWAeUPExporter(Interface):
+    """An exporter for objects.
+    """
+    def export(obj, filepath=None):
+        """Export by pickling.
+
+        Returns a file-like object containing a representation of `obj`.
+
+        This is done using `pickle`. If `filepath` is ``None``, a
+        `cStringIO` object is returned, that contains the saved data.
+        """
+
+class IWAeUPXMLExporter(Interface):
+    """An XML exporter for objects.
+    """
+    def export(obj, filepath=None):
+        """Export as XML.
+
+        Returns an XML representation of `obj`.
+
+        If `filepath` is ``None``, a StringIO` object is returned,
+        that contains the transformed data.
+        """
+
+class IWAeUPXMLImporter(Interface):
+    """An XML import for objects.
+    """
+    def doImport(filepath):
+        """Create Python object from XML.
+
+        Returns a Python object.
+        """
+
+class IWAeUPCSVExporter(Interface):
+    """A CSV exporter for objects.
+    """
+
+    def export(obj, filepath=None):
+        """Export as CSV.
+
+        Returns a CSV representation of `obj`.
+
+        If `filepath` is ``None``, a StringIO` object is returned,
+        that contains the transformed data.
+        """
+
+class IWAeUPCSVImporter(Interface):
+    """A CSV importer for objects.
+    """
+    datatype = schema.TextLine(
+        title = u'Data type',
+        description = u'Type of data supported by this filter.',
+        required = True,)
+
+    def doImport(clear_old_data=True, overwrite=True):
+        """Read data from `filepath` and apply data.
+
+        It depends on the context, what 'applying data' means here.
+
+        If `clear_old_data` is False, old data will be
+        preserved. Otherwise all old data will get lost.
+
+        If `overwrite` is False, any existing entries with similar
+        keys might be overwritten. This option is ignored, when
+        `clear_old_data` is set to True.
+        """
+        
+class IUserAccount(IWAeUPObject):
+    """A user account.
+    """
+    name = schema.TextLine(
+        title = u'User ID',
+        description = u'Loginname',
+        required = True,)
+    title = schema.TextLine(
+        title = u'Username',
+        description = u'Real name',
+        required = False,)
+    description = schema.TextLine(
+        title = u'User description',
+        required = False,)
+    password = schema.Password(
+        title = u'Password',
+        required = True,)
+    roles = schema.List(
+        title = u'Roles',
+        value_type = schema.Choice(source=RoleSource()))
+    
+    
+class IUserContainer(IWAeUPObject):
+    """A container for users (principals).
+
+    These users are used for authentication purposes.
+    """
+
+    def addUser(name, password, title=None, description=None):
+        """Add a user.
+        """
+
+    def delUser(name):
+        """Delete a user if it exists.
+        """
+
+class IDataCenter(IWAeUPObject):
+    """A data center.
+
+    TODO : declare methods, at least those needed by pages.
+    """
+    pass
+
+class ICSVDataReceivers(Interface):
+    """An object containing things ready for CSV imports.
+
+    This is pure marker interface. Objects that implement it, indicate
+    that they might provide attributes, which are able to receive CSV
+    data, i.e. for which an IWAeUPCSVDataImporter exists.
+    """
+
+class IDataCenterFile(Interface):
+    """A data center file.
+    """
+    def getDate():
+        """Get creation timestamp from file in human readable form.
+        """
+
+    def getSize():
+        """Get human readable size of file.
+        """
Index: waeup/trunk/src/waeup/permissions.py
===================================================================
--- waeup/trunk/src/waeup/permissions.py	(revision 3551)
+++ waeup/trunk/src/waeup/permissions.py	(revision 4789)
@@ -1,5 +1,61 @@
 import grok
+from zc.sourcefactory.basic import BasicSourceFactory
 
+class Public(grok.Permission):
+    """Everyone-can-do-this-permission.
+
+    This permission is meant to be applied to objects/views/pages
+    etc., that should be usable/readable by everyone.
+
+    We need this to be able to tune default permissions more
+    restrictive and open up some dedicated objects like the front
+    page.
+    """
+    grok.name('waeup.Public')
+
+class ViewPermission(grok.Permission):
+    grok.name('waeup.View')
+
+class ManageUniversity(grok.Permission):
+    grok.name('waeup.manageUniversity')
+
+class ManageUsers(grok.Permission):
+    grok.name('waeup.manageUsers')
+    
 class FacultyRead(grok.Permission):
     grok.name('waeup.facultyread')
+    
+class PortalUser(grok.Role):
+    grok.name('waeup.PortalUser')
+    grok.permissions('waeup.facultyread', 'waeup.View', 'waeup.Public')
 
+class PortalManager(grok.Role):
+    grok.name('waeup.PortalManager')
+    grok.permissions('waeup.manageUniversity', 'waeup.manageUsers',
+                     'waeup.View', 'waeup.Public')
+
+def getRoles():
+    app = grok.getSite()
+    app = None
+    manager = None
+    if app is not None:
+        from zope.securitypolicy.interfaces import IRolePermissionManager
+        manager = IRolePermissionManager(app, None)
+    else:
+        from zope.securitypolicy.rolepermission import (
+            rolePermissionManager as manager)
+    role_permission_map =  manager.getRolesAndPermissions()
+    result = dict()
+    for item in role_permission_map:
+        if not item[1].startswith('waeup.'):
+            # Ignore non-WAeUP roles...
+            continue
+        result[item[1]] = True
+    return sorted(result.keys())
+
+class RoleSource(BasicSourceFactory):
+    def getValues(self):
+        return getRoles()
+    def getTitle(self, value):
+        if isinstance(value, basestring):
+            return value.split('.', 2)[1]
Index: waeup/trunk/src/waeup/permissions.txt
===================================================================
--- waeup/trunk/src/waeup/permissions.txt	(revision 4789)
+++ waeup/trunk/src/waeup/permissions.txt	(revision 4789)
@@ -0,0 +1,21 @@
+WAeUP permissions and roles
+***************************
+
+Permissions and roles used in a WAeUP portal.
+
+:Test-Layer: unit
+
+
+Before we can do something useful with roles and permissions, we have
+to register them. This can be done simply by grokking the `waeup`
+package:
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+We can get all roles defined in a WAeUP portal:
+
+    >>> from waeup.permissions import getRoles
+    >>> getRoles()
+    ['waeup.PortalManager', 'waeup.PortalUser']
+
Index: waeup/trunk/src/waeup/static/app.css
===================================================================
--- waeup/trunk/src/waeup/static/app.css	(revision 3551)
+++ waeup/trunk/src/waeup/static/app.css	(revision 4789)
@@ -8,7 +8,54 @@
 }
 
+#uni-title { 
+        font-size: 24px;
+        font-weight: bold;
+        padding-top: 20px;
+        position: absolute;
+}
+
+#yui-gen1 {
+        background-color: #FFEEBB;
+        border: 3px;
+}
+
+
+div.yui-layout-bd-nohd {
+        background-color: #0f0;
+        border: 0px;
+}
+
+.actionbarlink { 
+        background-color: #feb;
+        padding: 3px;
+        text-align: center;
+}
+
+.actionbarlink a{
+        text-decoration: underline;
+}
+
+.actionbarlink img {
+        position: absolute;
+}
+
+.actionbarlink span { 
+        padding-left: 20px;
+}
+
+.yui-link-button img { 
+        position: absolute;
+        padding-top: 5px;
+}
+
+.yui-link-button a span {
+        padding-left: 20px;
+}
+
 h1,h2,h3 {
 	color: #005500;
-	font-weight: normal;
+	font-weight: bold;
+        font-size: 120%;
+        border-bottom: 1px solid;
 }
 
@@ -17,5 +64,5 @@
 }
 
-a:hover {
+a:hovers {
 	text-decoration: underline;
 }
@@ -62,4 +109,5 @@
 	*/
 	font-size: x-small;
+        text-align: center;
 }
 
@@ -120,2 +168,62 @@
 	font-size: small;
 }
+
+.message { 
+  background-color: #fe0;
+  color: #000;
+  text-align: center;
+  font-weight: bold;
+  padding: 2px;
+  padding-right: 10px;
+  border: 1px dotted #050;
+}
+
+.yui-skin-sam .yui-layout .yui-layout-clip {
+/* collapsed unit */
+  background-color: #edc;
+}
+
+.yui-layout, .yui-layout-doc {
+  background-color: #feb;
+}
+
+.yui-layout-wrap, .yui-layout-bd, .yui-layout-clip { 
+  backgr/ound-color: #feb;
+}
+
+div.yui-layout-bd-nohd { 
+  background-color: #ffc;
+}
+
+.yui-layout-unit { 
+  background-color: #feb;
+}
+
+#alayout-doc { 
+	background-color: #FFEEBB;
+}
+
+#div, .yui-skin-sam .yui-layout .yui-layout-unit div.yui-layout-bd {
+
+}
+
+.yui-skin-sam .yui-layout .yui-layout-unit div.yui-layout-bd {
+/* asd */
+  background-color: #ffc;
+}
+
+#innermain {
+  padding: 5px;
+}
+
+.yui-skin-sam .yui-resize .yui-resize-handle { 
+  background-color: #feb;
+}
+
+#hd {
+  padding: 5px;
+}
+
+th, td { 
+  font-family: arial, sans-serif;
+}
Index: waeup/trunk/src/waeup/static/authenticated.css
===================================================================
--- waeup/trunk/src/waeup/static/authenticated.css	(revision 4789)
+++ waeup/trunk/src/waeup/static/authenticated.css	(revision 4789)
@@ -0,0 +1,4 @@
+/* Styles applied to authenticated users only. */
+html {
+	background-color: #ddd;
+}
Index: waeup/trunk/src/waeup/static/pagelayout.js
===================================================================
--- waeup/trunk/src/waeup/static/pagelayout.js	(revision 4789)
+++ waeup/trunk/src/waeup/static/pagelayout.js	(revision 4789)
@@ -0,0 +1,64 @@
+(function() {
+    var Dom = YAHOO.util.Dom,
+        Event = YAHOO.util.Event;
+
+    Event.onDOMReady(function() {
+        var layout = new YAHOO.widget.Layout({
+            units: [
+                /* header */
+                { position: 'top', height: 114, body: 'hd',
+                  gutter: '1px' },
+                /* footer */
+                { position: 'bottom', height: 30,
+                  body: 'ft', gutter: '-1px' },
+                /* left sidebar */
+                { position: 'left', header: 'Actions', width: 200, resize: true,
+                  body: 'left', gutter: '5px', collapse: true,
+                  collapseSize: 25, scroll: true, animate: true },
+                /* main area */
+	  { position: 'center', body: 'yui-main', scroll: true }
+            ]
+        });
+        layout.on('render', function() {
+            layout.getUnitByPosition('left').on('close', function() {
+                closeLeft();
+            });
+        });
+        layout.render();
+        Event.on('tLeft', 'click', function(ev) {
+            Event.stopEvent(ev);
+            layout.getUnitByPosition('left').toggle();
+        });
+        Event.on('padRight', 'click', function(ev) {
+            Event.stopEvent(ev);
+            var pad = prompt('CSS gutter to apply: ("2px" or "2px 4px" or any combination of the 4 sides)', layout.getUnitByPosition('right').get('gutter'));
+            layout.getUnitByPosition('right').set('gutter', pad);
+        });
+        var closeLeft = function() {
+            var a = document.createElement('a');
+            a.href = '#';
+            a.innerHTML = 'Add Left Unit';
+            Dom.get('closeLeft').parentNode.appendChild(a);
+
+            Dom.setStyle('tLeft', 'display', 'none');
+            Dom.setStyle('closeLeft', 'display', 'none');
+            Event.on(a, 'click', function(ev) {
+                Event.stopEvent(ev);
+                Dom.setStyle('tLeft', 'display', 'inline');
+                Dom.setStyle('closeLeft', 'display', 'inline');
+                a.parentNode.removeChild(a);
+                layout.addUnit(layout.get('units')[3]);
+                layout.getUnitByPosition('left').on('close', function() {
+                    closeLeft();
+                });
+            });
+        };
+        Event.on('closeLeft', 'click', function(ev) {
+            Event.stopEvent(ev);
+            layout.getUnitByPosition('left').close();
+        });
+    });
+
+
+})();
+
Index: waeup/trunk/src/waeup/student/interfaces.py
===================================================================
--- waeup/trunk/src/waeup/student/interfaces.py	(revision 4789)
+++ waeup/trunk/src/waeup/student/interfaces.py	(revision 4789)
@@ -0,0 +1,14 @@
+##
+## interfaces.py
+from zope.interface import Interface
+from zope import schema
+
+class IStudent(Interface):
+    """Representation of a student.
+    """
+    name = schema.TextLine(
+        title = u'Name of student',
+        default = u'Nobody',
+        required = True,
+        )
+
Index: waeup/trunk/src/waeup/student/student.py
===================================================================
--- waeup/trunk/src/waeup/student/student.py	(revision 4789)
+++ waeup/trunk/src/waeup/student/student.py	(revision 4789)
@@ -0,0 +1,73 @@
+import grok
+from grok import index
+from waeup.student.interfaces import IStudent
+
+def fromCsv(a):
+    #Factory method
+    student = Student("Dummy")
+    student.entry_mode, student.end_level, student.name, student.jamb_reg_no, student.level, student.id, student.lga, student.entry_session, student.matric_no, student.sex, student.phone, student.session, student.course, student.mode, student.faculty, student.department, student.verdict, student.review_state,  student.perm_address, student.email = a
+    return student
+
+class Student(grok.Model):
+    """This is a student record.
+    """
+    grok.implements(IStudent)
+
+    #TODO: REFACTOR TO SCHEMA
+    fields = [   "entry_mode",
+                 "end_level",
+                 "name",
+                 "jamb_reg_no",
+                 "level",
+                 "id",
+                 "lga",
+                 "entry_session",
+                 "matric_no",
+                 "sex",
+                 "phone",
+                 "session",
+                 "course",
+                 "mode",
+                 "faculty",
+                 "department",
+                 "verdict",
+                 "review_state",
+                 "perm_address",
+                 "email"
+            ]
+
+    def __init__(self, name=None):
+        """
+        TODO: REFACTOR TO IMPORT METHOD
+        Fields:
+                 entry_mode,
+                 end_level,
+                 name,
+                 jamb_reg_no,
+                 level,
+                 id,
+                 lga,
+                 entry_session,
+                 matric_no,
+                 sex,
+                 phone,
+                 session,
+                 course,
+                 mode,
+                 faculty,
+                 department,
+                 verdict,
+                 review_state,
+                 perm_address,
+                 email
+
+        """
+        self.name = name
+        
+
+    def getCsv(self):
+        #return self.entry_mode, self.end_level, self.name, self.jamb_reg_no, self.level, self.id, self.lga, self.entry_session, self.matric_no, self.sex, self.phone, self.session, self.course, self.mode, self.faculty, self.department, self.verdict, self.review_state,  self.perm_address, self.email
+        return [getattr(self, field, u"") for field in self.fields]
+
+
+grok.global_utility(Student, provides=IStudent)
Index: waeup/trunk/src/waeup/student/student.txt
===================================================================
--- waeup/trunk/src/waeup/student/student.txt	(revision 4789)
+++ waeup/trunk/src/waeup/student/student.txt	(revision 4789)
@@ -0,0 +1,17 @@
+waeup_ng.student
+****************
+
+:Test-Layer: nounit
+
+We can create students::
+
+  >>> from waeup_ng.student import Student
+  >>> s = Student(name='Foo Bar')
+  >>> s
+  <waeup_ng.student.Student object at 0x...>
+
+Students have a name::
+
+  >>> s.name
+  'Mr. Nobody'
+
Index: waeup/trunk/src/waeup/student/studentcontainer.py
===================================================================
--- waeup/trunk/src/waeup/student/studentcontainer.py	(revision 4789)
+++ waeup/trunk/src/waeup/student/studentcontainer.py	(revision 4789)
@@ -0,0 +1,54 @@
+import grok
+import os
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.interfaces import IStudentContainer
+from waeup.utils import importexport as csv
+
+class StudentContainer(grok.Container):
+    """
+    The node containing the student models
+    """
+
+    grok.implements(IStudentContainer)
+    
+class StudentContainerFactory(grok.GlobalUtility):
+    """A factory for faculty containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.StudentContainer')
+    title = u"Create a new student container.",
+    description = u"This factory instantiates new student containers."
+
+    def __call__(self):
+        return StudentContainer()
+
+    def getInterfaces(self):
+        return implementedBy(StudentContainer)
+    
+class Import(grok.View):
+    def render(self):
+        #XXX TODO: Make import form to choose csv-file
+        t = [0]
+        src = os.path.join(os.path.dirname(__file__), '..', 'tests',
+                           'TestData','Students.csv')
+        inputfile = file(src, "r")
+        num = 0
+        headers, rows = csv.readFile(inputfile)
+        for i in rows:
+             s = fromCsv(i)
+             self.context[str(num)] = s
+             num += 1
+             if num > 100:
+                 break
+        return "%s new Students added in %s" % (num, t[0])
+
+class Export(grok.View):    
+    def render(self):
+        #XXX TODO: Make export form to choose csv-file
+        t = [0]
+        result = []
+        for s in self.context.values():
+             result.append(s.getCsv())
+        csv.writeFile(result)
+        return "yeah %s" % t[0]
Index: waeup/trunk/src/waeup/tables.py
===================================================================
--- waeup/trunk/src/waeup/tables.py	(revision 4789)
+++ waeup/trunk/src/waeup/tables.py	(revision 4789)
@@ -0,0 +1,155 @@
+"""Datatables and related components used in different contexts.
+"""
+import grok
+from waeup.interfaces import (IUniversity, IFacultyContainer, IFaculty,
+                              IDepartment, ICourseContainer)
+from waeup.widgets.interfaces import ITableProvider
+from waeup.widgets.table import YUITable as Table
+from waeup.widgets.table import Col
+from zope.security.management import checkPermission
+
+class CodeAndTitleTableProvider(grok.Adapter):
+    """A table provider for objects that have a code, title and prefix.
+    """
+    grok.baseclass()
+    grok.provides(ITableProvider)
+
+    title = None
+
+    def __init__(self, context):
+        self.context = context
+        self.prepare()
+
+    def prepare(self):
+        self.data_source = self.context
+
+    def getItemURL(self, view, value):
+        context_url = ''
+        if not view is None:
+            context_url = view.url(self.data_source) + '/'
+        result = '<a href="%s%s">%s</a>' % (context_url, value, value)
+        return result
+    
+    def getTables(self, view=None):
+        data = self.data_source
+        context_url = ''
+        if view:
+            context_url = view.url(self.context) + '/'
+        cols = (
+            Col(
+                header='Code', sortable=True,
+                data = tuple([self.getItemURL(view, data[x].code)
+                              for x in data])),
+            Col(
+                header='Title', sortable=True,
+                data = tuple([data.getName(x) for x in data])),
+            )
+        self.table = Table(self.title, cols=cols)
+        return (self.table,)
+    
+class DepartmentTableProvider(CodeAndTitleTableProvider):
+    grok.context(IFaculty)
+    grok.provides(ITableProvider)
+    title = 'Departments'
+
+class FacultyTableProvider(CodeAndTitleTableProvider):
+    grok.context(IFacultyContainer)
+    grok.provides(ITableProvider)
+    title = 'Faculties'
+
+class UniversityTableProvider(CodeAndTitleTableProvider):
+    grok.context(IUniversity)
+    grok.provides(ITableProvider)
+    title = 'Faculties'
+
+    def prepare(self):
+        self.data_source = self.context.faculties
+
+class CourseTableProvider(CodeAndTitleTableProvider):
+    grok.context(IDepartment)
+    grok.provides(ITableProvider)
+    title = 'Courses'
+
+    def prepare(self):
+        self.data_source = self.context.courses
+        
+    def getTables(self, view=None):
+        data = self.data_source
+        context_url = ''
+        if view:
+            context_url = view.url(self.context) + '/'
+
+        # Create a courses table...
+        cols = (
+            Col(
+                header='Code', sortable=True,
+                data = tuple([self.getItemURL(view, data[x].code)
+                              for x in data])),
+            Col(
+                header='Title', sortable=True,
+                data = tuple([data[x].title for x in data])),
+            )
+        delcol = Col(header='', sortable=False,
+                     data = [
+                '<form method="post">'
+                '<input type="submit" name="delcourse" value="Delete" />'
+                '<input type="hidden" name="code" value="%s" />'
+                '</form>' % (data[x].code)
+                for x in data])
+
+        if checkPermission('waeup.manageUniversity', self.context.courses):
+            cols = list(cols)
+            cols.append(delcol)
+            cols = tuple(cols)
+        self.coursetable = Table('Courses', cols=cols)
+
+        # Create a certificates table...
+        self.data_source = self.context.certificates
+        data = self.data_source
+        certcols =(
+            Col(
+                header='Code', sortable=True,
+                data = tuple([self.getItemURL(view, data[x].code)
+                              for x in data])),
+            Col(
+                header='Title', sortable=True,
+                data = tuple([data[x].title for x in data])),
+            )
+
+        delcol = Col(header='', sortable=False,
+                     data = [
+                '<form method="post">'
+                '<input type="submit" name="delcert" value="Delete" />'
+                '<input type="hidden" name="code" value="%s" />'
+                '</form>' % (data[x].code)
+                for x in data])
+
+        if checkPermission('waeup.manageUniversity', self.context.certificates):
+            certcols = list(certcols)
+            certcols.append(delcol)
+            certcols = tuple(certcols)
+
+        self.certtable = Table('Certificates', cols=certcols)
+        return (self.coursetable, self.certtable)
+
+class CourseContainerCourseTableProvider(CodeAndTitleTableProvider):
+    grok.context(ICourseContainer)
+    grok.provides(ITableProvider)
+    title = 'Courses'
+
+    def getTables(self, view=None):
+        data = self.data_source
+        context_url = ''
+        if view:
+            context_url = view.url(self.context) + '/'
+        cols = (
+            Col(
+                header='Code', sortable=True,
+                data = tuple([self.getItemURL(view, data[x].code)
+                              for x in data])),
+            Col(
+                header='Title', sortable=True,
+                data = tuple([data[x].title for x in data])),
+            )
+        self.table = Table(self.title, cols=cols)
+        return (self.table,)
Index: waeup/trunk/src/waeup/testing.py
===================================================================
--- waeup/trunk/src/waeup/testing.py	(revision 3551)
+++ waeup/trunk/src/waeup/testing.py	(revision 4789)
@@ -5,3 +5,4 @@
 ftesting_zcml = os.path.join(
     os.path.dirname(waeup.__file__), 'ftesting.zcml')
-FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer')
+FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer',
+                            allow_teardown=True)
Index: waeup/trunk/src/waeup/tests/test_waeup.py
===================================================================
--- waeup/trunk/src/waeup/tests/test_waeup.py	(revision 3551)
+++ waeup/trunk/src/waeup/tests/test_waeup.py	(revision 4789)
@@ -5,5 +5,12 @@
 """
 import grok
+import zope.component.eventtesting
+from waeup.testing import FunctionalLayer
+
+def setUpZope(test):
+    zope.component.eventtesting.setUp(test)
+
 # Register all tests in the waeup_ng package
 test_suite = grok.testing.register_all_tests(
-    'waeup')
+    'waeup', usetup=setUpZope,
+    layer=FunctionalLayer)
Index: waeup/trunk/src/waeup/university/__init__.py
===================================================================
--- waeup/trunk/src/waeup/university/__init__.py	(revision 3551)
+++ waeup/trunk/src/waeup/university/__init__.py	(revision 4789)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: waeup/trunk/src/waeup/university/api.txt
===================================================================
--- waeup/trunk/src/waeup/university/api.txt	(revision 4789)
+++ waeup/trunk/src/waeup/university/api.txt	(revision 4789)
@@ -0,0 +1,25 @@
+:mod:`waeup.university` -- academic components
+**********************************************
+
+.. module:: waeup.university
+
+Components that represent academic 'core' stuff like faculties, 
+
+Submodules
+==========
+
+The :mod:`waeup.university` module provides the following submodules:
+
+.. toctree::
+   :maxdepth: 1
+
+   certificate.txt
+   certificatecontainer.txt
+   course.txt
+   coursecontainer.txt
+   department.txt
+   faculty.txt
+   facultycontainer.txt
+
+
+
Index: waeup/trunk/src/waeup/university/certificate.py
===================================================================
--- waeup/trunk/src/waeup/university/certificate.py	(revision 3551)
+++ waeup/trunk/src/waeup/university/certificate.py	(revision 4789)
@@ -0,0 +1,144 @@
+"""WAeUP portal certificates
+"""
+import grok
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from zope.app.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from zope.component.interfaces import IFactory, ComponentLookupError
+from zope.interface import implementedBy
+from waeup.interfaces import ICertificate, ICertificateCourse, ICourse
+
+class Certificate(grok.Container):
+    """A certificate.
+    """
+    grok.implements(ICertificate)
+
+    def __init__(self, code=u'NA', title=u'Unnamed Certificate',
+                 category=None, study_mode=None, start_level=None,
+                 end_level=None, application_category=None, m_prefix=u'',
+                 max_pass = u''):
+        super(Certificate, self).__init__()
+        self.code = code
+        self.title = title
+        self.category = category
+        self.study_mode = study_mode
+        self.start_level = start_level
+        self.end_level = end_level
+        self.application_category = application_category
+        self.m_prefix = m_prefix
+        self.max_pass = max_pass
+        # Initialize workflow state...
+        self.setReviewState('unchecked')
+
+    def check(self):
+        """Set Review State to ``checked``.
+        """
+        self.setReviewState(state='checked')
+
+    def getReviewState(self):
+        return IWorkflowState(self).getState()
+
+    def setReviewState(self, state=None):
+        """Try to set new state.
+
+        Instead of simply setting a value, we fire a transition of a
+        workflow. This might fail if, for instance, the required state
+        is not part of the workflow or unreachable from current state.
+        """
+        if state == self.getReviewState():
+            # Accept staying in the same state, even if workflow does
+            # not allow this.
+            return
+        info = IWorkflowInfo(self)
+        info.fireTransitionToward(state)
+        return
+
+    review_state = property(getReviewState, setReviewState)
+    
+    def addCourseRef(self, course, level=100, core_or_elective=True):
+        """Add a course reference.
+        """
+        code = "%s_%s" % (course.code, level)
+        self[code] = CertificateCourse(course, level, core_or_elective)
+        self[code].__parent__ = self
+        self[code].__name__ = code
+        self._p_changed = True
+
+    def delCourseRef(self, code, level=None):
+        """Delete a course denoted by its code.
+        """
+        for key in self.keys():
+            if self[key].getCourseCode() != code:
+                continue
+            if level is not None and str(self[key].level) != str(level):
+                # found a course with correct key but wrong level...
+                continue
+            del self[key]
+            self._p_changed = True
+        return
+
+
+
+
+class CertificateFactory(grok.GlobalUtility):
+    """A factory for certificates.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Certificate')
+    title = u"Create a new certificate.",
+    description = u"This factory instantiates new certificate instances."
+
+    def __call__(self, *args, **kw):
+        return Certificate(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(Certificate)
+
+class CertificateCourse(grok.Model):
+    grok.implements(ICertificateCourse)
+
+    def __init__(self, course=None, level=100, core_or_elective=True):
+        self.course = course
+        self.level = level
+        self.core_or_elective = core_or_elective
+
+    def getCourseCode(self):
+        """Get code of a course.
+        """
+        return self.course.code
+    
+class CertificateCourseFactory(grok.GlobalUtility):
+    """A factory for certificate courses.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.CertificateCourse')
+    title = u"Create a new certificate course.",
+    description = u"This factory instantiates new certificate courses."
+
+    def __call__(self, *args, **kw):
+        return CertificateCourse(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(CertificateCourse)
+
+@grok.subscribe(ICourse, grok.IObjectRemovedEvent)
+def removedCourseHandler(course, event):
+    """If a course is deleted, we make sure that also references in a
+       certificatecontainer are removed.
+    """
+    code = course.code
+
+    # Find all certcourses that reference the given course...
+    try:
+        cat = getUtility(ICatalog, name='certcourses_catalog')
+    except ComponentLookupError:
+        # catalog not available. This might happen during tests.
+        return
+        
+    results = cat.searchResults(course_code=(code, code))
+    for certcourse in results:
+        # Remove that reference...
+        cert = certcourse.__parent__
+        cert.delCourseRef(code)
+        cert._p_changed = True
+    return
Index: waeup/trunk/src/waeup/university/certificate.txt
===================================================================
--- waeup/trunk/src/waeup/university/certificate.txt	(revision 4789)
+++ waeup/trunk/src/waeup/university/certificate.txt	(revision 4789)
@@ -0,0 +1,443 @@
+:mod:`waeup.university.certificate` -- Certificates for WAeUP
+*************************************************************
+
+.. module:: waeup.university.certificate
+
+Components that represent and manage certificates.
+
+:Test-Layer: unit
+
+Because certificates make use of components registered with the Zope
+Component Architecture (ZCA), we first have to grok the `waeup`
+package. This happens automatically in real-world use:
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+
+Content Classes (models and containers)
+=======================================
+
+
+:class:`Certificate`
+--------------------
+
+.. class:: Certificate([code=u'NA',[ title=u'Unnamed Certificate',[ category=None,[ study_mode=None,[ start_level=None,[ end_level=None,[ application_category=None,[ m_prefix=u'',[  max_pass = u'']]]]]]]]])
+
+   Create a certificate object with the given parameters.
+
+   .. attribute:: grok.implements(ICertificate)
+
+   All parameters are optional:
+
+     >>> from waeup.university.certificate import Certificate
+     >>> mycertificate = Certificate()
+
+   Certificates have the attributes required by the `ICertificate` interface:
+
+     >>> from waeup.interfaces import ICertificate
+     >>> ICertificate.providedBy(mycertificate)
+     True
+
+     >>> from zope.interface.verify import verifyObject
+     >>> verifyObject(ICertificate, mycertificate)
+     True
+
+   Beside the attributes, certificates are containers for
+   certificate-courses (see :class:`CertificateCourse`). Each
+   certificate course can be accessed by the code of the course it wraps.
+
+   .. attribute:: title
+
+      Each certificate has a title:
+
+        >>> mycertificate.title
+        u'Unnamed Certificate'
+
+   .. attribute:: code
+
+      Each certificate holds a code, which might be a shortcut or
+      abbreviation of the real certificate name. By default the code
+      is ``NA`` (=not assigned):
+
+        >>> mycertificate.code
+        u'NA'
+
+   .. attribute:: review_state
+
+      The review state can have one of the ``checking`` states defined
+      in the WAeUP workflow. These are at least the states ``checked``
+      and ``unchecked``. After a certificate is created, the review
+      state is ``unchecked``:
+
+        >>> mycertificate.review_state
+        'unchecked'
+
+      .. seealso::
+         :meth:`Certificate.check` -- mark a certificate as ``checked``
+
+   .. attribute:: category
+
+      Each :class:`Certificate` instance has a category:
+
+        >>> print mycertificate.category
+        None
+
+      .. XXX: This is not a proper description
+
+   .. attribute:: study_mode
+
+      Each :class:`Certificate` instance has a study mode:
+
+        >>> print mycertificate.study_mode
+        None
+
+      .. XXX: This is not a proper description
+
+   .. attribute:: start_level
+
+      Each :class:`Certificate` instance has a start level:
+
+        >>> print mycertificate.start_level
+        None
+
+      .. XXX: This is not a proper description
+
+   .. attribute:: end_level
+
+      Each :class:`Certificate` instance has a end level:
+
+        >>> print mycertificate.end_level
+        None
+
+      .. XXX: This is not a proper description
+   
+
+   .. attribute:: application_category
+
+      Each :class:`Certificate` instance has an application category:
+
+        >>> print mycertificate.application_category
+        None
+
+      .. XXX: This is not a proper description
+
+   
+   .. attribute:: m_prefix
+
+      Each :class:`Certificate` instance has an ``m_prefix``:
+
+        >>> mycertificate.m_prefix
+        u''
+
+      .. XXX: This is not a proper description
+
+   
+   .. attribute:: max_pass
+
+      Each :class:`Certificate` instance has a maximum number of passes:
+
+        >>> mycertificate.max_pass
+        u''
+
+      .. XXX: This is not a proper description
+
+
+   .. method:: check()
+
+      Mark a certificate instance's review state as ``checked``:
+
+        >>> mycertificate.review_state
+        'unchecked'
+
+        >>> mycertificate.check()
+        >>> mycertificate.review_state
+        'checked'
+
+      We cannot uncheck a certificate:
+
+        >>> mycertificate.review_state = 'unchecked'
+        Traceback (most recent call last):
+	...
+        NoTransitionAvailableError
+
+      The only states accepted at all are ``checked`` and
+      ``unchecked``. If we want to set something else this won't work:
+
+        >>> mycertificate.review_state = 'nonsense'
+        Traceback (most recent call last):
+	...
+        NoTransitionAvailableError
+
+
+   .. method:: addCourseRef(course[, level=100,[ core_or_elective=True]])
+
+      Add a reference to a course. A course is an object implementing
+      :class:`waeup.interfaces.ICourse`.
+
+      Please don't be confused by the term 'reference'. This just
+      means an ordinary :class:`waeup.university.course.Course` object
+      in almost all cases. As this object will normaly be one stored
+      in a department, the course here will simply become a reference
+      to the 'real' one in the department container.
+
+   .. method:: delCourseRef(code)
+
+      Remove a course from a certificate.
+
+      The course must be given by its code number.
+
+:class:`CertificateCourse`
+--------------------------
+
+.. class:: CertificateCourse(course[, level=100[, core_or_elective=True]])
+
+   Create a certificate course.
+
+   A certificate-course is a course (:class:`Course`) which is part of
+   a certificate. Normally, certificate-courses are held in
+   certificates and refer to an existing :class:`Course` instance held
+   elsewhere.
+
+   A certificate can require several courses and one
+   course can be required by several certificates.
+
+   .. attribute:: course
+
+      An instance of :class:`ICourse`.
+
+   .. attribute:: level
+
+      An integer telling the level to which this course applies.
+
+   .. attribute:: core_or_elective
+
+      A bool stating whether this course is required or optional to
+      get the certificate.
+
+
+
+Utilities
+=========
+
+:class:`CertificateFactory`
+---------------------------
+
+.. class:: CertificateFactory()
+
+   .. attribute:: grok.name(u'waeup.Certificate')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`Certificate`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycertificate = createObject(u'waeup.Certificate')
+     >>> mycertificate
+     <waeup.university.certificate.Certificate object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.university.certificate import CertificateFactory
+     >>> verifyClass(IFactory, CertificateFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> certificate_factory = CertificateFactory()
+     >>> certificate_factory.getInterfaces()
+     <implementedBy waeup.university.certificate.Certificate>
+
+
+:class:`CertificateCourseFactory`
+---------------------------------
+
+.. class:: CertificateCourseFactory()
+
+   .. attribute:: grok.name(u'waeup.CertificateCourse')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`CertificateCourse`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycertificatecourse = createObject(u'waeup.CertificateCourse')
+     >>> mycertificatecourse
+     <waeup.university.certificate.CertificateCourse object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.university.certificate import CertificateCourseFactory
+     >>> verifyClass(IFactory, CertificateCourseFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> certcourse_factory = CertificateCourseFactory()
+     >>> certcourse_factory.getInterfaces()
+     <implementedBy waeup.university.certificate.CertificateCourse>
+
+
+Event Subscribers
+=================
+
+.. function:: removedCourseHandler(course, event)
+
+   An event subscriber triggered for
+   :class:`grok.IObjectRemovedEvent`s, when an :class:`ICourse`
+   instance is removed from a container.
+
+   Tries to remove all referencing :class:`CertificateCourse`
+   instances that reference the removed course.
+
+   To accomplish that, the parents of the removed course are looked up
+   for a certifcate container which contains a certificate-course that
+   contains a reference to the deleted course.
+
+   .. seealso:: :ref:`removecertificatecourses`
+
+   **handles:**
+     :class:`ICourse`
+
+   **event type:**
+     :class:`grok.IObjectRemovedEvent`
+
+Examples
+========
+
+Certificates
+------------
+
+We can create certificates:
+
+    >>> from waeup.university.certificate import Certificate
+    >>> mycertificate = Certificate()
+    >>> mycertificate
+    <waeup.university.certificate.Certificate object at 0x...>
+
+Another way to create certificates is by asking for a factory called
+``waeup.Certificate``. This way we can create a factory without
+importing a class:
+
+    >>> from zope.component import createObject
+    >>> mycertificate = createObject(u'waeup.Certificate')
+    >>> mycertificate
+    <waeup.university.certificate.Certificate object at 0x...>
+
+CertificateCourses
+------------------
+
+:class:`CertificateCourse` instances comply with the
+:class:`ICertificateCourse` interface:
+
+    >>> from waeup.interfaces import ICertificateCourse
+    >>> from waeup.university.certificate import CertificateCourse
+    >>> mycertcourse = CertificateCourse(None, 200, False)
+    >>> ICertificateCourse.providedBy(mycertcourse)
+    True
+
+    >>> from zope.interface.verify import verifyObject
+    >>> verifyObject(ICertificateCourse, mycertcourse)
+    True
+
+Also instances of :class:`CertificateCourse` can be created by asking
+the component architechture:
+
+    >>> from zope.component import createObject
+    >>> mycertcourse = createObject(u'waeup.CertificateCourse')
+    >>> mycertcourse
+    <waeup.university.certificate.CertificateCourse object at 0x...>
+
+.. _removecertificatecourses:
+
+Persistence of certificate courses
+----------------------------------
+
+If a certificate course requires a certain course and this is course
+is deleted, also the referencing certificate course is deleted.
+
+We setup a data structure that reflects typical usage. It looks like
+this::
+
+    Department-Instance
+    |
+    +---> courses
+    |        |
+    |        +--------------------> Course-Instance
+    |                                        ^
+    +---> certificates                       |
+             |                               |
+             +-----> Certificate-Instance    |
+                        |                    |
+                        +------> Certificate-Course
+
+The certifcate-Course here refers to a Course-Instance.
+
+In Python we build such a structure like this (from top to bottom):
+
+    >>> from zope.component import createObject
+    >>> mydept = createObject('waeup.Department')
+
+In real world use this data will be stored in a ZODB. We setup our own
+litte ZODB backend (which is easy!):
+
+    >>> from ZODB import FileStorage, DB
+    >>> dbpath = 'tinyData.fs'
+    >>> class TinyZODB(object):
+    ...   def __init__(self, path=dbpath):
+    ...     self.storage = FileStorage.FileStorage(path)
+    ...     self.db = DB(self.storage)
+    ...     self.connection = self.db.open()
+    ...     self.dbroot = self.connection.root()
+    ...   def close(self):
+    ...     self.connection.close()
+    ...     self.db.close()
+    ...     self.storage.close()
+
+Now we can use this ZODB as backend database and store our data
+structure:
+
+    >>> import transaction
+    >>> db = TinyZODB()
+    >>> dbroot = db.dbroot
+    >>> dbroot['mydept'] = mydept
+    >>> mycourse = createObject('waeup.Course')
+    >>> mycourse.code = 'MYCOURSE'
+    >>> mydept.courses.addCourse(mycourse)
+    >>> mycert = createObject('waeup.Certificate')
+    >>> mycert.code = 'MYCERT'
+    >>> mydept.certificates.addCertificate(mycert)
+    >>> mycert.addCourseRef(mycourse)
+
+    >>> transaction.commit()
+
+The data is now stored in the ZODB. We can close the DB, reopen it
+later and the data will still be there:
+
+    >>> db.close()
+    >>> newdb = TinyZODB()
+    >>> newdbroot = newdb.dbroot
+    >>> list(newdbroot)
+    ['mydept']
+
+The certificate-course we stored in the certificate is indeed a
+reference to the course, not a copy of it:
+
+    >>> course = newdbroot['mydept'].courses['MYCOURSE']
+    >>> certcourse = newdbroot['mydept'].certificates['MYCERT']['MYCOURSE_100']
+    >>> certcourse.course is course
+    True
+
+So, we can be sure that modifications to the course are immediately
+reflected in the certcourse.
+
Index: waeup/trunk/src/waeup/university/certificatecontainer.py
===================================================================
--- waeup/trunk/src/waeup/university/certificatecontainer.py	(revision 4789)
+++ waeup/trunk/src/waeup/university/certificatecontainer.py	(revision 4789)
@@ -0,0 +1,38 @@
+"""Containers for certificates.
+"""
+import grok
+from waeup.interfaces import ICertificateContainer, ICertificate
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+
+class CertificateContainer(grok.Container):
+    """See interfaces for description.
+    """
+    grok.implements(ICertificateContainer)
+    grok.require('waeup.manageUniversity')
+
+    def addCertificate(self, certificate):
+        if not ICertificate.providedBy(certificate):
+            raise TypeError('CertificateContainers contain only '
+                            'ICertificate instances')
+        self[certificate.code] = certificate
+        return
+
+    def clear(self):
+        keys = self.keys()
+        for key in keys:
+            del self[key]
+
+class CertificateContainerFactory(grok.GlobalUtility):
+    """A factory for certificate containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.CertificateContainer')
+    title = u"Create a new certificate container.",
+    description = u"This factory instantiates new certificate containers."
+
+    def __call__(self, *args, **kw):
+        return CertificateContainer(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(CertificateContainer)
Index: waeup/trunk/src/waeup/university/certificatecontainer.txt
===================================================================
--- waeup/trunk/src/waeup/university/certificatecontainer.txt	(revision 4789)
+++ waeup/trunk/src/waeup/university/certificatecontainer.txt	(revision 4789)
@@ -0,0 +1,155 @@
+:mod:`waeup.university.certificatecontainer` -- Certificate containers
+**********************************************************************
+
+.. module:: waeup.university.certificatecontainer
+
+Containers for certificates.
+
+:Test-Layer: unit
+
+Because this module makes use of components registered with the Zope
+Component Architecture (ZCA), we first have to grok the `waeup`
+package if we want to deploy it. This happens automatically in
+real-world use:
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+Content Classes (models and containers)
+=======================================
+
+:class:`CertificateContainer`
+-----------------------------
+
+.. class:: CertificateContainer()
+
+   Create a certificate container instance.
+
+   .. attribute:: grok.implements(ICertificateContainer)
+
+   .. attribute:: grok.require('waeup.manageUniversity')
+
+   :class:`CertificateContainer` instances are instances of
+   :class:`grok.Container` that hold and manage instances of
+   :class:`waeup.university.certificate.Certificate`.
+
+     >>> from waeup.university.certificatecontainer import CertificateContainer
+     >>> mycontainer = CertificateContainer()
+     >>> mycontainer
+     <waeup.university.certificatecontainer.CertificateContainer object at 0x...>
+
+   Certificate containers provide `ICertificateContainer`:
+
+     >>> from waeup.interfaces import ICertificateContainer
+     >>> ICertificateContainer.providedBy(mycontainer)
+     True
+
+   We can be sure, that the full interface is supported by the
+   CertificateContainer class:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> verifyClass(ICertificateContainer, CertificateContainer)
+     True
+
+   As normal ``grok.Container``, :class:`CertificateContainer` instances
+   can be used like dicts:
+
+     >>> list(mycontainer.keys())
+     []
+
+   .. method:: addCertificate(certificate)
+
+      Add a certificate.
+
+      *certificate* must be an instance of
+      :class:`waeup.university.certificate.Certificate`:
+
+        >>> from waeup.university.certificatecontainer import (
+        ...   CertificateContainer)
+        >>> mycontainer = CertificateContainer()
+        >>> mycontainer.addCertificate('blah')
+        Traceback (most recent call last):
+        ...
+        TypeError: CertificateContainers contain only ICertificate instances
+
+      The key of added items is the ``code`` attribute:
+
+        >>> from waeup.university.certificate import Certificate
+        >>> mycertificate = Certificate(code='CERTIFICATE1')
+        >>> mycontainer.addCertificate(mycertificate)
+        >>> list(mycontainer.keys())
+        [u'CERTIFICATE1']
+
+   .. method:: clear()
+
+      Remove all contained certificates:
+
+        >>> len(mycontainer.items())
+        1
+
+        >>> mycontainer.clear()
+        >>> len(mycontainer.items())
+        0
+
+Utilities
+=========
+
+:class:`CertificateContainerFactory`
+------------------------------------
+
+.. class:: CertificateContainerFactory()
+
+   .. attribute:: grok.name(u'waeup.CertificateContainer')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`CertificateContainer`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycertificatecontainer = createObject(u'waeup.CertificateContainer')
+     >>> mycertificatecontainer
+     <waeup.university.certificatecontainer.CertificateContainer object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.university.certificatecontainer import (
+     ...   CertificateContainerFactory)
+     >>> verifyClass(IFactory, CertificateContainerFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> certificatecontainer_factory = CertificateContainerFactory()
+     >>> certificatecontainer_factory.getInterfaces()
+     <implementedBy waeup.university.certificatecontainer.CertificateContainer>
+
+
+Examples
+========
+
+Getting a certificate container
+-------------------------------
+
+We can easily create `CertificateContainers`:
+
+    >>> from waeup.university.certificatecontainer import CertificateContainer
+    >>> mycontainer = CertificateContainer()
+
+
+Another way to get a certificate container -- without importing the
+class -- is via factories. We registered a factory for certificate
+containers under the name ``waeup.CertificateContainer``. Now we can
+ask for an object by calling the appropriate factory:
+
+    >>> from zope.component import createObject
+    >>> createObject(u'waeup.CertificateContainer')
+    <waeup.university.certificatecontainer.CertificateContainer object at 0x...>
+
+This way we get a thing that implements ICertificateContainer without
+imports or similar.
+
Index: waeup/trunk/src/waeup/university/course.py
===================================================================
--- waeup/trunk/src/waeup/university/course.py	(revision 3551)
+++ waeup/trunk/src/waeup/university/course.py	(revision 4789)
@@ -0,0 +1,40 @@
+"""Courses.
+"""
+import grok
+from zope.interface import implementedBy
+from zope.component.interfaces import IFactory
+from waeup.interfaces import ICourse
+
+class Course(grok.Model):
+    """A university course.
+    """
+    grok.implements(ICourse)
+
+    def __init__(self,
+                 title=u'Unnamed Course',
+                 code=u'NA',
+                 level=None,
+                 credits=0,
+                 passmark=40,
+                 semester=1, **kw):
+        super(Course, self).__init__(**kw)
+        self.title = title
+        self.code = code
+        self.level = level
+        self.credits = credits
+        self.passmark = passmark
+        self.semester = semester
+        
+class CourseFactory(grok.GlobalUtility):
+    """A factory for courses.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Course')
+    title = u"Create a new course.",
+    description = u"This factory instantiates new course instances."
+
+    def __call__(self, *args, **kw):
+        return Course(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(Course)
Index: waeup/trunk/src/waeup/university/course.txt
===================================================================
--- waeup/trunk/src/waeup/university/course.txt	(revision 4789)
+++ waeup/trunk/src/waeup/university/course.txt	(revision 4789)
@@ -0,0 +1,157 @@
+:mod:`waeup.university.course` -- Courses
+*****************************************
+
+.. module:: waeup.university.course
+
+Components that represent courses.
+
+:Test-Layer: unit
+
+Because this module makes use of components registered with the Zope
+Component Architecture (ZCA), we first have to grok the `waeup`
+package if we want to deploy it. This happens automatically in
+real-world use:
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+Content Classes (models and containers)
+=======================================
+
+:class:`Course`
+---------------
+
+.. class:: Course([code=u'NA'[, title=u'Unnamed Course'[, level=None[, credits=0[, passmark=40[, semester=1]]]]]])
+
+   Create a course instance with the given parameters.
+
+   .. attribute:: grok.implements(ICourse)
+
+   All parameters are optional:
+
+     >>> from waeup.university.course import Course
+     >>> mycourse = Course()
+     >>> mycourse
+     <waeup.university.course.Course object at 0x...>
+
+   Course instances have the attributes required by the
+   :class:`waeup.interfaces.ICourse` interface:
+
+     >>> from waeup.interfaces import ICourse
+     >>> ICourse.providedBy(mycourse)
+     True
+
+     >>> from zope.interface.verify import verifyObject
+     >>> verifyObject(ICourse, mycourse)
+     True
+
+   .. attribute:: title
+
+      Each course has a title:
+
+        >>> mycourse.title
+        u'Unnamed Course'
+
+   .. attribute:: code
+
+      Each course holds a code, which might be a shortcut or
+      abbreviation of the real course name. By default the code is
+      ``NA`` (=not assigned).
+
+      This value has to be unique if used in a :class:`Department`
+      instance as it serves as a key:
+
+        >>> mycourse.code
+        u'NA'
+
+   .. attribute:: passmark
+
+      Each course holdes a passmark, which is ``40`` by default:
+
+        >>> mycourse.passmark
+        40
+
+   .. attribute:: semester
+
+      Each course holds a semester attribute which is ``1`` by
+      default:
+
+        >>> mycourse.semester
+        1
+
+   .. attribute:: credits
+
+      Each course holds the number of credits that can be
+      earned. Default is ``0``.
+
+        >>> mycourse.credits
+        0
+
+   .. attribute:: level
+
+      Courses can have a level. This attribute is not required and the
+      default value is ``None``:
+
+        >>> mycourse.level is None
+        True
+
+
+Utilities
+=========
+
+:class:`CourseFactory`
+---------------------------
+
+.. class:: CourseFactory()
+
+   .. attribute:: grok.name(u'waeup.Course')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`Course`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycourse = createObject(u'waeup.Course')
+     >>> mycourse
+     <waeup.university.course.Course object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.university.course import CourseFactory
+     >>> verifyClass(IFactory, CourseFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> course_factory = CourseFactory()
+     >>> course_factory.getInterfaces()
+     <implementedBy waeup.university.course.Course>
+
+Examples
+========
+
+Creating courses
+----------------
+
+The simplest way to create a :class:`Course` instance is to import the
+class and calling the constructor:
+
+    >>> from waeup.university.course import Course
+    >>> mycourse = Course()
+    >>> mycourse
+    <waeup.university.course.Course object at 0x...>
+
+Another way to create courses is by asking for a factory called
+``waeup.Course``. This way we can create a factory without importing a
+class:
+
+    >>> from zope.component import createObject
+    >>> mycourse = createObject(u'waeup.Course')
+    >>> mycourse
+    <waeup.university.course.Course object at 0x...>
+
Index: waeup/trunk/src/waeup/university/coursecontainer.py
===================================================================
--- waeup/trunk/src/waeup/university/coursecontainer.py	(revision 4789)
+++ waeup/trunk/src/waeup/university/coursecontainer.py	(revision 4789)
@@ -0,0 +1,41 @@
+"""Course containers.
+"""
+import grok
+from zope.interface import implementedBy
+from zope.component.interfaces import IFactory
+from waeup.interfaces import ICourseContainer, ICourse
+
+class CourseContainer(grok.Container):
+    """See interfaces for description.
+    """
+    grok.implements(ICourseContainer)
+    grok.require('waeup.manageUniversity')
+
+    def addCourse(self, course):
+        """Add a course.
+        """
+        if not ICourse.providedBy(course):
+            raise TypeError('CourseContainers contain only ICourse instances')
+        self[course.code] = course
+        return
+
+    def clear(self):
+        """Remove all objects from course container.
+        """
+        keys = self.keys()
+        for key in keys:
+            del self[key]
+
+class CourseContainerFactory(grok.GlobalUtility):
+    """A factory for course containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.CourseContainer')
+    title = u"Create a new course container.",
+    description = u"This factory instantiates new course containers."
+
+    def __call__(self, *args, **kw):
+        return CourseContainer(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(CourseContainer)
Index: waeup/trunk/src/waeup/university/coursecontainer.txt
===================================================================
--- waeup/trunk/src/waeup/university/coursecontainer.txt	(revision 4789)
+++ waeup/trunk/src/waeup/university/coursecontainer.txt	(revision 4789)
@@ -0,0 +1,153 @@
+:mod:`waeup.university.coursecontainer` -- Course containers
+************************************************************
+
+.. module:: waeup.university.coursecontainer
+
+Containers for courses.
+
+:Test-Layer: unit
+
+Because this module makes use of components registered with the Zope
+Component Architecture (ZCA), we first have to grok the `waeup`
+package if we want to deploy it. This happens automatically in
+real-world use:
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+Content Classes (models and containers)
+=======================================
+
+:class:`CourseContainer`
+------------------------
+
+.. class:: CourseContainer()
+
+   Create a course container instance.
+
+   .. attribute:: grok.implements(ICourseContainer)
+
+   .. attribute:: grok.require('waeup.manageUniversity')
+
+   :class:`CourseContainer` instances are instances of
+   :class:`grok.Container` that hold and manage instances of
+   :class:`waeup.university.course.Course`.
+
+     >>> from waeup.university.coursecontainer import CourseContainer
+     >>> mycontainer = CourseContainer()
+     >>> mycontainer
+     <waeup.university.coursecontainer.CourseContainer object at 0x...>
+
+   Course containers provide `ICourseContainer`:
+
+     >>> from waeup.interfaces import ICourseContainer
+     >>> ICourseContainer.providedBy(mycontainer)
+     True
+
+   We can be sure, that the full interface is supported by the
+   CourseContainer class:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> verifyClass(ICourseContainer, CourseContainer)
+     True
+
+   As normal ``grok.Container``, :class:`CourseContainer` instances
+   can be used like dicts:
+
+     >>> list(mycontainer.keys())
+     []
+
+   .. method:: addCourse(course)
+
+      Add a course.
+
+      *course* must be an instance of
+      :class:`waeup.university.course.Course`:
+
+        >>> from waeup.university.coursecontainer import CourseContainer
+        >>> mycontainer = CourseContainer()
+        >>> mycontainer.addCourse('blah')
+        Traceback (most recent call last):
+        ...
+        TypeError: CourseContainers contain only ICourse instances
+
+      The key of added items is the ``code`` attribute:
+
+        >>> from waeup.university.course import Course
+        >>> mycourse = Course(code='COURSE1')
+        >>> mycontainer.addCourse(mycourse)
+        >>> list(mycontainer.keys())
+        [u'COURSE1']
+
+   .. method:: clear()
+
+      Remove all contained courses:
+
+        >>> len(mycontainer.items())
+        1
+
+        >>> mycontainer.clear()
+        >>> len(mycontainer.items())
+        0
+
+Utilities
+=========
+
+:class:`CourseContainerFactory`
+-------------------------------
+
+.. class:: CourseContainerFactory()
+
+   .. attribute:: grok.name(u'waeup.CourseContainer')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`CourseContainer`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycoursecontainer = createObject(u'waeup.CourseContainer')
+     >>> mycoursecontainer
+     <waeup.university.coursecontainer.CourseContainer object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.university.coursecontainer import CourseContainerFactory
+     >>> verifyClass(IFactory, CourseContainerFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> coursecontainer_factory = CourseContainerFactory()
+     >>> coursecontainer_factory.getInterfaces()
+     <implementedBy waeup.university.coursecontainer.CourseContainer>
+
+
+Examples
+========
+
+Getting a course container
+--------------------------
+
+We can easily create `CourseContainers`:
+
+    >>> from waeup.university.coursecontainer import CourseContainer
+    >>> mycontainer = CourseContainer()
+
+
+Another way to get a course container -- without importing the class
+-- is via factories. We registered a factory for course containers
+under the name ``waeup.CourseContainer``. Now we can ask for an object
+by calling the appropriate factory:
+
+    >>> from zope.component import createObject
+    >>> createObject(u'waeup.CourseContainer')
+    <waeup.university.coursecontainer.CourseContainer object at 0x...>
+
+This way we get a thing that implements ICourseContainer without
+imports or similar.
+
Index: waeup/trunk/src/waeup/university/courserefs.txt
===================================================================
--- waeup/trunk/src/waeup/university/courserefs.txt	(revision 4789)
+++ waeup/trunk/src/waeup/university/courserefs.txt	(revision 4789)
@@ -0,0 +1,118 @@
+Course references
+*****************
+
+Courses are referenced by certificate courses.
+
+:Test-Layer: functional
+
+We can be sure, that when removing courses or references to them,
+other data will be handled in a way we expect. That is, 
+
+* when we remove a course, we want all references to it be removed as
+  well
+
+* when we remove a reference to a course, nothing else should be
+  touched but only this very reference.
+
+
+As correct deletion of references needs support of catalogs (we lookup
+catalogs to find references), we need a fully blown ZODB here,
+including an instance of :class:`waeup.app.University`, as this is our
+`ISite` object where the catalogs are stored.
+
+We create a complete university tree, starting with a
+:class:`University` instance and going down to courses, certificats
+and course certificates:
+
+    >>> from waeup.app import University
+    >>> root = getRootFolder()
+    >>> root['app'] = University()
+
+We have to set the site, to enable proper working of catalogs and
+`IObjectRemoved` handlers:
+
+    >>> from zope.app.component.hooks import setSite
+    >>> setSite(root['app'])
+
+    >>> from zope.component import createObject
+    >>> faculty = createObject('waeup.Faculty')
+    >>> faculty.code = 'FAC'
+    >>> root['app']['faculties'].addFaculty(faculty)
+    >>> fac = root['app']['faculties']['FAC']
+
+    >>> dept = createObject('waeup.Department')
+    >>> dept.code = 'DEPT'
+    >>> root['app']['faculties']['FAC'].addDepartment(dept)
+
+    >>> course = createObject('waeup.Course')
+    >>> course.code = 'CRS1'
+    >>> fac['DEPT'].courses.addCourse(course)
+
+Now let's add another department, which will also hold a course:
+
+    >>> dept2 = createObject('waeup.Department')
+    >>> dept2.code = 'DEPT2'
+    >>> root['app']['faculties']['FAC'].addDepartment(dept2)
+
+    >>> course = createObject('waeup.Course')
+    >>> course.code = 'CRS2'
+    >>> fac['DEPT2'].courses.addCourse(course)
+
+Finally, we add a certificate with references (certificate courses) to
+the created courses:
+
+    >>> cert = createObject('waeup.Certificate')
+    >>> cert.code = 'CERT'
+    >>> fac['DEPT'].certificates.addCertificate(cert)
+
+    >>> fac['DEPT'].certificates['CERT'].addCourseRef(
+    ...   fac['DEPT'].courses['CRS1'])
+
+    >>> fac['DEPT'].certificates['CERT'].addCourseRef(
+    ...   fac['DEPT2'].courses['CRS2'])
+
+We add the latter course again, but this time for the second level:
+
+    >>> fac['DEPT'].certificates['CERT'].addCourseRef(
+    ...   fac['DEPT2'].courses['CRS2'], level=200)
+
+So, we have three references in our certificate to two different
+courses stored somewhere in the application:
+
+    >>> list(fac['DEPT'].certificates['CERT'])
+    [u'CRS1_100', u'CRS2_100', u'CRS2_200']
+
+Both, the courses themselves and the references to them, are the same
+(not only equal):
+
+    >>> a = root['app']['faculties']['FAC']['DEPT']
+    >>> b1 = a.courses['CRS1']
+    >>> b2 = a.certificates['CERT']['CRS1_100'].course
+    >>> b1 is b2
+    True
+
+If we remove the course, also the reference to it in the cert course
+will be gone:
+
+    >>> list(a.certificates['CERT'])
+    [u'CRS1_100', u'CRS2_100', u'CRS2_200']
+
+    >>> list(a.courses)
+    [u'CRS1']
+
+    >>> del root['app']['faculties']['FAC']['DEPT'].courses['CRS1']
+    >>> list(a.certificates['CERT'])
+    [u'CRS2_100', u'CRS2_200']
+
+    >>> list(a.courses)
+    []
+
+If we remove a reference, this will not touch the referenced course
+nor other references:
+
+    >>> del fac['DEPT'].certificates['CERT']['CRS2_200']
+    >>> list(a.certificates['CERT'])
+    [u'CRS2_100']
+
+    >>> list(fac['DEPT2'].courses)
+    [u'CRS2']
Index: waeup/trunk/src/waeup/university/department.py
===================================================================
--- waeup/trunk/src/waeup/university/department.py	(revision 3551)
+++ waeup/trunk/src/waeup/university/department.py	(revision 4789)
@@ -0,0 +1,57 @@
+"""University departments.
+"""
+import grok
+from zope.component import createObject
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.interfaces import IDepartment
+
+class Department(grok.Container):
+    """A university department.
+    """
+    grok.implements(IDepartment)
+
+    # A simple counter for ids.
+    next_id = 0L
+    
+    def __init__(self,
+                 title=u'Unnamed Department',
+                 title_prefix=u'department',
+                 code=u"NA", **kw):
+        super(Department, self).__init__(**kw)
+        self.title = title
+        self.title_prefix = title_prefix
+        self.code = code
+        self.courses = createObject(u'waeup.CourseContainer')
+        self.courses.__parent__ = self
+        self.courses.__name__ = 'courses'
+        self.certificates = createObject(u'waeup.CertificateContainer')
+        self.certificates.__parent__ = self
+        self.certificates.__name__ = 'certificates'
+
+    def traverse(self, name):
+        """Deliver appropriate containers, if someone wants to go to courses
+        or departments.
+        """
+        if name == 'courses':
+            return self.courses
+        elif name == 'certificates':
+            return self.certificates
+        return None
+        
+
+class DepartmentFactory(grok.GlobalUtility):
+    """A factory for department containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Department')
+    title = u"Create a new department.",
+    description = u"This factory instantiates new department instances."
+
+    def __call__(self, *args, **kw):
+        return Department(*args, **kw)
+
+    def getInterfaces(self):
+        """Get interfaces of objects provided by this factory.
+        """
+        return implementedBy(Department)
Index: waeup/trunk/src/waeup/university/department.txt
===================================================================
--- waeup/trunk/src/waeup/university/department.txt	(revision 4789)
+++ waeup/trunk/src/waeup/university/department.txt	(revision 4789)
@@ -0,0 +1,141 @@
+:mod:`waeup.university.department` -- Departments
+*************************************************
+
+.. module:: waeup.university.department
+
+Components that represent university departments.
+
+:Test-Layer: unit
+
+Before we can create departments, we have to grok the :mod:`waeup`
+package. This happens automatically in real-world use:
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+
+Content Classes (models and containers)
+=======================================
+
+
+:class:`Department`
+-------------------
+
+.. class:: Department(title=u'Unnamed Department'[, title_prefix=u'department' [, code=u"NA"]])
+
+   Create a representation of a university department:
+
+     >>> from waeup.university.department import Department
+     >>> mydept = Department()
+     >>> mydept
+     <waeup.university.department.Department object at 0x...>
+
+   Another way to create :class:`Department` instances is by asking
+   for a factory called ``waeup.Department``. This way we can create a
+   department without importing a class:
+
+     >>> from zope.component import createObject
+     >>> mydept = createObject(u'waeup.Department')
+     >>> mydept
+     <waeup.university.department.Department object at 0x...>
+
+   :class:`Department` instances have the attributes required by the
+   `IDepartment` interface:
+
+     >>> from waeup.interfaces import IDepartment
+     >>> IDepartment.providedBy(mydept)
+     True
+
+     >>> from zope.interface.verify import verifyObject
+     >>> verifyObject(IDepartment, mydept)
+     True
+
+
+   .. attribute:: title
+
+      (string) The title of a department.
+
+      Each department has a title:
+
+        >>> mydept.title
+        u'Unnamed Department'
+
+
+   .. attribute:: title_prefix
+
+      (string) The prefix of a department.
+
+      Each department has a title prefix, which specifies the kind of
+      institution:
+
+        >>> mydept.title_prefix
+        u'department'
+
+
+   .. attribute:: code
+
+      (string) An internally used unique code string.
+
+      Each department holds a code, which might be a shortcut or
+      abbreviation of the real department name. By default the code is
+      ``NA`` (=not assigned):
+
+        >>> mydept.code
+        u'NA'
+
+
+   .. attribute:: courses
+
+      (ICourseContainer instance) A container for courses.
+
+      Each department has a courses container:
+
+        >>> mydept.courses
+        <waeup.university.coursecontainer.CourseContainer object at 0x...>
+
+
+   .. attribute:: certificates
+
+      (ICertificateContainer instance) A container for certificates.
+
+      Each department has a certificate container, that holds the
+      descrtiptions of certificates the department has:
+
+        >>> mydept.certificates
+        <waeup...certificatecontainer.CertificateContainer object at 0x...>
+
+Utilities
+=========
+
+:class:`DepartmentFactory`
+--------------------------
+
+.. class:: DepartmentFactory()
+
+   .. attribute:: grok.name(u'waeup.Department')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`Department`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mydepartment = createObject(u'waeup.Department')
+     >>> mydepartment
+     <waeup.university.department.Department object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.university.department import DepartmentFactory
+     >>> verifyClass(IFactory, DepartmentFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> department_factory = DepartmentFactory()
+     >>> department_factory.getInterfaces()
+     <implementedBy waeup.university.department.Department>
Index: waeup/trunk/src/waeup/university/faculty.py
===================================================================
--- waeup/trunk/src/waeup/university/faculty.py	(revision 3551)
+++ waeup/trunk/src/waeup/university/faculty.py	(revision 4789)
@@ -1,15 +1,59 @@
+"""Faculty components.
+"""
+
 import grok
-from waeup.basecontainer import BaseContainer
-from interfaces import IFaculty
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.interfaces import IFaculty, IDepartment
 
-class Faculty(BaseContainer):
+class Faculty(grok.Container):
+    """A university faculty.
+    """
     grok.implements(IFaculty)
 
-    def __init__(self, name=u'Unnamed Faculty', **kw):
+    def __init__(self,
+                 title=u'Unnamed Faculty',
+                 title_prefix=u'faculty',
+                 code=u'NA', **kw):
         super(Faculty, self).__init__(**kw)
-        self.name = name
+        self.title = title
+        self.title_prefix = title_prefix
+        self.code = code
 
-class Index(grok.DisplayForm):
-    form_fields = grok.AutoFields(IFaculty)
+    def addDepartment(self, department):
+        """Add a department to the faculty.
+        """
+        if not IDepartment.providedBy(department):
+            raise TypeError('Faculties contain only IDepartment instances')
+        self[department.code] = department
 
+    def clear(self):
+        """Remove all departments from faculty.
+        """
+        keys = self.keys()
+        for key in keys:
+            del self[key]
 
+    def getName(self, key):
+        """Get complete faculty name.
+        """
+        if key in self.keys():
+            dept = self[key]
+            prefix = dept.title_prefix
+            prefix = prefix[0].upper() + prefix[1:]
+            return '%s of %s' % (prefix, dept.title)
+
+        
+class FacultyFactory(grok.GlobalUtility):
+    """A factory for faculty containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Faculty')
+    title = u"Create a new faculty.",
+    description = u"This factory instantiates new faculty instances."
+
+    def __call__(self, *args, **kw):
+        return Faculty()
+
+    def getInterfaces(self):
+        return implementedBy(Faculty)
Index: waeup/trunk/src/waeup/university/faculty.txt
===================================================================
--- waeup/trunk/src/waeup/university/faculty.txt	(revision 4789)
+++ waeup/trunk/src/waeup/university/faculty.txt	(revision 4789)
@@ -0,0 +1,211 @@
+:mod:`waeup.university.faculty` -- Faculties
+********************************************
+
+.. module:: waeup.university.faculty
+
+Components that represent university faculties.
+
+:Test-Layer: unit
+
+Before we can create faculties, we have to grok the :mod:`waeup`
+package. This happens automatically in real-world use:
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+Content Classes (models and containers)
+=======================================
+
+
+:class:`Faculty`
+----------------
+
+.. class:: Faculty(title=u'Unnamed Faculty'[, title_prefix=u'faculty' [, code=u"NA"]])
+
+   Create a representation of a university faculty:
+
+     >>> from waeup.university.faculty import Faculty
+     >>> myfac = Faculty()
+     >>> myfac
+     <waeup.university.faculty.Faculty object at 0x...>
+
+   Another way to create :class:`Faculty` instances is by asking
+   for a factory called ``waeup.Faculty``. This way we can create a
+   faculty without importing a class:
+
+     >>> from zope.component import createObject
+     >>> myfac = createObject(u'waeup.Faculty')
+     >>> myfac
+     <waeup.university.faculty.Faculty object at 0x...>
+
+   :class:`Faculty` instances have the attributes required by the
+   `IFaculty` interface:
+
+     >>> from waeup.interfaces import IFaculty
+     >>> IFaculty.providedBy(myfac)
+     True
+
+     >>> from zope.interface.verify import verifyObject
+     >>> verifyObject(IFaculty, myfac)
+     True
+
+   .. method:: addDepartment(department)
+
+      Add a department into this faculty. Faculties are containers for
+      departments. Added objects are checked and only `IDepartment`
+      objects accepted:
+
+        >>> myfac.addDepartment(object())
+        Traceback (most recent call last):
+        ...
+        TypeError: Faculties contain only IDepartment instances
+
+        >>> list(myfac.values())
+        []
+
+      Regular departments are accepted:
+
+        >>> from waeup.university.department import Department
+        >>> myfac.addDepartment(Department(title='Physics',
+        ...                                code='DP'))
+
+        >>> list(myfac.items())
+        [(u'DP', <waeup.university.department.Department object at 0x...>)]
+
+   .. method:: clear()
+
+      Remove all departments from this faculty:
+
+        >>> myfac.clear()
+        >>> list(myfac.items())
+        []
+
+   .. attribute:: title
+
+      (string) The title of a faculty.
+
+      Each faculty has a title:
+
+        >>> myfac.title
+        u'Unnamed Faculty'
+
+
+   .. attribute:: title_prefix
+
+      (string) The prefix of a faculty.
+
+      Each faculty has a title prefix, which specifies the kind of
+      institution:
+
+        >>> myfac.title_prefix
+        u'faculty'
+
+
+   .. attribute:: code
+
+      (string) An internally used unique code string.
+
+      Each faculty holds a code, which might be a shortcut or
+      abbreviation of the real faculty name. By default the code is
+      ``NA`` (=not assigned):
+
+        >>> myfac.code
+        u'NA'
+
+
+Utilities
+=========
+
+:class:`FacultyFactory`
+--------------------------
+
+.. class:: FacultyFactory()
+
+   .. attribute:: grok.name(u'waeup.Faculty')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`Faculty`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> myfaculty = createObject(u'waeup.Faculty')
+     >>> myfaculty
+     <waeup.university.faculty.Faculty object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.university.faculty import FacultyFactory
+     >>> verifyClass(IFactory, FacultyFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> faculty_factory = FacultyFactory()
+     >>> faculty_factory.getInterfaces()
+     <implementedBy waeup.university.faculty.Faculty>
+
+
+Examples:
+=========
+
+We can create faculties:
+
+    >>> from waeup.university.faculty import Faculty
+    >>> myfaculty = Faculty()
+    >>> myfaculty
+    <waeup.university.faculty.Faculty object at 0x...>
+
+Another way to create faculties is by asking for a factory called
+``waeup.Faculty``. Now we can create a factory without importing a
+class:
+
+    >>> from zope.component import createObject
+    >>> myfaculty = createObject(u'waeup.Faculty')
+    >>> myfaculty
+    <waeup.university.faculty.Faculty object at 0x...>
+
+Faculty attributes
+==================
+
+Faculties have the attributes required by the `IFaculty` interface:
+
+    >>> from waeup.interfaces import IFaculty
+    >>> IFaculty.providedBy(myfaculty)
+    True
+
+    >>> from zope.interface.verify import verifyObject
+    >>> verifyObject(IFaculty, myfaculty)
+    True
+
+Each of the following attributes is mandatory.
+
+`title`
+-------
+
+Each faculty has a title:
+
+    >>> myfaculty.title
+    u'Unnamed Faculty'
+
+`title_prefix`
+--------------
+
+Each faculty has a title prefix, which specifies the kind of
+institution:
+
+    >>> myfaculty.title_prefix
+    u'faculty'
+
+`code`
+------
+
+Each faculty holds a code, which might be a shortcut or abbreviation
+of the real faculty name. By default the code is ``NA`` (=not assigned):
+
+    >>> myfaculty.code
+    u'NA'
Index: waeup/trunk/src/waeup/university/facultycontainer.py
===================================================================
--- waeup/trunk/src/waeup/university/facultycontainer.py	(revision 3551)
+++ waeup/trunk/src/waeup/university/facultycontainer.py	(revision 4789)
@@ -1,9 +1,42 @@
 import grok
-from waeup.basecontainer import BaseContainer
-from interfaces import IFaculty
-from faculty import Faculty
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.interfaces import IFacultyContainer, IFaculty
 
-class FacultyContainer(BaseContainer):
-    
-    name = u'Faculties'
-    childClass = Faculty
+class FacultyContainer(grok.Container):
+    """See interfaces for description.
+    """
+    grok.implements(IFacultyContainer)
+    grok.require('waeup.manageUniversity')
+
+    def addFaculty(self, faculty):
+        if not IFaculty.providedBy(faculty):
+            raise TypeError('FacultyContainers contain only IFaculty instances')
+        self[faculty.code] = faculty
+        return
+
+    def clear(self):
+        keys = self.keys()
+        for key in keys:
+            del self[key]
+
+    def getName(self, key):
+        if key in self.keys():
+            fac = self[key]
+            prefix = fac.title_prefix
+            prefix = prefix[0].upper() + prefix[1:]
+            return '%s of %s' % (prefix, fac.title)
+            
+class FacultyContainerFactory(grok.GlobalUtility):
+    """A factory for faculty containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.FacultyContainer')
+    title = u"Create a new faculty container.",
+    description = u"This factory instantiates new faculty containers."
+
+    def __call__(self):
+        return FacultyContainer()
+
+    def getInterfaces(self):
+        return implementedBy(FacultyContainer)
Index: waeup/trunk/src/waeup/university/facultycontainer.txt
===================================================================
--- waeup/trunk/src/waeup/university/facultycontainer.txt	(revision 4789)
+++ waeup/trunk/src/waeup/university/facultycontainer.txt	(revision 4789)
@@ -0,0 +1,70 @@
+FacultyContainer
+****************
+
+Containers for faculties.
+
+:Test-Layer: unit
+
+Getting a faculty container
+===========================
+
+We can easily create `FacultyContainers`::
+
+    >>> from waeup.university.facultycontainer import FacultyContainer
+    >>> mycontainer = FacultyContainer()
+
+Faculty containers provide `IFacultyContainer`:
+
+    >>> from waeup.interfaces import IFacultyContainer
+    >>> IFacultyContainer.providedBy(mycontainer)
+    True
+
+Another way to get a faculty container -- without importing the class
+-- is via factories. We registered a factory for faculty containers
+under the name ``waeup.facultycontainer``::
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+Now we can ask for an object by calling the appropriate factory:
+
+    >>> from zope.component import createObject
+    >>> createObject(u'waeup.FacultyContainer')
+    <waeup.university.facultycontainer.FacultyContainer object at 0x...>
+
+This way we get a thing that implements IFacultyContainer without
+imports or similar.
+
+We can be sure, that the full interface is supported by the
+FacultyContainer class::
+
+    >>> from zope.interface.verify import verifyClass
+    >>> verifyClass(IFacultyContainer, FacultyContainer)
+    True
+
+
+Storing things in faculty containers
+====================================
+
+We can, of course, store things in a faculty container. But when we
+really store an object, then it must be a faculty::
+
+    >>> mycontainer.addFaculty(42)
+    Traceback (most recent call last):
+    ...
+    TypeError: FacultyContainers contain only IFaculty instances
+
+Okay, so we have to get a faculty first::
+
+    >>> from waeup.university.faculty import Faculty
+    >>> myfaculty = Faculty()
+
+We can add this faculty to our container::
+
+    >>> mycontainer.addFaculty(myfaculty)
+
+We get back the key, under which the faculty was stored. It will be
+some string, but there is no guarantee at all, how this key looks
+like. It might be a string of integers, a name or whatever; you cannot
+know before.
+
Index: waeup/trunk/src/waeup/university/interfaces.py
===================================================================
--- waeup/trunk/src/waeup/university/interfaces.py	(revision 3551)
+++ 	(revision )
@@ -1,11 +1,0 @@
-from zope.interface import Interface
-from zope import schema
-
-class IFaculty(Interface):
-    """Representation of a university faculty.
-    """
-    name = schema.TextLine(
-        title = u'Name of Faculty',
-        default = u'Unnamed',
-        required = True,
-        )
Index: waeup/trunk/src/waeup/users.py
===================================================================
--- waeup/trunk/src/waeup/users.py	(revision 4789)
+++ waeup/trunk/src/waeup/users.py	(revision 4789)
@@ -0,0 +1,35 @@
+"""Users (principals) for the WAeUP portal.
+"""
+import grok
+from waeup.authentication import Account
+from waeup.interfaces import IUserContainer
+
+class UserContainer(grok.Container):
+    """A container for principals.
+
+    See interfaces.py and users.txt for extensive description.
+    """
+    grok.implements(IUserContainer)
+    grok.require('waeup.manageUsers')
+
+    def addUser(self, name, password, title=None, description=None, roles=[]):
+        """Add a new Account instance, created from parameters.
+        """
+        if title is None:
+            title = name
+        if description is None:
+            description = title
+        self[name] = Account(name, password, title, description, roles)
+
+    def addAccount(self, account):
+        """Add the account passed.
+        """
+        self[account.name] = account
+        
+    def delUser(self, name):
+        """Delete user, if an account with the given name exists.
+
+        Do not complain, if the name does not exist.
+        """
+        if name in self.keys():
+            del self[name]
Index: waeup/trunk/src/waeup/users.txt
===================================================================
--- waeup/trunk/src/waeup/users.txt	(revision 4789)
+++ waeup/trunk/src/waeup/users.txt	(revision 4789)
@@ -0,0 +1,67 @@
+User for the WAeUP portal
+*************************
+
+:Test-Layer: unit
+
+Before we can start, we need some password managers available:
+
+    >>> from zope.app.authentication.placelesssetup import (
+    ...   PlacelessSetup)
+    >>> PlacelessSetup().setUp()
+
+User Containers
+===============
+
+We can create a user container which will hold the useraccounts for
+us:
+
+    >>> from waeup.users import UserContainer
+    >>> myusers = UserContainer()
+
+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.authentication.Account object at 0x...>
+
+    >>> bob.name
+    'bob'
+
+    >>> bob.title
+    'bob'
+
+    >>> bob.description
+    'bob'
+
+As we did not give a title nor description, the name was taken instead
+for these values. The password, however, is stored encoded:
+
+    >>> bob.password
+    '...15aca8166'
+
+Remark: that we can tell the last chars of the 'encrypted' password,
+means, that the encryption is broken or at least waeker as it should
+be.
+
+XXX: We could provide a stronger (correct) SHA encryption.
+
+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)
+    []
Index: waeup/trunk/src/waeup/utils/api.txt
===================================================================
--- waeup/trunk/src/waeup/utils/api.txt	(revision 4789)
+++ waeup/trunk/src/waeup/utils/api.txt	(revision 4789)
@@ -0,0 +1,18 @@
+:mod:`waeup.utils` -- misc. helpers
+***********************************
+
+.. module:: waeup.utils
+
+Components and other stuff of general interest.
+
+Submodules
+==========
+
+The :mod:`waeup.utils` module provides the following submodules:
+
+.. toctree::
+   :maxdepth: 1
+
+   helpers.txt
+   importexport.txt
+   csvimport.txt
Index: waeup/trunk/src/waeup/utils/csvimport.py
===================================================================
--- waeup/trunk/src/waeup/utils/csvimport.py	(revision 4789)
+++ waeup/trunk/src/waeup/utils/csvimport.py	(revision 4789)
@@ -0,0 +1,246 @@
+"""CSV importers.
+"""
+import grok
+from waeup.csvfile import CSVFile, toBool
+from waeup.csvfile.interfaces import ICSVFile
+from waeup.interfaces import IFacultyContainer, IWAeUPCSVImporter
+from waeup.utils.importexport import CSVImporter
+from zope.app.catalog.interfaces import ICatalog
+from zope.component import createObject, getUtility
+
+#
+# CSV import stuff
+#
+class IFacultyCSVFile(ICSVFile):
+    """A CSV file that contains faculty data.
+    """
+
+class FacultyCSVFile(CSVFile):
+    """An abstraction of a CSV file containing faculties.
+    """
+    grok.implements(IFacultyCSVFile)
+    grok.provides(IFacultyCSVFile)
+    required_fields = ['code', 'title', 'title_prefix']
+
+class FacultyCSVImporter(CSVImporter):
+    """Shuffle data from faculty CSV files into faculty containers.
+    """
+    # Tell, what kinds of objects we connect...
+    grok.adapts(IFacultyCSVFile, IFacultyContainer)
+    # Tell the world, that we are an importer...
+    grok.implements(IWAeUPCSVImporter)
+    grok.provides(IWAeUPCSVImporter)
+
+    datatype = u'Faculty Importer'
+
+    def doImport(self, clear_old_data=True, overwrite=True):
+        # CSVImporter instances have a `csvfile` and a `receiver`
+        # object defined which refer to the CSV file and the container.
+        for row in self.csvfile.getData():
+            new_item = createObject(u'waeup.Faculty')
+            for key, val in row.items():
+                setattr(new_item, key, val)
+            self.receiver.addFaculty(new_item)
+        return
+
+class IDepartmentCSVFile(ICSVFile):
+    """A CSV file that contains department data.
+    """
+
+class DepartmentCSVFile(CSVFile):
+    """An abstraction of a CSV file containing departments.
+    """
+    grok.implements(IDepartmentCSVFile)
+    grok.provides(IDepartmentCSVFile)
+    required_fields = ['code', 'title', 'title_prefix', 'faculty_code']
+
+class DepartmentCSVImporter(CSVImporter):
+    """Shuffle data from department CSV files into faculty containers.
+    """
+    # Tell, what kinds of objects we connect...
+    grok.adapts(IDepartmentCSVFile, IFacultyContainer)
+    # Tell the world, that we are an importer...
+    grok.implements(IWAeUPCSVImporter)
+    grok.provides(IWAeUPCSVImporter)
+
+    datatype = u'Department Importer'
+
+    def doImport(self, clear_old_data=True, overwrite=True):
+        # CSVImporter instances have a `csvfile` and a `receiver`
+        # object defined which refer to the CSV file and the container.
+        for row in self.csvfile.getData():
+            new_item = createObject(u'waeup.Department')
+            faculty_code = row['faculty_code']
+            faculty = self.receiver[faculty_code]
+            del row['faculty_code']
+            for key, val in row.items():
+                setattr(new_item, key, val)
+            faculty.addDepartment(new_item)
+        return
+
+class ICourseCSVFile(ICSVFile):
+    """A CSV file that contains course data.
+    """
+
+class CourseCSVFile(CSVFile):
+    """An abstraction of a CSV file containing courses.
+    """
+    grok.implements(ICourseCSVFile)
+    grok.provides(ICourseCSVFile)
+    required_fields = ['code', 'title', 'level', 'passmark', 'credits',
+                       'semester', 'faculty', 'department']
+
+class CourseCSVImporter(CSVImporter):
+    """Shuffle data from course CSV files into faculty containers.
+    """
+    # Tell, what kinds of objects we connect...
+    grok.adapts(ICourseCSVFile, IFacultyContainer)
+    # Tell the world, that we are an importer...
+    grok.implements(IWAeUPCSVImporter)
+    grok.provides(IWAeUPCSVImporter)
+
+    datatype = u'Course Importer'
+
+    def doImport(self, clear_old_data=True, overwrite=True):
+        # CSVImporter instances have a `csvfile` and a `receiver`
+        # object defined which refer to the CSV file and the container.
+        for row in self.csvfile.getData():
+
+            new_item = createObject(u'waeup.Course')
+            
+            faculty_code = row['faculty']
+            faculty = self.receiver[faculty_code]
+            del row['faculty']
+
+            dept_code = row['department']
+            dept = faculty[dept_code]
+            del row['department']
+
+            # Type casts...
+            if row['semester'] == '' or row['semester'] == 'None':
+                    row['semester'] = 0
+            if row['semester'] == '1,2':
+                row['semester'] = 3
+            for key in ['semester', 'credits', 'passmark']:
+                if row[key] == '' or row[key] == 'None':
+                    row[key] = None
+                    continue
+                row[key] = int(row[key])
+
+            for key, val in row.items():
+                setattr(new_item, key, val)
+            dept.courses.addCourse(new_item)
+        return
+
+class ICertificateCSVFile(ICSVFile):
+    """A CSV file that contains certificate data.
+    """
+
+class CertificateCSVFile(CSVFile):
+    """An abstraction of a CSV file containing certificates.
+    """
+    grok.implements(ICertificateCSVFile)
+    grok.provides(ICertificateCSVFile)
+    required_fields = ['code', 'title', 'faculty_code', 'department_code',
+                       'category', 'study_mode', 'start_level', 'end_level',
+                       'm_prefix', 'max_pass', 'application_category']
+
+class CertificateCSVImporter(CSVImporter):
+    """Shuffle data from certificate CSV files into faculty containers.
+    """
+    # Tell, what kinds of objects we connect...
+    grok.adapts(ICertificateCSVFile, IFacultyContainer)
+    # Tell the world, that we are an importer...
+    grok.implements(IWAeUPCSVImporter)
+    grok.provides(IWAeUPCSVImporter)
+
+    datatype = u'Certificate Importer'
+
+    def doImport(self, clear_old_data=True, overwrite=True):
+        # CSVImporter instances have a `csvfile` and a `receiver`
+        # object defined which refer to the CSV file and the container.
+        for row in self.csvfile.getData():
+
+            new_item = createObject(u'waeup.Certificate')
+            
+            faculty_code = row['faculty_code']
+            faculty = self.receiver[faculty_code]
+            del row['faculty_code']
+
+            dept_code = row['department_code']
+            dept = faculty[dept_code]
+            del row['department_code']
+
+            for key, val in row.items():
+                setattr(new_item, key, val)
+            dept.certificates.addCertificate(new_item)
+        return
+
+class ICertificateCourseCSVFile(ICSVFile):
+    """A CSV file that contains certificate-course data.
+    """
+
+class CertificateCourseCSVFile(CSVFile):
+    """An abstraction of a CSV file containing certificate-courses.
+    """
+    grok.implements(ICertificateCourseCSVFile)
+    grok.provides(ICertificateCourseCSVFile)
+    required_fields = ['code', 'faculty_code', 'department_code',
+                       'certificate_code', 'level', 'core_or_elective']
+
+class CertificateCourseCSVImporter(CSVImporter):
+    """Shuffle data from certificate CSV files into faculty containers.
+    """
+    # Tell, what kinds of objects we connect...
+    grok.adapts(ICertificateCourseCSVFile, IFacultyContainer)
+    # Tell the world, that we are an importer...
+    grok.implements(IWAeUPCSVImporter)
+    grok.provides(IWAeUPCSVImporter)
+
+    datatype = u'Certificate Course Importer'
+
+    def doImport(self, clear_old_data=True, overwrite=True):
+        # CSVImporter instances have a `csvfile` and a `receiver`
+        # object defined which refer to the CSV file and the container.
+        for row in self.csvfile.getData():
+            new_item = createObject(u'waeup.CertificateCourse')
+            code = row['code']
+            del row['code']
+            
+            faculty_code = row['faculty_code']
+            faculty = self.receiver[faculty_code]
+            del row['faculty_code']
+
+            dept_code = row['department_code']
+            dept = faculty[dept_code]
+            del row['department_code']
+
+            cert_code = row['certificate_code']
+            cert = dept.certificates[cert_code]
+            del row['certificate_code']
+
+            course = self.getCourse(code)
+            
+            # Type casts...
+            row['core_or_elective'] = toBool(row['core_or_elective'])
+            row['level'] = int(row['level'])
+            
+            cert.addCourseRef(course, **row)
+        return
+
+    def getCourse(self, code):
+        """Get the course with code `code`.
+
+        Courses can be anywhere in the site. Therefore we ask the
+        catalog.
+        """
+        cat = getUtility(ICatalog, name='courses_catalog')
+        result = cat.searchResults(code=(code, code))
+        if len(result) < 1:
+            raise KeyError('No such course found: %s' % code)
+
+        # We might get more than one result, but this should not
+        # happen, as course codes are supposed to be unique.
+        return list(result)[0]
+
+        
Index: waeup/trunk/src/waeup/utils/csvimport.txt
===================================================================
--- waeup/trunk/src/waeup/utils/csvimport.txt	(revision 4789)
+++ waeup/trunk/src/waeup/utils/csvimport.txt	(revision 4789)
@@ -0,0 +1,79 @@
+:mod:`waeup.utils.csvimport` -- CSV file importers
+**************************************************
+
+.. module:: waeup.utils.csvimport
+
+Components for importing CSV files.
+
+:Test-Layer: unit
+
+Because CSV importers make use of components registered with the Zope
+Component Architecture (ZCA), we first have to grok the `waeup`
+package. This happens automatically in real-world use:
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+Importing from CSV files
+------------------------
+
+We create a facultycontainer, which we want to populate with some CSV
+data afterwards:
+
+    >>> from waeup.university.facultycontainer import FacultyContainer
+    >>> mycontainer = FacultyContainer()
+
+Next, we create a CSV file we want to import:
+
+    >>> open('myfaculties.csv', 'wb').write(
+    ... """code,review_state,title,title_prefix
+    ... AGR,static,Agriculture,faculty
+    ... ART,static,Arts,faculty
+    ... DEN,static,Dentistry,faculty
+    ... """)
+
+We get a suitable importer by asking for a multi-adapter to
+our file and IWAeUPCSVExporter (see section above, the importexport
+module and the `waeup.csvfile` README for details):
+
+    >>> from waeup.csvfile import getCSVFile
+    >>> from waeup.interfaces import IWAeUPCSVImporter
+    >>> from zope.component import getMultiAdapter
+    >>> importer = getMultiAdapter((getCSVFile('myfaculties.csv'), 
+    ...                             mycontainer),
+    ...                            IWAeUPCSVImporter)
+    >>> importer
+    <waeup.utils.csvimport.FacultyCSVImporter object at 0x...>
+
+This importer complies with the IWAeUPCSVImporter interface:
+
+    >>> from waeup.utils.csvimport import FacultyCSVImporter
+    >>> IWAeUPCSVImporter.implementedBy(FacultyCSVImporter)
+    True
+
+    >>> from zope.interface.verify import verifyClass
+    >>> verifyClass(IWAeUPCSVImporter, FacultyCSVImporter)
+    True
+
+The container is still empty:
+
+    >>> len(mycontainer)
+    0
+
+Now we can easily import the data:
+
+    >>> importer.doImport()
+
+The container is now filled:
+
+    >>> len(mycontainer)
+    3
+
+    >>> print [(x.code, x.title) for x in list(mycontainer.values())]
+    [('AGR', 'Agriculture'), ('ART', 'Arts'), ('DEN', 'Dentistry')]
+
+
+Clean up:
+
+    >>> import os
+    >>> os.unlink('myfaculties.csv')
Index: waeup/trunk/src/waeup/utils/helpers.py
===================================================================
--- waeup/trunk/src/waeup/utils/helpers.py	(revision 4789)
+++ waeup/trunk/src/waeup/utils/helpers.py	(revision 4789)
@@ -0,0 +1,97 @@
+"""General helper functions for WAeUP.
+"""
+import os
+import re
+import sys
+import shutil
+
+def removeFileOrDirectory(filepath):
+    """Remove a file or directory.
+    """
+    filepath = os.path.abspath(filepath)
+    if not os.path.exists(filepath):
+        return
+    if os.path.isdir(filepath):
+        shutil.rmtree(filepath)
+    else:
+        os.unlink(filepath)
+    return
+
+def copyFileSystemTree(src, dst, overwrite=False, del_old=False):
+    """Copy contents of directory src to directory dst.
+
+    Both directories must exists.
+
+    If `overwrite` is true, any same named objects will be
+    overwritten. Otherwise these files will not be touched.
+
+    If `del_old` is true, copied files and directories will be removed
+    from the src directory.
+
+    This functions returns a list of non-copied files.
+
+    Unix hidden files and directories (starting with '.') are not
+    processed by this function.
+    """
+    if not os.path.exists(src):
+        raise ValueError('source path does not exist: %s' % src)
+    if not os.path.exists(dst):
+        raise ValueError('destination path does not exist: %s' % dst)
+    if not os.path.isdir(src):
+        raise ValueError('source path is not a directory: %s' % src)
+    if not os.path.isdir(dst):
+        raise ValueError('destination path is not a directory: %s' % dst)
+    not_copied = []
+    for item in os.listdir(src):
+        if item.startswith('.'):
+            continue # We do not copy hidden stuff...
+        itemsrc = os.path.join(src, item)
+        itemdst = os.path.join(dst, item)
+
+        if os.path.exists(itemdst):
+            if overwrite is True:
+                removeFileOrDirectory(itemdst)
+            else:
+                not_copied.append(item)
+                continue
+        
+        if os.path.isdir(itemsrc):
+            shutil.copytree(itemsrc, itemdst)
+        else:
+            shutil.copy2(itemsrc, itemdst)
+        if del_old:
+            removeFileOrDirectory(itemsrc)
+    return not_copied
+
+
+def getInnerHTMLPart(html_code):
+    """Return the 'inner' part of a complete HTML snippet.
+
+    If there is a form part, get this.
+
+    If there is no form part, try to return the body part contents.
+
+    If there is no body, return as-is.
+    """
+
+    try:
+        result = re.match('^.+(<forms[^\>]*>.*</form>).+$', html_code,
+                          re.DOTALL).groups()[0]
+        return result
+    except AttributeError:
+        # No <form> part included
+        try:
+            result = re.match('^.+<body[^\>]*>(.*)</body>.*$', html_code,
+                              re.DOTALL).groups()[0]
+            return result
+        except AttributeError:
+            # No <form> and no <body> tag...
+            pass
+    return html_code
+
+def getName(context):
+    """Construct a name out of an object with prefix and title.
+    """
+    prefix = context.title_prefix
+    prefix = prefix[0].upper() + prefix[1:]
+    return '%s of %s' % (prefix, context.title)
Index: waeup/trunk/src/waeup/utils/helpers.txt
===================================================================
--- waeup/trunk/src/waeup/utils/helpers.txt	(revision 4789)
+++ waeup/trunk/src/waeup/utils/helpers.txt	(revision 4789)
@@ -0,0 +1,221 @@
+:mod:`waeup.utils.helpers` -- Helpers for the WAeUP SRP
+*******************************************************
+
+.. module:: waeup.utils.helpers
+
+Helper functions for the WAeUP SRP.
+
+:Test-Layer: unit
+
+:func:`removeFileOrDirectory`
+=============================
+
+.. function:: removeFileOrDirectory(path)
+
+   Removes a file or directory given by a path. We can remove files:
+
+     >>> import os
+     >>> from waeup.utils.helpers import removeFileOrDirectory
+     >>> open('blah', 'wb').write('nonsense')
+     >>> 'blah' in os.listdir('.')
+     True
+
+     >>> removeFileOrDirectory('blah')
+     >>> 'blah' in os.listdir('.')
+     False
+
+   We can remove directories:
+
+     >>> os.mkdir('blah')
+     >>> 'blah' in os.listdir('.')
+     True
+
+     >>> removeFileOrDirectory('blah')
+     >>> 'blah' in os.listdir('.')
+     False
+
+
+:func:`copyFileSystemTree`
+==========================
+
+.. function:: copyFileSystemTree(src_path, dst_path[, overwrite=False[, del_old=False]])
+
+   Copies the contents of an (existing) directory to another
+   (existing) directory.
+
+   :param src_path: filesystem path to copy from
+   :type  src_path: string
+   :param dst_path: filesystem path to copy to
+   :type  dst_path: string
+   :keyword overwrite: Whether exiting files with same names should be
+                     overwritten.
+   :type  overwrite: bool
+   :keyword del_old: Whether old contents in destination path should be 
+                   removed.
+   :type  del_old: bool
+   :return: List of non-copied files
+ 
+   Both directories must exist.
+
+   Unix hidden files and directories (starting with '.') are not
+   processed by this function.
+
+   Without any further parameters, we can copy complete file trees:
+
+     >>> os.mkdir('src')
+     >>> os.mkdir('dst')
+     >>> open(os.path.join('src', 'blah'), 'wb').write('nonsense')
+
+     >>> from waeup.utils.helpers import copyFileSystemTree
+     >>> result = copyFileSystemTree('src', 'dst')
+
+   As a result we get a list of non-copied files:
+
+     >>> result
+     []
+
+   The created file was indeed copied:
+
+     >>> 'blah' in os.listdir('dst')
+     True
+
+   Hidden files (i.e. such starting with a dot) are not copied:
+
+     >>> open(os.path.join('src', '.blah'), 'wb').write('nonsense')
+     >>> result = copyFileSystemTree('src', 'dst')
+     >>> '.blah' in os.listdir('dst')
+     False
+
+   This function supports some keyword parameters as explained below.
+
+Using ``overwrite``
+-------------------
+
+Boolean. If set to ``True``, any existing and same named files and
+directories in the destination dir are overwritten with copies from
+the source. Default is `False`.
+
+Normally, existing same named files in the destination are not
+overwritten:
+
+    >>> open(os.path.join('src', 'blah'), 'wb').write('newnonsense')
+    >>> result = copyFileSystemTree('src', 'dst')
+    >>> open(os.path.join('dst', 'blah'), 'rb').read()
+    'nonsense'
+
+Instead the filename is added to the result (a list of non-copied
+files):
+
+    >>> result
+    ['blah']
+
+If, however, we use `overwrite`, the existing file will be
+overwritten:
+
+    >>> result = copyFileSystemTree('src', 'dst', overwrite=True)
+    >>> open(os.path.join('dst', 'blah'), 'rb').read()
+    'newnonsense'
+
+    >>> result
+    []
+
+This also works for complete directories:
+
+    >>> os.mkdir(os.path.join('src', 'mydir'))
+    >>> os.mkdir(os.path.join('dst', 'mydir'))
+    >>> open(os.path.join(
+    ...   'src', 'mydir', 'blah'), 'wb').write('srcblah')
+    >>> open(os.path.join(
+    ...   'dst', 'mydir', 'blah'), 'wb').write('dstblah')
+
+    >>> result = copyFileSystemTree('src', 'dst', overwrite=True)
+    >>> open(os.path.join('dst', 'mydir', 'blah'), 'rb').read()
+    'srcblah'
+
+
+Using ``del_old``
+-----------------
+
+Boolean. If set to ``True``, any copied files and directories will be
+removed from the src dir. Default is `False`.
+
+    >>> result = copyFileSystemTree('src', 'dst', overwrite=True, 
+    ...                                           del_old=True)
+    >>> os.listdir('src')
+    ['.blah']
+
+All files and directories are removed from src, except the hidden file
+we created in the beginning.
+
+
+Clean up:
+
+    >>> removeFileOrDirectory('src')
+    >>> removeFileOrDirectory('dst')
+
+
+:func:`getInnerHTMLPart()`
+==========================
+
+.. function:: getInnerHTMLPart(html_code)
+
+   Get the 'inner' part out of a piece of HTML code.
+
+   Helper function mainly to extract 'real content' from already
+   rendered forms.
+
+   The term 'inner part' here means the ``<form>`` part of an HTML
+   snippet. If this cannot be found, we look for a ``<body>`` part and
+   if this cannot be found as well, we simply return the whole input.
+
+   If a ``<form>`` part can be found in an HTML snippet, this is
+   returned with all preceeding/following stuff stripped:
+
+     >>> from waeup.utils.helpers import getInnerHTMLPart
+     >>> print getInnerHTMLPart("""<html>
+     ... <head>
+     ... </head>
+     ... <BLANKLINE>
+     ... <body>
+     ... <form action="http://localhost/myuniversity/faculties/TF/add"
+     ...       method="post" class="edit-form"
+     ...       enctype="multipart/form-data">
+     ...   <h1>Add a department</h1>
+     ... </form>
+     ... </body>
+     ... </html>
+     ... """)
+     <BLANKLINE>
+     <form action="http://localhost/myuniversity/faculties/TF/add"
+           method="post" class="edit-form"
+           enctype="multipart/form-data">
+     <BLANKLINE>
+       <h1>Add a department</h1>
+     </form>
+     <BLANKLINE>
+     <BLANKLINE>
+
+   If there is no ``<form>`` part, try to find any ``<body>`` part:
+
+     >>> print getInnerHTMLPart("""<html>
+     ... <head>
+     ... </head>
+     ... <BLANKLINE>
+     ... <body>
+     ...  <div>Some content</div>
+     ... </body>
+     ... </html>
+     ... """)
+     <BLANKLINE>
+      <div>Some content</div>
+     <BLANKLINE>
+
+   If there is also no ``<body>`` tag, we return the input as-is:
+
+     >>> print getInnerHTMLPart("""<div>
+     ...  <div>Some content</div>
+     ... </div>
+     ... """)
+     <div>
+      <div>Some content</div>
+     </div>
Index: waeup/trunk/src/waeup/utils/importexport.py
===================================================================
--- waeup/trunk/src/waeup/utils/importexport.py	(revision 3551)
+++ waeup/trunk/src/waeup/utils/importexport.py	(revision 4789)
@@ -1,3 +1,13 @@
+import re
 import csv
+import grok
+import cPickle
+import zope.xmlpickle
+from cStringIO import StringIO
+from xml.dom.minidom import Document, getDOMImplementation
+from zope.interface import Interface
+from waeup.csvfile.interfaces import ICSVFile
+from waeup.interfaces import (IWAeUPObject, IWAeUPExporter, IWAeUPXMLExporter,
+                              IWAeUPXMLImporter, IWAeUPCSVImporter)
 
 def readFile(f):
@@ -14,2 +24,77 @@
     writer = csv.writer(open("export.csv", "wb"))
     writer.writerows(data)
+
+class Exporter(grok.Adapter):
+    """Export a WAeUP object as pickle.
+
+    This all-purpose exporter exports attributes defined in schemata
+    and contained objects (if the exported thing is a container).
+    """
+    grok.context(IWAeUPObject)
+    grok.provides(IWAeUPExporter)
+
+    def __init__(self, context):
+        self.context = context
+
+    def export(self, filepath=None):
+        if filepath is None:
+            filelike_obj = StringIO()
+        else:
+            filelike_obj = open(filepath, 'wb')
+        exported_obj = cPickle.dump(self.context, filelike_obj)
+        filelike_obj.close()
+        return filelike_obj
+
+class XMLExporter(grok.Adapter):
+    """Export a WAeUP object as XML.
+
+    This all-purpose exporter exports XML representations of pickable
+    objects.
+    """
+    grok.context(Interface)
+    grok.provides(IWAeUPXMLExporter)
+
+    def __init__(self, context):
+        self.context = context
+    
+    def export(self, filepath=None):
+        pickled_obj = cPickle.dumps(self.context)
+        result = zope.xmlpickle.toxml(pickled_obj)
+        if filepath is None:
+            filelike_obj = StringIO()
+        else:
+            filelike_obj = open(filepath, 'wb')
+        filelike_obj.write(result)
+        filelike_obj.seek(0)
+        return filelike_obj
+
+class XMLImporter(grok.Adapter):
+    """Import a WAeUP object from XML.
+    """
+    grok.context(Interface)
+    grok.provides(IWAeUPXMLImporter)
+
+    def __init__(self, context):
+        self.context = context
+    
+    def doImport(self, filepath):
+        xml = None
+        if isinstance(filepath, basestring):
+            xml = open(filepath, 'rb').read()
+        else:
+            xml = filepath.read()
+        obj = zope.xmlpickle.loads(xml)
+        return obj
+
+
+class CSVImporter(grok.MultiAdapter):
+    grok.adapts(ICSVFile, IWAeUPObject)
+    grok.provides(IWAeUPCSVImporter)
+    grok.baseclass()  # We don't want this base to be registered really.
+
+    def __init__(self, csvfile, receiver):
+        self.csvfile = csvfile
+        self.receiver = receiver
+
+    def doImport(self):
+        pass
Index: waeup/trunk/src/waeup/utils/importexport.txt
===================================================================
--- waeup/trunk/src/waeup/utils/importexport.txt	(revision 4789)
+++ waeup/trunk/src/waeup/utils/importexport.txt	(revision 4789)
@@ -0,0 +1,289 @@
+Importing and Exporting Things
+******************************
+
+The waeup package tries to support all special object types to be
+exportable and importable.
+
+:Test-Layer: unit
+
+As imports and exports of data are a cruical point when doing updates
+or to enable data backups, this support must be considered a serious
+topic for the base functionality of the whole software.
+
+Exports and imports are implemented using the Zope component framework
+capabilities. This means we need some adapters and utilities, which we
+register, by grokking the whole package::
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+
+
+Exporting XML
+=============
+
+One of the major formats we support for data export is XML.
+
+When we want to export an object, we can choose the destination
+file. If we do not give a file to write to, we get a StringIO object,
+i.e. an in-memory file.
+
+To show this basic functionality provided for all XML exportable
+objects, we choose faculties.
+
+We can export faculties. First we create a faculty::
+
+    >>> from waeup.university.faculty import Faculty
+    >>> myfaculty = Faculty()
+    >>> myfaculty.name = 'My very faculty.'
+
+Exporting plain XML
+-------------------
+
+Now let's export it to XML. To do this we first ask the framework for
+an exporter::
+
+    >>> from waeup.interfaces import IWAeUPXMLExporter
+    >>> exporter = IWAeUPXMLExporter(myfaculty)
+    >>> exporter
+    <waeup.utils.importexport.XMLExporter object at 0x...>
+
+All exporters provide an ``export(<obj>)`` method::
+
+    >>> result = exporter.export()
+
+The result is an object of type StringIO. We therefore use its
+`read()` method to get the actual XML data::
+
+    >>> print result.read()
+    <?xml version="1.0" encoding="utf-8" ?>...
+
+
+Generating XML files
+--------------------
+
+We can also let the results be written to a file::
+
+    >>> result = exporter.export('myexport.xml')
+    >>> print open('myexport.xml', 'rb').read()
+    <?xml version="1.0" encoding="utf-8" ?>
+    <pickle>
+      <initialized_object>
+    ...
+    </pickle>
+
+Clean up::
+
+    >>> import os
+    >>> os.unlink('myexport.xml')
+
+Importing XML
+=============
+
+We can generate objects from XML.
+
+Let's create a faculty instance, that we want to be restored
+afterwards::
+
+    >>> from waeup.university.faculty import Faculty
+    >>> myfaculty = Faculty()
+    >>> myfaculty.name = 'My very faculty.'
+
+We create an XML dump of this object::
+
+    >>> from waeup.interfaces import IWAeUPXMLExporter
+    >>> exporter = IWAeUPXMLExporter(myfaculty)
+    >>> result = exporter.export('myexport.xml')
+
+We change the name of the faculty::
+
+    >>> myfaculty.name = 'Another name'
+
+Now we create an importer for that file::
+
+    >>> from waeup.interfaces import IWAeUPXMLImporter
+    >>> importer = IWAeUPXMLImporter(myfaculty)
+
+Importing from filenames
+------------------------
+
+We can use a filepath to import from the denoted file::
+
+    >>> new_obj = importer.doImport('myexport.xml')
+
+The object created is indeed a faculty::
+
+    >>> new_obj
+    <waeup.university.faculty.Faculty object at 0x...>
+
+    >>> new_obj.name
+    'My very faculty.'
+
+The new object is really a new object::
+
+    >>> new_obj is myfaculty
+    False
+
+Importing from filelike objects
+-------------------------------
+
+We can also pass a file-like object instead of a filepath::
+
+    >>> filelike_obj = open('myexport.xml', 'rb')
+    >>> new_obj = importer.doImport(filelike_obj)
+
+The object created is indeed a faculty::
+
+    >>> new_obj
+    <waeup.university.faculty.Faculty object at 0x...>
+
+Clean up::
+
+    >>> import os
+    >>> os.unlink('myexport.xml')
+
+
+Importing CSV data
+==================
+
+The WAeUP portal supports import of CSV data for several different
+data types like faculties, students, and the like.
+
+For our purposes we define CSV imports as tuples, containing a CSV
+source and a CSV receiver:
+
+  ``CSV Import := <CSVSource, CSVReceiver>``
+
+CSV sources
+-----------
+
+CSV sources build a plugin framework inside the WAeUP portal (that
+might be factored out one day). See `waeup.csvfile` to learn more
+about that.
+
+A `waeup.csvfile.CSVFile` plugin cares for a special kind of CSV file.
+
+To make clear what that means, we start by creating a simple CSV
+file.
+
+CSVFile objects are abstractions of simple .csv-files. But as not
+every CSV file contains the same kind of data, we can create different
+flavours of it, for instance to import departments or faculties. As
+each kind of import needs different kind of data, we have different
+kinds of CSV sources.
+
+CSV sources can check their associated files for consistency,
+importability, etc.
+
+We create a simple CSV file with cave data:
+
+    >>> open('mycavedata.csv', 'wb').write(
+    ... """size,owner
+    ... 42,Manfred
+    ... """)
+
+We have a very simple CSVFile available, which is registered as an
+adapter for strings. To make this work, the string should be a path to
+a file:
+
+    >>> from waeup.csvfile import getCSVFile
+    >>> mysource = getCSVFile('mycavedata.csv')
+    >>> mysource
+    <waeup.csvfile.csvfile.CSVFile object at 0x...>
+
+We define caves like this:
+
+    >>> class Cave(object):
+    ...   def __init__(self, size, owner):
+    ...     self.size = size
+    ...     self.owner = owner
+    ...   def __repr__(self):
+    ...     return '<Cave object [size: %s, owner: %s]>' % (
+    ...       self.size, self.owner)
+
+Let's assume, we have a container for caves:
+
+    >>> from zope.interface import Interface
+    >>> class ICaveContainer(Interface):
+    ...   """A container for caves."""
+
+    >>> class CaveContainer(object):
+    ...   grok.implements(ICaveContainer)
+    ...   caves = []
+    ...   def addCave(self, cave):
+    ...     self.caves.append(cave)
+
+    >>> mycaves = CaveContainer()
+
+Now, if we want to import caves from CSV files, we also need an
+importer, that imports data to the container:
+
+    >>> from waeup.csvfile.interfaces import ICSVFile
+    >>> from waeup.utils.importexport import CSVImporter
+    >>> from waeup.interfaces import IWAeUPCSVImporter
+    >>> class CaveImporter(CSVImporter):
+    ...   # Tell, what kinds of objects we connect...
+    ...   grok.adapts(ICSVFile, ICaveContainer)
+    ...   # Tell the world, that we are an importer...
+    ...   grok.provides(IWAeUPCSVImporter)
+    ...   # The datatype can be used by UI components and is required
+    ...   # by the interface...
+    ...   datatype = u'Cave Importer'
+    ...   def doImport(self):
+    ...     # CSVImporter instances have a `csvfile` and a `receiver`
+    ...     # object defined which refer to the CSV file and the container.
+    ...     for row in self.csvfile.getData():
+    ...       cave = Cave(size = row['size'],
+    ...                   owner = row['owner'])
+    ...       self.receiver.addCave(cave) 
+
+
+An CSV importer must be grokked before we can use it. In real life,
+this is normally done on startup automatically:
+
+    >>> grok.testing.grok_component('CaveImporter', CaveImporter)
+    True
+
+Importers are multi-adapters, so we need to ask for one:
+
+    >>> from zope.component import getMultiAdapter
+    >>> myimporter = getMultiAdapter((mysource, mycaves), IWAeUPCSVImporter)
+
+Now we can finally do the import:
+
+    >>> myimporter.doImport()
+
+The import was really done:
+
+    >>> mycaves.caves
+    [<Cave object [size: 42, owner: Manfred]>]
+
+**Important note**:
+
+  This importer adapts ICSVFile and ICaveContainer, which means that
+  the type of data receiver is specified correctly. For the CSV
+  source, however, the importer cannot be sure to find a column
+  ``size`` or ``owner`` in the file.
+
+  This can be prevented, by defining a more special CSVFile type and
+  adapting this instead of ICSVData.
+
+  In the `waeup.csvfile` subpackage's README it is shown, how this can
+  be accomplished.
+
+  Summing up we would define an ICaveCSVFile type, provide an
+  appropriate CSV file wrapper and then let our importer adapt::
+
+    (ICaveCSVFile, ICaveContainer)
+
+**Another note**:
+
+  The importer we defined above does not support the ``clear_old_data``
+  and ``overwrite`` keywords as would be required by the
+  IWAeUPImporter interface.
+
+  In real life this keywords should be supported.
+
+Clean up:
+
+   >>> import os
+   >>> os.unlink('mycavedata.csv')
Index: waeup/trunk/src/waeup/viewlets.py
===================================================================
--- waeup/trunk/src/waeup/viewlets.py	(revision 3551)
+++ 	(revision )
@@ -1,61 +1,0 @@
-# -*- coding: utf-8 -*-
-import grok
-import zope.interface
-from interfaces import IUniversity 
-
-grok.context(zope.interface.Interface)
-
-class Index(grok.View):
-    """main structure for all pages"""
-    grok.template('master')
-    
-    def __init__(self, context, request):#
-        super(Index, self).__init__(context, request)
-
-class Head(grok.ViewletManager):
-    """additions to HTML head"""
-    grok.name('head')
-
-class Title(grok.Viewlet):
-    grok.viewletmanager(Head)
-
-class AppCSS(grok.Viewlet):
-    grok.viewletmanager(Head)
-
-class Header(grok.ViewletManager):
-    grok.name('header')
-
-class Logo(grok.Viewlet):
-    grok.viewletmanager(Header)
-    grok.order(1)
-
-class NavigationBar(grok.Viewlet):
-    grok.viewletmanager(Header)
-    grok.order(2)
-    
-    def getSite(self):
-        return grok.getSite()
-
-class LeftSidebar(grok.ViewletManager):
-    grok.name('left')
-
-class AuthUser(grok.Viewlet):
-    grok.viewletmanager(LeftSidebar)
-    grok.view(Index)
-    grok.order(1)
-
-class Login(grok.Viewlet):
-    grok.viewletmanager(LeftSidebar)
-    grok.context(IUniversity)
-    grok.view(Index)
-    grok.order(2)
-
-class MainArea(grok.ViewletManager):
-    grok.name('main')
-
-
-class Footer(grok.ViewletManager):
-    grok.name('footer')
-
-class Copyright(grok.Viewlet):
-    grok.viewletmanager(Footer)
Index: waeup/trunk/src/waeup/widgets/__init__.py
===================================================================
--- waeup/trunk/src/waeup/widgets/__init__.py	(revision 4789)
+++ waeup/trunk/src/waeup/widgets/__init__.py	(revision 4789)
@@ -0,0 +1,2 @@
+"""JS-driven widgets for WAeUP.
+"""
Index: waeup/trunk/src/waeup/widgets/interfaces.py
===================================================================
--- waeup/trunk/src/waeup/widgets/interfaces.py	(revision 4789)
+++ waeup/trunk/src/waeup/widgets/interfaces.py	(revision 4789)
@@ -0,0 +1,12 @@
+##
+## interfaces.py
+"""Interfaces for widgets.
+"""
+from zope.interface import Interface
+
+class ITableProvider(Interface):
+    """A provider for tables.
+    """
+    def getTables():
+        """Get a tuple of tables.
+        """
Index: waeup/trunk/src/waeup/widgets/table.py
===================================================================
--- waeup/trunk/src/waeup/widgets/table.py	(revision 4789)
+++ waeup/trunk/src/waeup/widgets/table.py	(revision 4789)
@@ -0,0 +1,126 @@
+"""Table widget.
+"""
+import grok
+from hurry import yui
+from zope.interface import Interface
+from waeup.widgets.interfaces import ITableProvider
+
+def varName(string):
+    varname = string.lower()
+    return ''.join([x for x in varname
+                    if x in 'abcdefghijklmnopqrstuvwxyz_0123456789'])
+
+class Col(object):
+    """A column in a table.
+    """
+    header = 'Unnamed'
+    type = None
+    sortable = False
+    width = 100
+
+    def __init__(self, header=u'Unnamed', width=None, sortable=False,
+                 type=None, data=()):
+        self.header = header
+        self.width = width
+        self.sortable = sortable
+        self.type = type
+        self.data = data
+        self.key = varName(header)
+    
+class Table(object):
+    """A table.
+    """
+    def __init__(self, title=u'Unnamed', width=500, height=180, cols=()):
+        self.title = title
+        self.cols = cols
+        self.width = width
+        self.height = height
+        self.internal_title = varName(title)
+
+    def renderHTML(self):
+        result = '<div id="%s-tablecontainer"><table id="%s-table">' % (
+            self.internal_title, self.internal_title)
+        data_rows = self.getRows()
+        result += '<thead><tr>'
+        for col in self.cols:
+            result += '<th>%s</th>' % (col.header,)
+        result += '</tr></thead>\n'
+        result += '<tbody>'
+            
+        for row in data_rows:
+            result += '<tr>\n  '
+            for elem in row:
+                result += '<td>%s</td>' % elem
+            result += '\n</tr>'
+        result += '</tbody></table></div>'
+        return result
+
+    def getRows(self):
+        data_cols = (x.data for x in self.cols)
+        # turn rows into cols and vice versa:
+        # ((a, 1, p), (b, 2, q)) --> ((a, b), (1, 2), (p, q))
+        data_rows = tuple(map(lambda *x: (x), *data_cols))
+        return data_rows
+
+
+class YUITable(Table):
+    """A table that can generate JavaScript code for use with YUI.
+    """
+    def need(self):
+        yui.dragdrop.need() # datatable needs dragdrop loaded in advance.
+                            # This dependency is not defined yet in hurry.yui.
+        yui.paginator.need()
+        yui.datatable.need()
+        yui.sam.need()
+        yui.fonts.need()
+
+    def getJSTableCode(self, paginator=True):
+        title = self.internal_title
+        fielddefs = ",".join(['{key:"%s"}'%x.key for x in self.cols])
+        coldefs = ",".join([
+                '{key:"%s",label:"%s",sortable:%s}' % (
+                    x.key, x.header, str(x.sortable).lower())
+                for x in self.cols
+                ])
+        result = """
+YAHOO.util.Event.addListener(window, "load", function() {
+  var %sDataSource = new YAHOO.util.DataSource(YAHOO.util.Dom.get("%s-table"));
+  %sDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
+  %sDataSource.responseSchema = {
+    fields: [%s]
+  };
+
+  var %sColDefs = [%s];
+  var %sDataTable = new YAHOO.widget.DataTable("%s-tablecontainer",
+                                               %sColDefs, %sDataSource,
+                                               {
+paginator: new YAHOO.widget.Paginator({ 
+	                rowsPerPage: 15, 
+	                template: YAHOO.widget.Paginator.TEMPLATE_ROWS_PER_PAGE, 
+	                rowsPerPageOptions: [10,15,30,50,100], 
+	                pageLinks: 5 
+	            }), 
+draggableColumns:true });
+});
+""" % (
+            title, title, title, title, fielddefs, title, coldefs,
+            title, title, title, title)
+        return result
+
+
+    
+class YUIStaticTableView(grok.View):
+    grok.context(Interface)
+    grok.name('yuistatictables.js')
+
+    def render(self):
+        try:
+            provider = ITableProvider(self.context)
+        except:
+            # No table provider for the context. Be gentle...
+            return ''
+        self.response.setHeader('Content-Type',
+                                'text/javascript; charset=UTF-8')
+        tables = provider.getTables()
+        results = [table.getJSTableCode() for table in tables]
+        return '\n'.join(results)
Index: waeup/trunk/src/waeup/widgets/table.txt
===================================================================
--- waeup/trunk/src/waeup/widgets/table.txt	(revision 4789)
+++ waeup/trunk/src/waeup/widgets/table.txt	(revision 4789)
@@ -0,0 +1,134 @@
+waeup.widgets
+*************
+
+Widgets for the WAeUP SRP.
+
+
+:doctest:
+
+Tables
+======
+
+    >>> from waeup.widgets.table import Col, Table
+    >>> id = Col(
+    ...       header='Id', width=100, sortable=True, type='int',
+    ...       data = (1, 2, 3))
+    >>> mycol1 = Col(
+    ...       header='Column 1', width=100, sortable=True,
+    ...       data = ('bli', 'bla', 'blub'))
+
+    >>> mycol2 = Col(
+    ...     header='Column 2', width=100, sortable=True,
+    ...     data = ('schwi', 'schwa', 'schwafel'))
+
+    >>> table = Table('My Table', cols=(id, mycol1, mycol2))
+    >>> print table.renderHTML()
+    <div id="mytable-tablecontainer"><table id="mytable-table"><thead><tr><th>Id</th><th>Column 1</th><th>Column 2</th></tr></thead>
+    ...<tbody><tr>
+    ...<td>1</td><td>bli</td><td>schwi</td>
+    ...</tr><tr>
+    ...<td>2</td><td>bla</td><td>schwa</td>
+    ...</tr><tr>
+    ...<td>3</td><td>blub</td><td>schwafel</td>
+    ...</tr></tbody></table></div>
+
+    >>> print table.internal_title
+    mytable
+
+YUITable
+--------
+
+A YUI table extends a regular table by adding some JavaScript
+code-generation facilities suitable for rendering YUI based
+datatables. Futhermore they provide a `need` method, which includes
+all needed YUI scripts and stylesheets.
+
+
+    >>> from waeup.widgets.table import YUITable
+    >>> table = YUITable('My YUI Table', cols=(id, mycol1, mycol2))
+    >>> print table.getJSTableCode()
+    <BLANKLINE>
+    YAHOO.util.Event.addListener(window, "load", function() {...
+    ...
+    draggableColumns:true });
+    });
+
+Naturally, YUITable objects are suited for use by views.
+
+
+YUIStaticTableView
+==================
+
+This is a view named ``yuistatictables.js`` which from a users'
+perspective can be used like a static file. It is, however, not
+static, but returns the JavaScript code suitable for a given table.
+
+We define some content object that contains a table for which we want
+the JavaScript to be generated:
+
+    >>> class SomeContent(object):
+    ...   mytable = table
+
+    >>> obj = SomeContent()
+
+Now we need an adapter, that knows how to get a table from this kind
+of object. The YUIStaticTableView looks up such an adapter for a given
+context to find a table to manage. The adapter has to implement
+`ITableProvider` from `waeup.widgets.interfaces` and should provide a
+`getTables` method.
+
+We define and register such an adapter using grok:
+
+    >>> import grok
+    >>> grok.testing.grok('waeup')
+    >>> from waeup.widgets.interfaces import ITableProvider
+    >>> class TableAdapter(grok.Adapter):
+    ...   grok.context(SomeContent)
+    ...   grok.provides(ITableProvider)
+    ...
+    ...   def getTables(self):
+    ...     # We know that our context stores a table in `mytable`
+    ...     return (self.context.mytable,)
+    >>> grok.testing.grok_component('TableAdapter', TableAdapter)
+    True
+
+Now let's get a view on the object defined above. We want the view
+named ``yuistatictables.js``:
+
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.component import getMultiAdapter
+    >>> from zope.interface import Interface
+    >>> view = getMultiAdapter((obj, TestRequest()), Interface, 
+    ...                        name='yuistatictables.js')
+    >>> view
+    <waeup.widgets.table.YUIStaticTableView object at 0x...>
+
+When we call the view we get pure JavaScript:
+
+    >>> print view()
+    <BLANKLINE>
+    YAHOO.util.Event.addListener(window, "load", function() {
+    ...
+    draggableColumns:true });
+    });
+
+Table views for objects that have no table
+------------------------------------------
+
+What happens, when no `ITableProvider` can be found for an object? The
+view will return an empty string.
+
+Let's try this with a simple object for which no adapter is
+registered. We can get the view:
+
+    >>> obj = object()
+    >>> view = getMultiAdapter((obj, TestRequest()), Interface, 
+    ...                        name='yuistatictables.js')
+    >>> view
+    <waeup.widgets.table.YUIStaticTableView object at 0x...>
+
+But if we call this adapter, we will get an empty result, because
+there is no ITableProvider adapter registered for the 'viewed' object:
+
+    >>> view()
+    ''
Index: waeup/trunk/src/waeup/workflow.py
===================================================================
--- waeup/trunk/src/waeup/workflow.py	(revision 4789)
+++ waeup/trunk/src/waeup/workflow.py	(revision 4789)
@@ -0,0 +1,100 @@
+"""Workflows used in the WAeUP SRP portal.
+"""
+import grok
+from hurry.workflow.workflow import Transition, Workflow, WorkflowVersions
+from hurry.workflow.workflow import WorkflowInfo, WorkflowState, NullCondition
+from hurry.workflow.interfaces import MANUAL, AUTOMATIC, SYSTEM
+from hurry.workflow.interfaces import IWorkflow, IWorkflowState, IWorkflowInfo
+from hurry.workflow.interfaces import IWorkflowVersions
+from waeup.interfaces import IWAeUPObject
+
+UNCHECKED = 'unchecked'
+CHECKED = 'checked'
+
+def create_workflow():
+    init_transition = Transition(
+        transition_id = 'init',
+        title = 'Initialize',
+        source = None,
+        condition = NullCondition,
+        destination = UNCHECKED)
+
+    check_transition = Transition(
+        transition_id = 'check',
+        title = 'Check',
+        source = UNCHECKED,
+        destination = CHECKED)
+
+    final_transition = Transition(
+        transition_id = 'finalize',
+        title = 'Delete',
+        source = CHECKED,
+        destination = None)
+
+    return [init_transition, check_transition, final_transition]
+
+
+class WAeUPWorkflow(Workflow):
+    """A hurry.workflow Workflow with more appropriate error messages.
+    """
+    grok.provides(IWorkflow)
+    def __init__(self):
+        super(Workflow, self).__init__()
+        self.refresh(create_workflow())
+
+    def getTransition(self, source, transition_id):
+        from hurry.workflow.interfaces import\
+            InvalidTransitionError, ConditionFailedError
+
+        transition = self._id_transitions[transition_id]
+        if transition.source != source:
+            raise InvalidTransitionError(
+                "Transition '%s' requires '%s' as source state (is: '%s')" % (
+                    transition_id, transition.source, source))
+        return transition
+
+
+class WorkflowNullVersions(WorkflowVersions):
+    """A workflow versions manager that does not handle versions.
+
+    Sounds odd, but the default implementation of
+    :class:`hurry.workflow.workflow.WorkflowVersions` is a base
+    implementation that raises :exc:`NotImplemented` exceptions for
+    most of the methods defined below.
+
+    If we want to register a versionless workflow, an utility
+    implementing IWorkflowVersions is looked up nevertheless by
+    WorkflowInfo and WorkflowState components so we **have** to
+    provide workflow versions, even if we do not support versioned
+    workflows.
+
+    This implementation returns empty result sets for any requests,
+    but does not raise :exc:`NotImplemented`.
+    """
+    def getVersions(self, state, id):
+        return []
+
+    def getVersionsWithAutomaticTransitions(self):
+        return []
+
+    def hasVersion(self, id, state):
+        return False
+
+    def hasVersionId(self, id):
+        return False
+
+# Register global utilities for workflows and workflow versions...
+grok.global_utility(WAeUPWorkflow, IWorkflow)
+grok.global_utility(WorkflowNullVersions, IWorkflowVersions)
+
+class WorkflowState(grok.Adapter, WorkflowState):
+    """An adapter to adapt WAeUP objects to workflow states.
+    """
+    grok.context(IWAeUPObject)
+    grok.provides(IWorkflowState)
+    
+class WorkflowInfo(grok.Adapter, WorkflowInfo):
+    """Adapter to adapt WAeUP objects to workflow info objects.
+    """
+    grok.context(IWAeUPObject)
+    grok.provides(IWorkflowInfo)
