[Checkins] SVN: lovely.remotetask/trunk/ - ZMI menu to add cron jobs to a task service

Juergen Kartnaller juergen at kartnaller.at
Mon Jul 2 02:37:40 EDT 2007


Log message for revision 77285:
  - ZMI menu to add cron jobs to a task service
  - named detail views can be registered for jobs specific to the task
  - edit view for cron jobs
  - improved ZMI views
  - catch exception if a job was added for which there is no task registered
  - fixed tests to work in all timezones
  
  

Changed:
  U   lovely.remotetask/trunk/CHANGES.txt
  U   lovely.remotetask/trunk/setup.py
  U   lovely.remotetask/trunk/src/lovely/remotetask/README.txt
  U   lovely.remotetask/trunk/src/lovely/remotetask/browser/README.txt
  U   lovely.remotetask/trunk/src/lovely/remotetask/browser/configure.zcml
  A   lovely.remotetask/trunk/src/lovely/remotetask/browser/cronjob.pt
  U   lovely.remotetask/trunk/src/lovely/remotetask/browser/job.py
  U   lovely.remotetask/trunk/src/lovely/remotetask/browser/service.py
  U   lovely.remotetask/trunk/src/lovely/remotetask/interfaces.py
  U   lovely.remotetask/trunk/src/lovely/remotetask/job.py
  U   lovely.remotetask/trunk/src/lovely/remotetask/service.py
  U   lovely.remotetask/trunk/src/lovely/remotetask/task.py

-=-
Modified: lovely.remotetask/trunk/CHANGES.txt
===================================================================
--- lovely.remotetask/trunk/CHANGES.txt	2007-07-02 02:52:13 UTC (rev 77284)
+++ lovely.remotetask/trunk/CHANGES.txt	2007-07-02 06:37:39 UTC (rev 77285)
@@ -2,7 +2,20 @@
 Changes for lovely.remotetask
 =============================
 
+after 0.2.2:
+============
 
+2007/07/02 0.2.2:
+=================
+
+- ZMI menu to add cron jobs to a task service
+- named detail views can be registered for jobs specific to the task
+- edit view for cron jobs
+- improved ZMI views
+- catch exception if a job was added for which there is no task registered
+- fixed tests to work in all timezones
+
+
 2007/06/12 0.2.1:
 =================
 
@@ -15,3 +28,4 @@
 
  - added namespace declaration in lovely/__init__.py
  - allow to delay a job
+

Modified: lovely.remotetask/trunk/setup.py
===================================================================
--- lovely.remotetask/trunk/setup.py	2007-07-02 02:52:13 UTC (rev 77284)
+++ lovely.remotetask/trunk/setup.py	2007-07-02 06:37:39 UTC (rev 77285)
@@ -34,6 +34,7 @@
                         'zope.app.session',
                         'zope.app.xmlrpcintrospection',
                         'zope.component',
+                        'zope.formlib',
                         'zope.interface',
                         'zope.publisher',
                         'zope.schema',

Modified: lovely.remotetask/trunk/src/lovely/remotetask/README.txt
===================================================================
--- lovely.remotetask/trunk/src/lovely/remotetask/README.txt	2007-07-02 02:52:13 UTC (rev 77284)
+++ lovely.remotetask/trunk/src/lovely/remotetask/README.txt	2007-07-02 06:37:39 UTC (rev 77285)
@@ -218,8 +218,8 @@
   >>> import time
   >>> from lovely.remotetask.job import CronJob
   >>> now = 0
-  >>> time.localtime(now)
-  (1970, 1, 1, 1, 0, 0, 3, 1, 0)
+  >>> time.gmtime(now)
+  (1970, 1, 1, 0, 0, 0, 3, 1, 0)
 
 We set up a job to be executed once an hour at the current minute. The next
 call time is the one our from now.
@@ -227,72 +227,72 @@
 Minutes
 
   >>> cronJob = CronJob(-1, u'echo', (), minute=(0, 10))
-  >>> time.localtime(cronJob.timeOfNextCall(0))
-  (1970, 1, 1, 1, 10, 0, 3, 1, 0)
-  >>> time.localtime(cronJob.timeOfNextCall(10*60))
-  (1970, 1, 1, 2, 0, 0, 3, 1, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(0))
+  (1970, 1, 1, 0, 10, 0, 3, 1, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(10*60))
+  (1970, 1, 1, 1, 0, 0, 3, 1, 0)
 
 Hour
 
   >>> cronJob = CronJob(-1, u'echo', (), hour=(2, 13))
-  >>> time.localtime(cronJob.timeOfNextCall(0))
+  >>> time.gmtime(cronJob.timeOfNextCall(0))
   (1970, 1, 1, 2, 0, 0, 3, 1, 0)
