[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