diff --git a/CHANGES.txt b/CHANGES.txt
index 0a4e071..ed72985 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -6,4 +6,10 @@ $Id$
0.9
---
+- basic job management: a job executor view calls job managers specified
+ by loops root option ``organize.job.managers``
+- allow ``__getitem__`` on Loops and ViewManager, this is a prerequisite for
+ using virtual hosts over more than one path element (e.g. leading to
+ views/home) on protected sites; this also allows calling of job processiong
+ views via wget without login credentials
- add definition of loops package version (see loops/version.py)
diff --git a/organize/configure.zcml b/organize/configure.zcml
index 2ee6835..499b27c 100644
--- a/organize/configure.zcml
+++ b/organize/configure.zcml
@@ -68,6 +68,7 @@
+
diff --git a/organize/job/README.txt b/organize/job/README.txt
new file mode 100644
index 0000000..f6d99c7
--- /dev/null
+++ b/organize/job/README.txt
@@ -0,0 +1,71 @@
+===============================================================
+loops - Linked Objects for Organization and Processing Services
+===============================================================
+
+ ($Id$)
+
+Let's do some basic setup
+
+ >>> from zope.app.testing.setup import placefulSetUp, placefulTearDown
+ >>> site = placefulSetUp(True)
+ >>> from zope import component, interface
+
+and set up a simple loops site with a concept manager and some concepts
+(with all the type machinery, what in real life is done via standard
+ZCML setup):
+
+ >>> from loops.organize.setup import SetupManager
+ >>> component.provideAdapter(SetupManager, name='organize')
+
+ >>> from loops.tests.setup import TestSite
+ >>> t = TestSite(site)
+ >>> concepts, resources, views = t.setup()
+ >>> loopsRoot = site['loops']
+
+Let's also set up logging in a way that we get notified about problems.
+
+ >>> import sys
+ >>> from logging import getLogger, StreamHandler
+ >>> getLogger('loops.organize.job').addHandler(StreamHandler(sys.stdout))
+
+
+Execute Jobs via a cron Call
+============================
+
+ >>> from zope.publisher.browser import TestRequest
+ >>> from loops.organize.job.browser import Executor
+
+The executor is a view that will be called by calling its ``processJobs``
+method. As we haven't yet defined any job managers nothing happens.
+
+ >>> executor = Executor(loopsRoot, TestRequest())
+ >>> executor.processJobs()
+
+We now register a job manager via an options setting on the loops root object.
+As the corresponding job manager is not yet defined an registered a
+warning is issued.
+
+ >>> loopsRoot.options = ['organize.job.managers:loops_notifier']
+ >>> executor = Executor(loopsRoot, TestRequest())
+ >>> executor.processJobs()
+ Job manager 'loops_notifier' not found.
+
+So let's now define a job manager class and register it as an adapter for
+the loops root object.
+
+ >>> from loops.organize.job.base import JobManager
+ >>> class Notifier(JobManager):
+ ... def process(self):
+ ... print 'processing...'
+
+ >>> component.provideAdapter(Notifier, name='loops_notifier')
+ >>> loopsRoot.options = ['organize.job.managers:loops_notifier']
+ >>> executor = Executor(loopsRoot, TestRequest())
+ >>> executor.processJobs()
+ processing...
+
+
+Fin de partie
+=============
+
+ >>> placefulTearDown()
diff --git a/organize/job/__init__.py b/organize/job/__init__.py
new file mode 100644
index 0000000..38314f3
--- /dev/null
+++ b/organize/job/__init__.py
@@ -0,0 +1,3 @@
+"""
+$Id$
+"""
diff --git a/organize/job/base.py b/organize/job/base.py
new file mode 100644
index 0000000..1737694
--- /dev/null
+++ b/organize/job/base.py
@@ -0,0 +1,40 @@
+#
+# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
+#
+# 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
+#
+
+"""
+Base class(es) for job management.
+
+$Id$
+"""
+
+from zope import component, interface
+
+from cybertools.organize.interfaces import IJobManager
+from loops.interfaces import ILoops
+
+
+class JobManager(object):
+
+ interface.implements(IJobManager)
+ component.adapts(ILoops)
+
+ def __init__(self, context):
+ self.context = context
+
+ def process(self):
+ raise NotImplementedError("Method 'process' has to be implementd by subclass.")
diff --git a/organize/job/browser.py b/organize/job/browser.py
new file mode 100644
index 0000000..d800d51
--- /dev/null
+++ b/organize/job/browser.py
@@ -0,0 +1,56 @@
+#
+# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
+#
+# 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
+#
+
+"""
+Definition of view classes and other browser related stuff for job management.
+
+$Id$
+"""
+
+from logging import getLogger
+from zope import component
+from zope.cachedescriptors.property import Lazy
+
+from cybertools.meta.interfaces import IOptions
+from cybertools.organize.interfaces import IJobManager
+
+
+class Executor(object):
+ """ A view whose processJobs method should be called via cron + wget
+ in order to execute all jobs that are found.
+ """
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ @Lazy
+ def options(self):
+ return IOptions(self.context)
+
+ @Lazy
+ def logger(self):
+ return getLogger('loops.organize.job')
+
+ def processJobs(self):
+ for name in self.options('organize.job.managers', []):
+ manager = component.queryAdapter(self.context, IJobManager, name=name)
+ if manager is None:
+ self.logger.warn("Job manager '%s' not found." % name)
+ else:
+ manager.process()
diff --git a/organize/job/configure.zcml b/organize/job/configure.zcml
new file mode 100644
index 0000000..1572dce
--- /dev/null
+++ b/organize/job/configure.zcml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/organize/job/tests.py b/organize/job/tests.py
new file mode 100755
index 0000000..8a5ea94
--- /dev/null
+++ b/organize/job/tests.py
@@ -0,0 +1,26 @@
+# $Id$
+
+import os
+import unittest, doctest
+from zope.testing.doctestunit import DocFileSuite
+
+
+testDir = os.path.join(os.path.dirname(__file__), 'testdata')
+
+
+class Test(unittest.TestCase):
+ "Basic tests for the loops.organize.job package."
+
+ def testBasics(self):
+ pass
+
+
+def test_suite():
+ flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
+ return unittest.TestSuite((
+ unittest.makeSuite(Test),
+ DocFileSuite('README.txt', optionflags=flags),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')