-  >>> time.localtime(cronJob.timeOfNextCall(2*60*60))
+  >>> time.gmtime(cronJob.timeOfNextCall(2*60*60))
   (1970, 1, 1, 13, 0, 0, 3, 1, 0)
 
 Month
 
   >>> cronJob = CronJob(-1, u'echo', (), month=(1, 5, 12))
-  >>> time.localtime(cronJob.timeOfNextCall(0))
-  (1970, 5, 1, 1, 0, 0, 4, 121, 0)
-  >>> time.localtime(cronJob.timeOfNextCall(cronJob.timeOfNextCall(0)))
-  (1970, 12, 1, 1, 0, 0, 1, 335, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(0))
+  (1970, 5, 1, 0, 0, 0, 4, 121, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(cronJob.timeOfNextCall(0)))
+  (1970, 12, 1, 0, 0, 0, 1, 335, 0)
 
 Day of week [0..6], jan 1 1970 is a wednesday.
 
   >>> cronJob = CronJob(-1, u'echo', (), dayOfWeek=(0, 2, 4, 5))
-  >>> time.localtime(cronJob.timeOfNextCall(0))
-  (1970, 1, 2, 1, 0, 0, 4, 2, 0)
-  >>> time.localtime(cronJob.timeOfNextCall(60*60*24))
-  (1970, 1, 3, 1, 0, 0, 5, 3, 0)
-  >>> time.localtime(cronJob.timeOfNextCall(2*60*60*24))
-  (1970, 1, 5, 1, 0, 0, 0, 5, 0)
-  >>> time.localtime(cronJob.timeOfNextCall(4*60*60*24))
-  (1970, 1, 7, 1, 0, 0, 2, 7, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(0))
+  (1970, 1, 2, 0, 0, 0, 4, 2, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(60*60*24))
+  (1970, 1, 3, 0, 0, 0, 5, 3, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(2*60*60*24))
+  (1970, 1, 5, 0, 0, 0, 0, 5, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(4*60*60*24))
+  (1970, 1, 7, 0, 0, 0, 2, 7, 0)
 
 DayOfMonth [1..31]
 
   >>> cronJob = CronJob(-1, u'echo', (), dayOfMonth=(1, 12, 21, 30))
-  >>> time.localtime(cronJob.timeOfNextCall(0))
-  (1970, 1, 12, 1, 0, 0, 0, 12, 0)
-  >>> time.localtime(cronJob.timeOfNextCall(12*24*60*60))
-  (1970, 1, 21, 1, 0, 0, 2, 21, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(0))
+  (1970, 1, 12, 0, 0, 0, 0, 12, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(12*24*60*60))
+  (1970, 1, 21, 0, 0, 0, 2, 21, 0)
 
 Combined
 
   >>> cronJob = CronJob(-1, u'echo', (), minute=(10,),
   ...                                 dayOfMonth=(1, 12, 21, 30))
-  >>> time.localtime(cronJob.timeOfNextCall(0))
+  >>> time.gmtime(cronJob.timeOfNextCall(0))
+  (1970, 1, 1, 0, 10, 0, 3, 1, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(10*60))
   (1970, 1, 1, 1, 10, 0, 3, 1, 0)
-  >>> time.localtime(cronJob.timeOfNextCall(10*60))
-  (1970, 1, 1, 2, 10, 0, 3, 1, 0)
 
   >>> cronJob = CronJob(-1, u'echo', (), minute=(10,),
   ...                                 hour=(4,),
   ...                                 dayOfMonth=(1, 12, 21, 30))
-  >>> time.localtime(cronJob.timeOfNextCall(0))
+  >>> time.gmtime(cronJob.timeOfNextCall(0))
   (1970, 1, 1, 4, 10, 0, 3, 1, 0)
-  >>> time.localtime(cronJob.timeOfNextCall(10*60))
+  >>> time.gmtime(cronJob.timeOfNextCall(10*60))
   (1970, 1, 1, 4, 10, 0, 3, 1, 0)
 
 
 A cron job can also be used to delay the execution of a job.
 
   >>> cronJob = CronJob(-1, u'echo', (), delay=10,)
-  >>> time.localtime(cronJob.timeOfNextCall(0))
-  (1970, 1, 1, 1, 0, 10, 3, 1, 0)
-  >>> time.localtime(cronJob.timeOfNextCall(1))
-  (1970, 1, 1, 1, 0, 11, 3, 1, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(0))
+  (1970, 1, 1, 0, 0, 10, 3, 1, 0)
+  >>> time.gmtime(cronJob.timeOfNextCall(1))
+  (1970, 1, 1, 0, 0, 11, 3, 1, 0)
 
 
 Creating Delayed Jobs
@@ -371,3 +371,24 @@
   >>> service.getResult(jobid)
   2
 
