Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/__init__.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/__init__.py	(revision 5676)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/__init__.py	(revision 5676)
@@ -0,0 +1,3 @@
+"""This package contains everything regarding university applicants.
+"""
+# Make this a package.
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/container.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/container.py	(revision 5676)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/container.py	(revision 5676)
@@ -0,0 +1,70 @@
+##
+## container.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu Jan 20 04:33:18 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Containers for university applicants.
+"""
+import grok
+from waeup.sirp.applicants.interfaces import IApplicantsContainer
+
+class ApplicantsContainer(grok.Container):
+    """An applicants container contains university applicants.
+    """
+    grok.implements(IApplicantsContainer)
+
+    id = None
+    title = None
+    description = None
+    startdate = None
+    enddate = None
+    strict_deadline = True
+
+    def archive(self, app_ids=None):
+        """Create on-dist archive of applicants stored in this term.
+
+        If app_ids is `None`, all applicants are archived.
+
+        If app_ids contains a single id string, only the respective
+        applicants are archived.
+
+        If app_ids contains a list of id strings all of the respective
+        applicants types are saved to disk.
+        """
+        raise NotImplemented()
+
+    def clear(self, app_ids=None, archive=True):
+        """Remove applicants of type given by 'id'.
+
+        Optionally archive the applicants.
+        
+        If id is `None`, all applicants are archived.
+
+        If id contains a single id string, only the respective
+        applicants are archived.
+
+        If id contains a list of id strings all of the respective
+        applicant types are saved to disk.
+
+        If `archive` is ``False`` none of the archive-handling is done
+        and respective applicants are simply removed from the
+        database.
+        """
+        raise NotImplemented()
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py	(revision 5676)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py	(revision 5676)
@@ -0,0 +1,113 @@
+##
+## interfaces.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun Jan 16 15:30:01 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Interfaces regarding student applicants and related components.
+"""
+from zope import schema
+from waeup.sirp.interfaces import IWAeUPObject
+
+class IApplicantsRoot(IWAeUPObject):
+    """A container for university applicants containers.
+    """
+    def addApplicantsContainer(container, name=None):
+        """Add an applicants container.
+
+        Adds an applicants container that implements `interface`
+        under the name `name`.
+
+        `container` the container instance to be added. Should
+          implement :class:`IApplicantsContainer`.
+
+        `name`
+          the name under which the container will be accessible. We
+          usually use names like ``pume_2011`` to indicate, that the
+          new container will contain university applicants for a
+          certain screening type (``pume``) and of the year 2011.
+        """
+
+class IApplicantsContainer(IWAeUPObject):
+    """An applicants container contains university applicants.
+
+    """
+    id = schema.TextLine(
+        title = u'Internal ID',
+        required = True,
+        )
+
+    title = schema.TextLine(
+        title = u'Short description of the type of applicants stored here.',
+        required = True,
+        default = u'Untitled',
+        )
+    
+    description = schema.Text(
+        title = u'Human readable description in reST format',
+        required = False,
+        default = u'Not set.'
+        )
+
+    startdate = schema.Date(
+        title = u'Date when the application period starts',
+        required = False,
+        default = None,
+        )
+
+    enddate = schema.Date(
+        title = u'Date when the application period ends',
+        required = False,
+        default = None,
+        )
+
+    strict_deadline = schema.Bool(
+        title = u'Forbid additions after deadline (enddate)',
+        required = True,
+        default = True,
+        )
+
+    def archive(id=None):
+        """Create on-dist archive of applicants stored in this term.
+
+        If id is `None`, all applicants are archived.
+
+        If id contains a single id string, only the respective
+        applicants are archived.
+
+        If id contains a list of id strings all of the respective
+        applicants types are saved to disk.
+        """
+
+    def clear(id=None, archive=True):
+        """Remove applicants of type given by 'id'.
+
+        Optionally archive the applicants.
+        
+        If id is `None`, all applicants are archived.
+
+        If id contains a single id string, only the respective
+        applicants are archived.
+
+        If id contains a list of id strings all of the respective
+        applicant types are saved to disk.
+
+        If `archive` is ``False`` none of the archive-handling is done
+        and respective applicants are simply removed from the
+        database.
+        """
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/root.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/root.py	(revision 5676)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/root.py	(revision 5676)
@@ -0,0 +1,107 @@
+##
+## root.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu Jan 20 04:17:59 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+The root for applicants.
+"""
+import grok
+from waeup.sirp.interfaces import IWAeUPSIRPPluggable
+from waeup.sirp.applicants.interfaces import IApplicantsRoot
+
+class ApplicantsRoot(grok.Container):
+    """The root of applicants-related components. It contains primarily 
+    containers for applicants.
+    """
+    grok.implements(IApplicantsRoot)
+
+    def addApplicantsContainer(self, container, name=None):
+        """Add an applicants container.
+
+        Adds an applicants container that implements `interface`
+        under the name `name`.
+
+        `container`
+          the container instance to be added. Should
+          implement :class:`IApplicantsContainer`.
+
+        `name`
+          the name under which the container will be accessible. We
+          usually use names like ``pume_2011`` to indicate, that the
+          new container will contain student applicants for a
+          certain screening type (``pume``) and of the year 2011.
+
+        """
+        self[name] = container
+        return
+
+class ApplicantsPlugin(grok.GlobalUtility):
+    """A WAeUPSIRPPlugin that creates an applicants root in portal.
+
+    This plugin should be called by a typical
+    `waeup.sirp.app.Universtiy` instance on creation time. The
+    :meth:`update` method normally can also be triggered manually over
+    the main site configuration.
+
+    Implements :class:`waeup.sirp.interfaces.IWAeUPSIRPPluggable`
+    """
+    grok.name('applicants')
+    grok.implements(IWAeUPSIRPPluggable)
+    log_prefix = 'ApplicantsPlugin'
+
+    def setup(self, site, name, logger):
+        """Create a new :class:`ApplicantsRoot` instance in `site`.
+        """
+        site['applicants'] = ApplicantsRoot()
+        logger.info(
+            '%s: Installed applicants root.' % (self.log_prefix,)
+            )
+        return
+
+    def update(self, site, name, logger):
+        """Update site wide ``applicants`` root.
+
+        If the site already contains a suitable ``applicants`` root,
+        leave it that way. If not create one and delete the old one if
+        appropriate.
+        """
+        app_folder = site.get('applicants', None)
+        site_name = getattr(site, '__name__', 'Unnamed Site?')
+        if IApplicantsRoot.providedBy(app_folder):
+            # Applicants up to date. Return.
+            logger.info(
+                '%s: Updating site at %s: Nothing to do.' % (
+                    self.log_prefix, site_name,)
+                )
+            return
+        elif app_folder is not None:
+            # Applicants need update. Remove old instance.
+            logger.warn(
+                '%s: Outdated applicants folder detected at site %s.'
+                'Removing it.' % (self.log_prefix, site_name)
+                    )
+            del site['applicants']
+        # Add new applicants.
+        logger.info(
+            '%s: Updating site at %s. Installing '
+            'applicants.' % (self.log_prefix, site_name,)
+            )
+        self.setup(site, name, logger)
+        return
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/__init__.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/__init__.py	(revision 5676)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/__init__.py	(revision 5676)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_container.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_container.py	(revision 5676)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_container.py	(revision 5676)
@@ -0,0 +1,53 @@
+##
+## test_container.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu Jan 20 09:30:37 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for applicants containers.
+"""
+import unittest
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.sirp.applicants import interfaces
+from waeup.sirp.applicants.container import ApplicantsContainer
+
+class ApplicantsContainerTestCase(unittest.TestCase):
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                interfaces.IApplicantsContainer, ApplicantsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                interfaces.IApplicantsContainer, ApplicantsContainer())
+            )
+        return
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTests(
+        unittest.TestLoader().loadTestsFromTestCase(
+            ApplicantsContainerTestCase,
+            )
+        )
+    return suite
+
+test_suite = suite
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_root.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_root.py	(revision 5676)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_root.py	(revision 5676)
@@ -0,0 +1,152 @@
+##
+## test_root.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu Jan 20 09:26:54 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test applicants root.
+"""
+import logging
+import unittest
+from StringIO import StringIO
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.sirp.applicants import interfaces
+from waeup.sirp.applicants.root import (
+    ApplicantsRoot, ApplicantsPlugin,
+    )
+
+class FakeSite(dict):
+    pass
+
+class ApplicantsRootTestCase(unittest.TestCase):
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                interfaces.IApplicantsRoot, ApplicantsRoot)
+            )
+        self.assertTrue(
+            verifyObject(
+                interfaces.IApplicantsRoot, ApplicantsRoot())
+            )
+        return
+
+class ApplicantsRootPluginTestCase(unittest.TestCase):
+    def create_logger(self):
+        # create a logger suitable for local tests.
+        test_logger = logging.getLogger('waeup.sirp.applicants.testlogger')
+        log = StringIO()
+        handler = logging.StreamHandler(log)
+        handler.setLevel(logging.DEBUG)
+        test_logger.addHandler(handler)
+        test_logger.setLevel(logging.DEBUG)
+        self.logger = test_logger
+        self.log = log
+        self.handler = handler
+        return self.logger
+
+    def remove_logger(self):
+        del self.handler
+        del self.logger
+        del self.log
+        pass
+
+    def get_log(self):
+        self.log.seek(0)
+        return self.log.read()
+
+    def setUp(self):
+        self.create_logger()
+        return
+
+    def tearDown(self):
+        self.remove_logger()
+        return
+
+    # Real tests start here...
+    def test_pluginsetup(self):
+        # Make sure we can add ApplicantsRoot to sites.
+        site = FakeSite()
+        plugin = ApplicantsPlugin()
+        plugin.setup(site, 'blah', self.logger)
+        self.assertTrue('applicants' in site.keys())
+        log = self.get_log()
+        self.assertTrue('Installed applicants root.' in log)
+        return
+
+    def test_update_new(self):
+        # Run update on a site without applicants root.
+        site = FakeSite()
+        plugin = ApplicantsPlugin()
+        plugin.update(site, 'blah', self.logger)
+        self.assertTrue('applicants' in site.keys())
+        log = self.get_log()
+        self.assertTrue('Updating site at Unnamed Site' in log)
+        self.assertTrue('Installed applicants root.' in log)
+        return
+
+    def test_update_outdated(self):
+        # Run update on a site with outdated applicants root.
+        site = FakeSite()
+        root = object() # # This is not a proper applicants root
+        site['applicants'] = root
+        plugin = ApplicantsPlugin()
+        plugin.update(site, 'blah', self.logger)
+        self.assertTrue(site['applicants'] is not root)
+        self.assertTrue(isinstance(site['applicants'], ApplicantsRoot))
+        log = self.get_log()
+        self.assertTrue('Outdated applicants folder detected' in log)
+        self.assertTrue('Updating site at Unnamed Site' in log)
+        self.assertTrue('Installed applicants root.' in log)
+        return
+
+    def test_update_uptodate(self):
+        # Run update on a site with proper applicants root.
+        site = FakeSite()
+        root = ApplicantsRoot()
+        site['applicants'] = root
+        plugin = ApplicantsPlugin()
+        plugin.update(site, 'blah', self.logger)
+        self.assertTrue(site['applicants'] is root)
+        log = self.get_log()
+        self.assertTrue('Updating site at Unnamed Site' in log)
+        self.assertTrue('Nothing to do' in log)
+        return
+
+    def test_update_log(self):
+        # Check that sitename is used in log messages on updates.
+        site = FakeSite()
+        site.__name__ = 'my_site'
+        plugin = ApplicantsPlugin()
+        plugin.update(site, 'blah', self.logger)
+        log = self.get_log()
+        self.assertTrue('Updating site at my_site.' in log)
+        return
+        
+def suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+            ApplicantsRootTestCase,
+            ApplicantsRootPluginTestCase,
+            ]:
+        suite.addTests(unittest.TestLoader().loadTestsFromTestCase(testcase))
+    return suite
+
+test_suite = suite
Index: main/waeup.sirp/trunk/src/waeup/sirp/interfaces.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/interfaces.py	(revision 5672)
+++ main/waeup.sirp/trunk/src/waeup/sirp/interfaces.py	(revision 5676)
@@ -251,5 +251,5 @@
     needing to know about that parts. The site simply collects all
     available plugins, calls them and the plugins care for their
-    respective subarea like the applications area or the datacenter
+    respective subarea like the applicants area or the datacenter
     area.
 