+A job can be rescheduled.
+
+  >>> job = service.jobs[jobid]
+  >>> job.update(minute = (11, 13))
+
+After the update the job must be rescheduled in the service.
+
+  >>> service.reschedule(jobid)
+
+Now the job is not executed at the old registration minute which was 10.
+
+  >>> service.process(10*60+60*60)
+  >>> service.getResult(jobid)
+  2
+
+But it executes at the new minute which is set to 11.
+
+  >>> service.process(11*60+60*60)
+  >>> service.getResult(jobid)
+  3
+

Modified: lovely.remotetask/trunk/src/lovely/remotetask/browser/README.txt
===================================================================
--- lovely.remotetask/trunk/src/lovely/remotetask/browser/README.txt	2007-07-02 02:52:13 UTC (rev 77284)
+++ lovely.remotetask/trunk/src/lovely/remotetask/browser/README.txt	2007-07-02 06:37:39 UTC (rev 77285)
@@ -89,6 +89,34 @@
   </tbody>
   ...
 
+It is possible to provide custom views for the details. Note the name of the
+view "echo_detail", it consists of the task name and "_detail". This allows us
+to use different detail views on the same job classes. if no such view is
+found a view with name 'detail' is searched.
+
+  >>> from zope import interface
+  >>> from zope.publisher.interfaces.browser import IBrowserView
+  >>> class EchoDetailView(object):
+  ...     interface.implements(IBrowserView)
+  ...     def __init__(self, context, request):
+  ...         self.context = context
+  ...         self.request = request
+  ...     def __call__(self):
+  ...         return u'echo: foo=%s'% self.context.input['foo']
+  >>> from lovely.remotetask.interfaces import IJob
+  >>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+  >>> from zope import component
+  >>> component.provideAdapter(EchoDetailView,
+  ...                          (IJob, IDefaultBrowserLayer),
+  ...                          name='echo_detail')
+  >>> browser.reload()
+  >>> print browser.contents
+  <!DOCTYPE
+  ...
+  <td class="tableDetail">
+    echo: foo=bar
+  ...
+
 You can cancel scheduled jobs:
 
   >>> browser.getControl('Cancel').click()
@@ -164,7 +192,7 @@
   >>> service.getStatus(jobid)
   'error'
 
-We do the same again to see if the same thin happens again. This test is
+We do the same again to see if the same thing happens again. This test is
 necessary to see if the internal runCount in the task service is reset.
 
   >>> io.seek(0)

Modified: lovely.remotetask/trunk/src/lovely/remotetask/browser/configure.zcml
===================================================================
--- lovely.remotetask/trunk/src/lovely/remotetask/browser/configure.zcml	2007-07-02 02:52:13 UTC (rev 77284)
+++ lovely.remotetask/trunk/src/lovely/remotetask/browser/configure.zcml	2007-07-02 06:37:39 UTC (rev 77285)
@@ -1,5 +1,6 @@
 <configure
-    xmlns="http://namespaces.zope.org/browser">
+    xmlns="http://namespaces.zope.org/browser"
+    xmlns:zope="http://namespaces.zope.org/zope">
 
   <resourceDirectory
       name="lovely-remotetask-icons"
@@ -22,10 +23,42 @@
       />
 
   <page
+      name="detail"
       for="..interfaces.IJob"
-      name="detail"
       permission="zope.ManageContent"
       class=".job.JobDetail"
       />
 
+  <page
+      name="detail"
+      for="..interfaces.ICronJob"
+      permission="zope.Public"
+      class=".job.CronJobDetail"
+      />
+
+  <page
+      name="editjob"
+      for="..interfaces.ICronJob"
+      permission="zope.ManageContent"
+      class=".job.CronJobEdit"
+      menu="zmi_views" title="edit"
+      />
+
+  <page
+      name="addcronjob.html"
+      for="..interfaces.ITaskService"
+      class=".job.AddCronJob"
+      permission="zope.ManageContent"
+      menu="zmi_views" title="add cron"
+      />
+
+  <!-- traverser for the site -->
+  <zope:view
+      for="..interfaces.ITaskService"
+      type="zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.publisher.interfaces.browser.IBrowserPublisher"
+      factory=".service.ServiceJobTraverser"
+      permission="zope.Public"
+      />
+
 </configure>

Added: lovely.remotetask/trunk/src/lovely/remotetask/browser/cronjob.pt
===================================================================
--- lovely.remotetask/trunk/src/lovely/remotetask/browser/cronjob.pt	                        (rev 0)
+++ lovely.remotetask/trunk/src/lovely/remotetask/browser/cronjob.pt	2007-07-02 06:37:39 UTC (rev 77285)
@@ -0,0 +1,9 @@
+<div metal:use-macro="view/base_template/macros/main" >
+  <div metal:fill-slot="above_buttons"
+       tal:define="inputForm view/inputForm | nothing"
+       tal:condition="inputForm">
+    <p>Job input parameter</p>
+    <div tal:replace="structure inputForm" /><hr/>
+  </div>
+</div>
+


Property changes on: lovely.remotetask/trunk/src/lovely/remotetask/browser/cronjob.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: lovely.remotetask/trunk/src/lovely/remotetask/browser/job.py
===================================================================
--- lovely.remotetask/trunk/src/lovely/remotetask/browser/job.py	2007-07-02 02:52:13 UTC (rev 77284)
+++ lovely.remotetask/trunk/src/lovely/remotetask/browser/job.py	2007-07-02 06:37:39 UTC (rev 77285)
@@ -17,11 +17,181 @@
 """
 __docformat__ = 'restructuredtext'
 
+from datetime import datetime
+
+from zope import interface
+from zope import component
+from zope import schema
+
+from zope import formlib
+
+from zope.traversing.browser.absoluteurl import absoluteURL
 from zope.publisher.browser import BrowserPage
 
+from zope.app.pagetemplate import ViewPageTemplateFile
+from zope.app.form.browser.textwidgets import TextWidget
 
+from lovely.remotetask.interfaces import CRONJOB, ICronJob
+
+
+def noValidation(self, *args, **kwargs):
+    return ()
+
+
 class JobDetail(BrowserPage):
     """A simple task input detail view."""
 
     def __call__(self):
         return u'No input detail available'
+
+
+class CronJobDetail(BrowserPage):
+    """A simple task input detail view."""
+
+    def __call__(self):
+        job = self.context
+        if job.scheduledFor is None:
+            return u'not yet scheduled'
+        if job.status == CRONJOB:
+            dformat = self.request.locale.dates.getFormatter('dateTime',
+                                                             'short')
+        else:
+            dformat = self.request.locale.dates.getFormatter('dateTime',
+                                                             'medium')
+        return u'Scheduled for %s'% dformat.format(job.scheduledFor)
+
+
+class StringTupleWidget(TextWidget):
+
+    def _toFormValue(self, input):
+        if not input:
+            return u''
+        return u' '.join([str(v) for v in input])
+
+    def _toFieldValue(self, input):
+        if self.convert_missing_value and input == self._missing:
+            value = self.context.missing_value
+        else:
+            value = tuple([int(v) for v in input.split()])
+        return value
+
+
+class CronJobEdit(formlib.form.EditForm):
+    """An edit view for cron jobs."""
+
+    form_fields = formlib.form.Fields(ICronJob).select(
+            'task',
+            'hour',
+            'minute',
+            'dayOfMonth',
+            'month',
+            'dayOfWeek',
+            'delay',
+            )
+    form_fields['hour'].custom_widget = StringTupleWidget
+    form_fields['minute'].custom_widget = StringTupleWidget
+    form_fields['dayOfMonth'].custom_widget = StringTupleWidget
+    form_fields['month'].custom_widget = StringTupleWidget
+    form_fields['dayOfWeek'].custom_widget = StringTupleWidget
+
+    inputForm = None
+
+    base_template = formlib.form.EditForm.template
+    template = ViewPageTemplateFile('cronjob.pt')
+
+    def setUpWidgets(self, ignore_request=False):
+        jobtask = component.queryUtility(self.context.__parent__.taskInterface,
+                                       name=self.context.task)
+        if (    jobtask is not None
+            and hasattr(jobtask, 'inputSchema')
+            and jobtask.inputSchema is not interface.Interface
+           ):
+            subform = InputSchemaForm(context=self.context,
+                                      request=self.request,
+                                     )
+            subform.prefix = 'taskinput'
+            subform.form_fields = formlib.form.Fields(jobtask.inputSchema)
+            self.inputForm = subform
+        super(CronJobEdit, self).setUpWidgets(ignore_request=ignore_request)
+
+    @formlib.form.action(u'Apply')
+    def handle_apply_action(self, action, data):
+        inputData = None
+        if self.inputForm is not None:
+            self.inputForm.update()
+            inputData = {}
+            errors = formlib.form.getWidgetsData(self.inputForm.widgets,
+                                                 self.inputForm.prefix,
+                                                 inputData)
+            if len(inputData) == 0:
+                inputData = None
+        self.context.task = data['task']
+        self.context.update(
+                hour = data['hour'],
+                minute = data['minute'],
+                dayOfMonth = data['dayOfMonth'],
+                month = data['month'],
+                dayOfWeek = data['dayOfWeek'],
+                delay = data['delay'],
+                )
+        self.context.__parent__.reschedule(self.context.id)
+
+    @formlib.form.action(u'Cancel', validator=noValidation)
+    def handle_cancel_action(self, action, data):
+        self.request.response.redirect(self.nextURL())
+
+    def nextURL(self):
+        return '%s/@@jobs.html'% absoluteURL(self.context.__parent__,
+                                             self.request)
+
+
+class AddCronJob(formlib.form.Form):
+    """An edit view for cron jobs."""
+
+    form_fields = formlib.form.Fields(
+            ICronJob,
+            ).select(
+                'task',
+                'hour',
+                'minute',
+                'dayOfMonth',
+                'month',
+                'dayOfWeek',
+                'delay',
+                )
+    form_fields['hour'].custom_widget = StringTupleWidget
+    form_fields['minute'].custom_widget = StringTupleWidget
+    form_fields['dayOfMonth'].custom_widget = StringTupleWidget
+    form_fields['month'].custom_widget = StringTupleWidget
+    form_fields['dayOfWeek'].custom_widget = StringTupleWidget
+
+    base_template = formlib.form.EditForm.template
+    template = ViewPageTemplateFile('cronjob.pt')
+
+    @formlib.form.action(u'Add')
+    def handle_add_action(self, action, data):
+        self.context.addCronJob(
+                task = data['task'],
+                hour = data['hour'],
+                minute = data['minute'],
+                dayOfMonth = data['dayOfMonth'],
+                month = data['month'],
+                dayOfWeek = data['dayOfWeek'],
+                delay = data['delay'],
+                )
+
+    @formlib.form.action(u'Cancel', validator=noValidation)
+    def handle_cancel_action(self, action, data):
+        self.request.response.redirect(self.nextURL())
+
+    def nextURL(self):
+        return '%s/@@jobs.html'% absoluteURL(self.context,
+                                             self.request)
+
+
+class InputSchemaForm(formlib.form.AddForm):
+    """An editor for input data of a job"""
+    interface.implements(formlib.interfaces.ISubPageForm)
+    template = formlib.namedtemplate.NamedTemplate('default')
+    actions = []
+

Modified: lovely.remotetask/trunk/src/lovely/remotetask/browser/service.py
===================================================================
--- lovely.remotetask/trunk/src/lovely/remotetask/browser/service.py	2007-07-02 02:52:13 UTC (rev 77284)
+++ lovely.remotetask/trunk/src/lovely/remotetask/browser/service.py	2007-07-02 06:37:39 UTC (rev 77285)
@@ -21,8 +21,14 @@
 
 import zope.interface
 import zope.component
+
+from zope.traversing.browser.absoluteurl import absoluteURL
+from zope.security.proxy import removeSecurityProxy
+
 from zope.app.pagetemplate import ViewPageTemplateFile
 from zope.app.session.interfaces import ISession
+from zope.app.container.contained import contained
+
 from zope.publisher.browser import BrowserPage
 from zc.table import column, table
 from zc.table.interfaces import ISortableColumn
@@ -46,7 +52,7 @@
         passed in batch size, since we could be at the end of the list and
         there are not enough elements left to fill the batch completely."""
 
-    def __iter__(): 
+    def __iter__():
         """Creates an iterator for the contents of the batch (not the entire
         list)."""
 
@@ -55,7 +61,7 @@
 
     def nextBatch(self):
         """Return the next batch. If there is no next batch, return None."""
-    
+
     def prevBatch(self):
         """Return the previous batch. If there is no previous batch, return
         None."""
@@ -102,7 +108,7 @@
             raise IndexError('batch index out of range')
         return self.list[self.start+key]
 
-    def __iter__(self): 
+    def __iter__(self):
         return iter(self.list[self.start:self.end+1])
 
     def __contains__(self, item):
@@ -113,7 +119,7 @@
         if start >= len(self.list):
             return None
         return Batch(self.list, start, self.size)
-    
+
     def prevBatch(self):
         start = self.start - self.size
         if start < 0:
@@ -144,14 +150,30 @@
         return widget %item.id
 
 
+class TaskNameColumn(column.Column):
+    """Provide a column for the task name and provide a link to an edit page
+    is one is available."""
+
+    def renderCell(self, item, formatter):
+        view = zope.component.queryMultiAdapter((item, formatter.request),
+                                                name='editjob')
+        if view:
+            url = absoluteURL(formatter.context, formatter.request)
+            return '<a href="%s/%s/editjob">%s</a>'% (
+                                                url, item.id, item.task)
+        else:
+            return item.task
+
+
 class JobDetailColumn(column.Column):
     """Provide a column of taks input detail view."""
 
     def renderCell(self, item, formatter):
-        if not item.input:
-            return u'No input data given.'
-        view = zope.component.getMultiAdapter((item, formatter.request), 
-            name='detail')
+        view = zope.component.queryMultiAdapter((item, formatter.request),
+                                              name='%s_detail'% item.task)
+        if view is None:
+            view = zope.component.getMultiAdapter((item, formatter.request),
+                                                  name='detail')
         return view()
 
 
@@ -173,13 +195,13 @@
     def renderCell(self, item, formatter):
         date = self.getter(item, formatter)
         dformat = formatter.request.locale.dates.getFormatter(
-            'dateTime', 'short')
+            'dateTime', 'medium')
         return date and dformat.format(date) or '[not set]'
 
     def getSortKey(self, item, formatter):
         return self.getter(item, formatter)
 
-class ListFormatter(table.SortingFormatterMixin, 
+class ListFormatter(table.SortingFormatterMixin,
     table.AlternatingRowFormatter):
     """Provides a width for each column."""
 
@@ -265,7 +287,7 @@
     columns = (
         CheckboxColumn(u'Sel'),
         column.GetterColumn(u'Id', lambda x, f: str(x.id), name='id'),
-        column.GetterColumn(u'Task', lambda x, f: x.task, name='task'),
+        TaskNameColumn(u'Task', name='task'),
         StatusColumn(u'Status', lambda x, f: x.status, name='status'),
         JobDetailColumn(u'Detail', name='detail'),
         DatetimeColumn(u'Creation',
@@ -280,7 +302,7 @@
         formatter = ListFormatter(
             self.context, self.request, self.jobs(),
             prefix='zc.table', columns=self.columns)
-        formatter.widths=[25, 50, 150, 75, 250, 100, 100, 100]
+        formatter.widths=[25, 50, 100, 75, 250, 120, 120, 120]
         formatter.cssClasses['table'] = 'list'
         formatter.columnCSS['id'] = 'tableId'
         formatter.columnCSS['task'] = 'tableTask'
@@ -350,3 +372,26 @@
     def __call__(self):
         self.update()
         return self.template()
+
+from zope.publisher.interfaces import IPublishTraverse
+
+class ServiceJobTraverser(object):
+    zope.interface.implements(IPublishTraverse)
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def publishTraverse(self, request, name):
+        try:
+            job = removeSecurityProxy(self.context.jobs[int(name)])
+            # we provide a location proxy
+            return contained(job, self.context, name)
+        except (KeyError, ValueError):
+            pass
+        view = zope.component.queryMultiAdapter((self.context, request),
+                                                name=name)
+        if view is not None:
+            return view
+        raise NotFound(self.context, name, request)
+

Modified: lovely.remotetask/trunk/src/lovely/remotetask/interfaces.py
===================================================================
--- lovely.remotetask/trunk/src/lovely/remotetask/interfaces.py	2007-07-02 02:52:13 UTC (rev 77284)
+++ lovely.remotetask/trunk/src/lovely/remotetask/interfaces.py	2007-07-02 06:37:39 UTC (rev 77285)
@@ -18,6 +18,7 @@
 __docformat__ = 'restructuredtext'
 from zope import interface
 from zope import schema
+from zope.configuration import fields
 from zope.app.container.interfaces import IContained
 
 QUEUED = 'queued'
@@ -28,6 +29,37 @@
 DELAYED = 'delayed'
 CRONJOB = 'cronjob'
 
+
+class ITask(interface.Interface):
+    """A task available in the task service"""
+
+    inputSchema = schema.Object(
+        title=u'Input Schema',
+        description=u'A schema describing the task input signature.',
+        schema=interface.Interface,
+        required=False)
+
+    outputSchema = schema.Object(
+        title=u'Output Schema',
+        description=u'A schema describing the task output signature.',
+        schema=interface.Interface,
+        required=False)
+
+    def __call__(self, service, jobid, input):
+        """Execute the task.
+
+        The ``service`` argument is the task service object. It allows access to
+        service wide data and the system as a whole.
+
+        Tasks do not live in a vacuum, but are tightly coupled to the job
+        executing it. The ``jobid`` argument provides the id of the job being
+        processed.
+
+        The ``input`` object must conform to the input schema (if
+        specified). The return value must conform to the output schema.
+        """
+
+
 class ITaskService(IContained):
     """A service for managing and executing long-running, remote tasks."""
 
@@ -36,6 +68,12 @@
         description=u'A mapping of all jobs by job id.',
         schema=interface.common.mapping.IMapping)
 
+    taskInterface = fields.GlobalInterface(
+            title = u'Task Interface',
+            description = u'The interface to lookup task utilities',
+            default = ITask,
+            )
+
     def getAvailableTasks():
         """Return a mapping of task name to the task."""
 
@@ -55,6 +93,12 @@
                   ):
         """Add a new cron job."""
 
+    def reschedule(jobid):
+        """Rescheudle a cron job.
+
+        This is neccessary if the cron jobs parameters are changed.
+        """
+
     def clean(stati=[CANCELLED, ERROR, COMPLETED]):
         """removes all jobs which are completed or canceled or have errors."""
 
@@ -95,36 +139,6 @@
         """
 
 
-class ITask(interface.Interface):
-    """A task available in the task service"""
-
-    inputSchema = schema.Object(
-        title=u'Input Schema',
-        description=u'A schema describing the task input signature.',
-        schema=interface.Interface,
-        required=False)
-
-    outputSchema = schema.Object(
-        title=u'Output Schema',
-        description=u'A schema describing the task output signature.',
-        schema=interface.Interface,
-        required=False)
-
-    def __call__(self, service, jobid, input):
-        """Execute the task.
-
-        The ``service`` argument is the task service object. It allows access to
-        service wide data and the system as a whole.
-
-        Tasks do not live in a vacuum, but are tightly coupled to the job
-        executing it. The ``jobid`` argument provides the id of the job being
-        processed.
-
-        The ``input`` object must conform to the input schema (if
-        specified). The return value must conform to the output schema.
-        """
-
-
 class IJob(interface.Interface):
     """An internal job object."""
 
@@ -212,7 +226,31 @@
             required=False
             )
 
+    delay = schema.Int(
+            title=u'delay',
+            default=0,
+            required=False
+            )
 
+    scheduledFor = schema.Datetime(
+            title=u'scheduled',
+            default=None,
+            required=False
+            )
+
+    def update(minute, hour, dayOfMonth, month, dayOfWeek, delay):
+        """Update the cron job.
+
+        The job must be rescheduled in the containing service.
+        """
+
+    def timeOfNextCall(self, now=None):
+        """Calculate the time for the next call of the job.
+
+        now is a convenience parameter for testing.
+        """
+
+
 class IStartRemoteTasksEvent(interface.Interface):
     """Event to start the Remote Tasks"""
 

Modified: lovely.remotetask/trunk/src/lovely/remotetask/job.py
===================================================================
--- lovely.remotetask/trunk/src/lovely/remotetask/job.py	2007-07-02 02:52:13 UTC (rev 77284)
+++ lovely.remotetask/trunk/src/lovely/remotetask/job.py	2007-07-02 06:37:39 UTC (rev 77285)
@@ -20,8 +20,11 @@
 import time
 import datetime
 import persistent
+
 import zope.interface
+
 from zope.schema.fieldproperty import FieldProperty
+
 from lovely.remotetask import interfaces
 
 
@@ -58,6 +61,7 @@
     dayOfMonth = FieldProperty(interfaces.ICronJob['dayOfMonth'])
     month = FieldProperty(interfaces.ICronJob['month'])
     dayOfWeek = FieldProperty(interfaces.ICronJob['dayOfWeek'])
+    scheduledFor = FieldProperty(interfaces.ICronJob['scheduledFor'])
 
     def __init__(self, id, task, input,
                  minute=(),
@@ -68,22 +72,34 @@
                  delay=None,
                 ):
         super(CronJob, self).__init__(id, task, input)
+        self.update(minute, hour, dayOfMonth, month, dayOfWeek, delay)
+
+    def update(self,
+               minute=(),
+               hour=(),
+               dayOfMonth=(),
+               month=(),
+               dayOfWeek=(),
+               delay=None,
+               ):
         self.minute = minute
         self.hour = hour
         self.dayOfMonth = dayOfMonth
         self.month = month
         self.dayOfWeek = dayOfWeek
+        if delay == 0:
+            delay = None
         self.delay = delay
 
     def timeOfNextCall(self, now=None):
         if now is None:
-            now = time.time()
+            now = int(time.time())
         next = now
         if self.delay is not None:
             next += self.delay
             return int(next)
         inc = lambda t: 60
-        lnow = list(time.localtime(now)[:5])
+        lnow = list(time.gmtime(now)[:5])
         if self.minute:
             pass
         elif self.hour:
@@ -98,10 +114,10 @@
         elif self.month:
             mlen = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
             def minc(t):
-                m = time.localtime(t)[1] - 1
+                m = time.gmtime(t)[1] - 1
                 if m == 1:
                     # see if we have a leap year
-                    y = time.localtime(t)[0]
+                    y = time.gmtime(t)[0]
                     if y % 4 != 0:
                         d = 28
                     elif y % 400 == 0:
@@ -119,7 +135,7 @@
             lnow.append(0)
         while next <= now+365*24*60*60:
             next += inc(next)
-            fields = time.localtime(next)
+            fields = time.gmtime(next)
             if ((self.month and fields[1] not in self.month) or
                 (self.dayOfMonth and fields[2] not in self.dayOfMonth) or
                 (self.dayOfWeek and fields[6] % 7 not in self.dayOfWeek) or

Modified: lovely.remotetask/trunk/src/lovely/remotetask/service.py
===================================================================
--- lovely.remotetask/trunk/src/lovely/remotetask/service.py	2007-07-02 02:52:13 UTC (rev 77284)
+++ lovely.remotetask/trunk/src/lovely/remotetask/service.py	2007-07-02 06:37:39 UTC (rev 77285)
@@ -27,7 +27,6 @@
 import zope.publisher.base
 import zope.publisher.publish
 from BTrees.IOBTree import IOBTree
-from BTrees.IFBTree import IFTreeSet
 from zope import component
 from zope.app import zapi
 from zope.app.appsetup.product import getProductConfiguration
@@ -35,6 +34,7 @@
 from zope.app.publication.zopepublication import ZopePublication
 from zope.security.proxy import removeSecurityProxy
 from zope.traversing.api import traverse
+from zope.component.interfaces import ComponentLookupError
 from lovely.remotetask import interfaces, job, task
 
 log = logging.getLogger('lovely.remotetask')
@@ -91,13 +91,16 @@
         newjob = job.CronJob(jobid, task, input,
                 minute, hour, dayOfMonth, month, dayOfWeek, delay)
         self.jobs[jobid] = newjob
-        self._scheduledQueue.put(newjob)
         if delay is None:
             newjob.status = interfaces.CRONJOB
         else:
             newjob.status = interfaces.DELAYED
+        self._scheduledQueue.put(newjob)
         return jobid
 
+    def reschedule(self, jobid):
+        self._scheduledQueue.put(self.jobs[jobid])
+
     def clean(self, stati=[interfaces.CANCELLED, interfaces.ERROR,
                            interfaces.COMPLETED]):
         """See interfaces.ITaskService"""
@@ -174,7 +177,15 @@
         job = self._pullJob(now)
         if job is None:
             return False
-        jobtask = component.getUtility(self.taskInterface, name=job.task)
+        try:
+            jobtask = component.getUtility(self.taskInterface, name=job.task)
+        except ComponentLookupError, error:
+            log.error('Task "%s" not found!'% job.task)
+            log.exception(error)
+            job.error = error
+            if job.status != interfaces.CRONJOB:
+                job.status = interfaces.ERROR
+            return True
         job.started = datetime.datetime.now()
         if not hasattr(storage, 'runCount'):
             storage.runCount = 0
@@ -210,7 +221,7 @@
         # first move new cron jobs from the scheduled queue into the cronjob
         # list
         if now is None:
-            now = time.time()
+            now = int(time.time())
         while len(self._scheduledQueue)>0:
             job = self._scheduledQueue.pull()
             if job.status is not interfaces.CANCELLED:
@@ -239,7 +250,17 @@
         return None
 
     def _insertCronJob(self, job, now):
+        for callTime, scheduled in list(self._scheduledJobs.items()):
+            if job in scheduled:
+                scheduled = list(scheduled)
+                scheduled.remove(job)
+                if len(scheduled) == 0:
+                    del self._scheduledJobs[callTime]
+                else:
+                    self._scheduledJobs[callTime] = tuple(scheduled)
+                break
         nextCallTime = job.timeOfNextCall(now)
+        job.scheduledFor = datetime.datetime.fromtimestamp(nextCallTime)
         set = self._scheduledJobs.get(nextCallTime)
         if set is None:
             self._scheduledJobs[nextCallTime] = ()
@@ -280,17 +301,17 @@
     serviceNames = []
     config = getProductConfiguration('lovely.remotetask')
     if config is not None:
-        serviceNames = [name.strip() 
+        serviceNames = [name.strip()
                         for name in config.get('autostart', '').split(',')]
     return serviceNames
 
 
 def bootStrapSubscriber(event):
-    """Start the queue processing services based on the 
+    """Start the queue processing services based on the
        settings in zope.conf"""
-    
+
     serviceNames = getAutostartServiceNames()
-    
+
     db = event.database
     connection = db.open()
     root = connection.root()
@@ -303,15 +324,15 @@
                                   for name in serviceNames if name]:
         site = root_folder.get(siteName)
         if site is not None:
-            service = component.queryUtility(interfaces.ITaskService, 
-                                           context=site, 
+            service = component.queryUtility(interfaces.ITaskService,
+                                           context=site,
                                            name=serviceName)
             if service is not None and not service.isProcessing():
                 service.startProcessing()
                 log.info('service %s on site %s started' % (serviceName,
                                                             siteName))
             else:
-                log.error('service %s on site %s not found' % (serviceName, 
+                log.error('service %s on site %s not found' % (serviceName,
                                                                siteName))
         else:
             log.error('site %s not found' % siteName)

Modified: lovely.remotetask/trunk/src/lovely/remotetask/task.py
===================================================================
--- lovely.remotetask/trunk/src/lovely/remotetask/task.py	2007-07-02 02:52:13 UTC (rev 77284)
+++ lovely.remotetask/trunk/src/lovely/remotetask/task.py	2007-07-02 06:37:39 UTC (rev 77285)
@@ -18,13 +18,17 @@
 __docformat__ = 'restructuredtext'
 
 import zope.interface
+
 from zope.schema.fieldproperty import FieldProperty
+
 from lovely.remotetask import interfaces
 
+
 class TaskError(Exception):
     """An error occurred while executing the task."""
     pass
 
+
 class SimpleTask(object):
     """A simple, non-persistent task implementation."""
     zope.interface.implements(interfaces.ITask)



More information about the Checkins mailing list