work in progress: new work item lifecycle

git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@3133 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2009-01-11 10:45:07 +00:00
parent b8997e7184
commit 8bf7ef1ea0
5 changed files with 240 additions and 265 deletions

View file

@ -64,110 +64,3 @@ Service Management
(See cyberapps.tumsm for comprehensive description and tests.)
Work
====
Work items are stored in a tracking storage; in order to conveniently access
the work items we have to provide an adapter to the tracking storage.
>>> from cybertools.tracking.btree import TrackingStorage
>>> from cybertools.organize.interfaces import IWorkItems
>>> from cybertools.organize.work import WorkItem, WorkItems
>>> component.provideAdapter(WorkItems)
The individual work item (a track) is carrying a state attribute that is
governed by a special states definition. We have to register this states
definition as a utility.
>>> from cybertools.organize.work import workItemStates
>>> component.provideUtility(workItemStates(), name='organize.workItemStates')
We are now ready to set up the tracking storage.
>>> tracks = TrackingStorage(trackFactory=WorkItem)
>>> workItems = component.getAdapter(tracks, IWorkItems)
The work management only deals with the IDs or names of tasks and persons,
so we do not have to set up real objects.
>>> wi01 = workItems.add('001', 'john')
>>> wi01
<WorkItem ['001', 1, 'john', '...', 'new']:
{'created': ..., 'creator': 'john'}>
Properties that have not been set explicitly have a default of None; properties
not specified in the IWorkItem interface will lead to an AttributeError.
>>> wi01.description is None
True
>>> wi01.something
Traceback (most recent call last):
...
AttributeError: something
Certain (not all) properties may be set after creation.
>>> wi01.setInitData(planStart=1229955772, planDuration=600, party='annie')
>>> wi01
<WorkItem ['001', 1, 'annie', '2008-12-22 14:22', 'new']:
{'created': ..., 'planEnd': 1229956372, 'planDuration': 600,
'planStart': 1229955772, 'creator': 'john', 'planEffort': 600}>
It's possible to change a value after it has been set as long as the work
item is in state 'new'.
>>> wi01.setInitData(planEffort=700)
>>> wi01.planEffort
700
>>> wi01.setInitData(party='jim')
>>> wi01.userName
'jim'
Change work item state
----------------------
Now Jim accepts the work item, i.e. he wants to work on it. Now the party
that the work item is assigned to may not be changed any more.
>>> wi01.assign()
>>> wi01.state
'assigned'
>>> wi01.setInitData(party='annie')
Traceback (most recent call last):
...
ValueError: Attribute 'party' already set to 'jim'.
Jim now really starts to work. The start time is usually set automatically
but may also be specified explicitly.
>>> wi01.startWork(start=1229958000)
>>> wi01
<WorkItem ['001', 1, 'jim', '2008-12-22 15:00', 'running']:
{'created': ..., 'planEnd': 1229956372, 'start': 1229958000,
'assigned': ..., 'planDuration': 600, 'planStart': 1229955772,
'creator': 'john', 'planEffort': 700}>
Stopping work
-------------
After five minutes of work Jim decides to stop working; but he will
continue work later, so he executes a ``continue`` transition that will
set up a copy of the work item.
He also specifies a new plan start and duration for the new work item.
Plan end and plan effort are given explicitly as None values so that they
won't be taken from the old work item but recalculated.
Note that the work item has already been set to assigned as Jim has
committed himself to continue working on it by selecting the ``continue``
transition.
>>> wi02 = wi01.stopWork('continue', end=1229958300, planStart=1229960000,
... planDuration=400, planEnd=None, planEffort=None)
>>> wi02
<WorkItem ['001', 1, 'jim', '2008-12-22 15:33', 'assigned']:
{'predecessor': '0000001', 'created': ..., 'planEnd': 1229960400,
'assigned': ..., 'planDuration': 400, 'planStart': 1229960000,
'creator': 'jim', 'planEffort': 400}>

View file

@ -426,6 +426,7 @@ class IWorkItem(ITrack):
done by exactly one party (usually a person).
"""
# index attributes
task = Attribute('The task this work item belongs to, identified by '
'its name or ID.')
run = Attribute('Used for recurring tasks: identifies the run '
@ -433,17 +434,9 @@ class IWorkItem(ITrack):
party = Attribute('Whoever does the work, usually a person, identified '
'by its name or ID.')
state = Attribute('The current state the work item is in.')
# standard attributes
title = Attribute('A short text characterizing the work item.')
description = Attribute('A note about what has to be done, and why...')
# optional plan fields; duration (and effort) may be derived from start and end
# all date/time fields are timeStamp values, all duration and effort
# fields are in seconds
planStart = Attribute('When the work should start.')
planEnd = Attribute('When the work should be finished.')
planDuration = Attribute('How long it may take to finish the work.')
planEffort = Attribute('How much effort (time units) it might take '
'to finish the work.')
# real stuff
start = Attribute('When the work was started.')
end = Attribute('When the work was finished.')
duration = Attribute('How long it took to finish the work.')
@ -452,32 +445,14 @@ class IWorkItem(ITrack):
# work item handling
creator = Attribute('The party that has set up the work item.')
created = Attribute('The timeStamp of the initial creation of the work item.')
assigned = Attribute('The timeStamp of the assignment of the work item.')
predecessor = Attribute('Optional: a work item this work item was created from.')
successor = Attribute('Optional: a work item that was created from this one '
'to continue the work.')
newTask = Attribute('Optional: a new task that has been created based '
'on this work item.')
def setInitData(**kw):
def setData(**kw):
""" Set initial work item data (if allowed by the current state);
values not given will be derived if appropriate.
"""
def assign(party):
""" Assign the work item to the party given.
"""
def startWork(**kw):
""" Start working on the work item; properties may be given
as keyword arguments.
"""
def stopWork(transition='finish', **kw):
""" Finish working on the work item; properties may be given
as keyword arguments.
"""
def doAction(action, **kw):
""" Execute an action.

View file

@ -24,6 +24,7 @@ def test_suite():
return unittest.TestSuite((
unittest.makeSuite(TestParty),
DocFileSuite('README.txt', optionflags=flags),
DocFileSuite('work.txt', optionflags=flags),
))
if __name__ == '__main__':

View file

@ -41,22 +41,37 @@ _not_found = object()
@implementer(IStatesDefinition)
def workItemStates():
return StatesDefinition('workItemStates',
State('new', 'new', ('assign', 'cancel',), color='red'),
State('assigned', 'assigned', ('start', 'finish', 'cancel', 'transfer'),
State('new', 'new',
('plan', 'accept', 'start', 'stop', 'finish', 'modify', 'delegate'),
color='red'),
State('planned', 'planned',
('plan', 'accept', 'start', 'stop', 'finish', 'cancel', 'modify'),
color='red'),
State('accepted', 'accepted',
('plan', 'accept', 'start', 'stop', 'finish', 'cancel', 'modify'),
color='yellow'),
State('running', 'running', ('finish', 'continue', 'cancel', 'transfer'),
State('running', 'running',
('stop', 'finish', 'cancel', 'modify'),
color='orange'),
State('finished', 'finished', ('cancel',), color='green'),
State('continued', 'continued', ('finish', 'cancel'), color='blue'),
State('transferred', 'transferred', ('finish', 'cancel'),
color='lightblue'),
State('cancelled', 'cancelled', (), color='grey'),
Transition('assign', 'assign', 'assigned'),
State('stopped', 'stopped',
('plan', 'accept', 'start', 'stop', 'finish', 'cancel', 'modify'),
color='orange'),
State('finished', 'finished',
('plan', 'accept', 'start', 'stop', 'modify', 'close'),
color='green'),
State('cancelled', 'cancelled',
('plan', 'accept', 'start', 'stop', 'modify', 'close'),
color='grey'),
State('closed', 'closed', (), color='lightblue'),
Transition('plan', 'plan', 'planned'),
Transition('accept', 'accept', 'accepted'),
Transition('start', 'start', 'running'),
Transition('stop', 'stop', 'stopped'),
Transition('finish', 'finish', 'finished'),
Transition('continue', 'continue', 'continued'),
Transition('transfer', 'transfer', 'transferred'),
Transition('cancel', 'cancel', 'cancelled'),
Transition('modify', 'modify', 'new'),
Transition('delegate', 'delegate', 'planned'),
Transition('close', 'close', 'closed'),
initialState='new')
@ -72,10 +87,8 @@ class WorkItem(Stateful, Track):
index_attributes = metadata_attributes
typeName = 'WorkItem'
initAttributes = set(['party', 'title', 'description', 'predecessor',
'planStart', 'planEnd', 'planDuration', 'planEffort'])
closeAttributes = set(['end', 'duration', 'effort', 'comment'])
initAttributes = set(['party', 'title', 'description', 'start', 'end',
'duration', 'effort'])
def __init__(self, taskId, runId, userName, data):
super(WorkItem, self).__init__(taskId, runId, userName, data)
@ -83,11 +96,6 @@ class WorkItem(Stateful, Track):
self.data['creator'] = userName
self.data['created'] = self.timeStamp
def __getattr__(self, attr):
if attr not in IWorkItem:
raise AttributeError(attr)
return self.data.get(attr)
def getStatesDefinition(self):
return component.getUtility(IStatesDefinition, name=self.statesDefinition)
@ -99,124 +107,85 @@ class WorkItem(Stateful, Track):
def title(self):
return self.data.get('title') or self.description
def setInitData(self, **kw):
indexChanged = False
updatePlanData = False
for k in kw:
if k not in self.initAttributes:
raise ValueError("Illegal initial attribute: '%s'." % k)
self.checkOverwrite(kw)
@property
def duration(self):
value = self.data.get('duration')
if value is None:
start, end = (self.data.get('start'), self.data.get('end'))
if not None in (start, end):
value = end - start
return value
@property
def effort(self):
value = self.data.get('effort')
if value is None:
return self.duration
def __getattr__(self, attr):
if attr not in IWorkItem:
raise AttributeError(attr)
return self.data.get(attr)
def doAction(self, action, **kw):
if action in self.specialActions:
return self.specialActions[action](self, **kw)
if action not in [t.name for t in self.getAvailableTransitions()]:
raise ValueError("Action '%s' not allowed in state '%s'" %
(action, self.state))
if self.state == 'new':
self.setData(**kw)
self.doTransition(action)
self.reindex('state')
return self
new = self.createNew(action, **kw)
new.doTransition(action)
new.reindex('state')
return new
def modify(self, **kw):
print '*** modifying'
if self.state == 'new':
self.setData(**kw)
return self
def delegate(self, **kw):
print '*** delegating'
def close(self, **kw):
print '*** closing'
specialActions = dict(modify=modify, delegate=delegate, close=close)
def setData(self, **kw):
if self.state != 'new':
raise ValueError("Attributes may only be changed in state 'new'.")
party = kw.pop('party', None)
if party is not None:
self.userName = party
indexChanged = True
self.reindex('userName')
start = kw.get('start')
if start is not None:
self.timeStamp = start
self.reindex('timeStamp')
data = self.data
for k, v in kw.items():
data[k] = v
if k.startswith('plan'):
updatePlanData = True
if self.planStart is not None and self.planStart != self.timeStamp:
self.timeStamp = self.planStart
indexChanged = True
if updatePlanData and self.planStart:
data['planEnd'], data['planDuration'], data['planEffort'] = \
recalcTimes(self.planStart, self.planEnd,
self.planDuration, self.planEffort)
if indexChanged:
self.reindex()
def assign(self, party=None):
self.doTransition('assign')
self.data['assigned'] = getTimeStamp()
if party is not None:
self.userName = party
self.reindex()
def startWork(self, **kw):
self.checkOverwrite(kw)
self.doTransition('start')
start = self.data['start'] = kw.pop('start', None) or getTimeStamp()
self.timeStamp = start
self.reindex()
def stopWork(self, transition='finish', **kw):
self.doTransition(transition)
data = self.data
for k in self.closeAttributes:
v = kw.pop(k, None)
def createNew(self, action, **kw):
newData = {}
for k in self.initAttributes:
v = kw.get(k, _not_found)
if v is _not_found:
v = self.data.get(k)
if v is not None:
data[k] = v
if self.start:
data['end'], data['duration'], data['effort'] = \
recalcTimes(self.start, self.end, self.duration, self.effort)
self.timeStamp = self.end or getTimeStamp()
self.reindex()
if transition in ('continue', 'transfer'):
if transition == 'continue' and kw.get('party') is not None:
raise ValueError("Setting 'party' is not allowed when continuing.")
newData = {}
for k in self.initAttributes:
v = kw.pop(k, _not_found)
if v is _not_found:
v = data.get(k)
if v is not None:
newData[k] = v
if transition == 'transfer' and 'party' not in newData:
raise ValueError("Property 'party' must be set when transferring.")
workItems = IWorkItems(getParent(self))
new = workItems.add(self.taskId, self.userName, self.runId, **newData)
if transition == 'continue':
new.assign()
#new.data['predecessor'] = getName(self)
new.data['predecessor'] = self.__name__
#self.data['successor'] = getName(new)
self.data['successor'] = new.__name__
return new
newData[k] = v
workItems = IWorkItems(getParent(self))
new = workItems.add(self.taskId, self.userName, self.runId, **newData)
return new
# actions
def doAction(self, action, **kw):
# TODO: check if action is allowed
m = getattr(self, 'action_' + action)
m(**kw)
def action_start(self, **kw):
if self.state == 'new':
self.assign(kw.pop('party', None))
self.startWork(**kw)
def action_finish(self, **kw):
for k in ('description', 'title'):
if k in kw:
self.data[k] = kw.pop(k)
if self.state == 'new':
self.assign(kw.pop('party', None))
if self.state == 'assigned':
self.startWork(start=kw.pop('start', None))
self.stopWork(**kw)
# auxiliary methods
def reindex(self):
getParent(self).updateTrack(self, {}) # force reindex
def checkOverwrite(self, kw):
if self.state == 'new':
return
for k, v in kw.items():
old = getattr(self, k, None)
if old is not None and old != v:
raise ValueError("Attribute '%s' already set to '%s'." % (k, old))
def recalcTimes(start, end, duration, effort):
if duration is None and start and end:
duration = end - start
if end is None and start and duration:
end = start + duration
if effort is None and duration:
effort = duration
return end, duration, effort
def reindex(self, idx=None):
getParent(self).indexTrack(None, self, idx)
class WorkItems(object):
@ -245,7 +214,9 @@ class WorkItems(object):
return self.context.query(**criteria)
def add(self, task, party, run=0, **kw):
if not run:
run = self.context.startRun()
trackId = self.context.saveUserTrack(task, run, party, {})
track = self[trackId]
track.setInitData(**kw)
track.setData(**kw)
return track

135
organize/work.txt Normal file
View file

@ -0,0 +1,135 @@
==========================================================
Organizations: Persons, Institutions, Addresses, Work, ...
==========================================================
($Id$)
>>> from zope import component
Basic Work Item Lifecycle
=========================
Work items are stored in a tracking storage; in order to conveniently access
the work items we have to provide an adapter to the tracking storage.
>>> from cybertools.tracking.btree import TrackingStorage
>>> from cybertools.organize.interfaces import IWorkItems
>>> from cybertools.organize.work import WorkItem, WorkItems
>>> component.provideAdapter(WorkItems)
The individual work item (a track) is carrying a state attribute that is
governed by a special states definition. We have to register this states
definition as a utility.
>>> from cybertools.organize.work import workItemStates
>>> component.provideUtility(workItemStates(), name='organize.workItemStates')
We are now ready to set up the tracking storage.
>>> tracks = TrackingStorage(trackFactory=WorkItem)
>>> workItems = component.getAdapter(tracks, IWorkItems)
The work management only deals with the IDs or names of tasks and persons,
so we do not have to set up real objects.
>>> wi01 = workItems.add('001', 'john')
>>> wi01
<WorkItem ['001', 1, 'john', '...', 'new']:
{'created': ..., 'creator': 'john'}>
Properties that have not been set explicitly have a default of None; properties
not specified in the IWorkItem interface will lead to an AttributeError.
>>> wi01.description is None
True
>>> wi01.something
Traceback (most recent call last):
...
AttributeError: something
Properties may be set as long as the work item is in status "new".
>>> wi01.setData(start=1229955772, party='annie')
>>> wi01
<WorkItem ['001', 1, 'annie', '2008-12-22 14:22', 'new']:
{'start': 1229955772, 'created': ..., 'creator': 'john'}>
The duration value is calculated automatically when start and end are set;
the effort is taken from the duration if not set explicitly.
>>> (wi01.end, wi01.duration, wi01.effort)
(None, None, None)
>>> wi01.setData(end=1229956372)
>>> (wi01.end, wi01.duration, wi01.effort)
(1229956372, 600, 600)
>>> wi01.setData(duration=700)
>>> wi01.effort
700
>>> w = wi01.doAction('plan', party='jim')
>>> wi01.userName
'jim'
Change work item state
----------------------
Now Jim accepts the work item, i.e. he wants to work on it.
>>> wi02 = wi01.doAction('accept')
>>> wi01.state
'planned'
>>> wi02
<WorkItem ['001', 1, 'jim', '2008-12-22 14:22', 'accepted']:
{'duration': 700, 'start': 1229955772, 'created': ...,
'end': 1229956372, 'creator': 'jim'}>
It is not possible to change a value of a work item that is not in state "new".
>>> wi01.setData(party='annie')
Traceback (most recent call last):
...
ValueError: Attributes may only be changed in state 'new'.
Jim now really starts to work. The start time is usually set automatically
but may also be specified explicitly.
>>> wi03 = wi02.doAction('start', start=1229958000)
>>> wi03
<WorkItem ['001', 1, 'jim', '2008-12-22 15:00', 'running']:
{'duration': 700, 'start': 1229958000, 'created': ...,
'end': 1229956372, 'creator': 'jim'}>
Stopping and finishing work
---------------------------
After five minutes of work Jim decides to stop working; but he will
continue work later, so he executes a ``stop`` action.
>>> wi04 = wi03.doAction('stop', end=1229958300)
>>> wi04
<WorkItem ['001', 1, 'jim', '2008-12-22 15:00', 'stopped']:
{'duration': 700, 'start': 1229958000, 'created': ...,
'end': 1229958300, 'creator': 'jim'}>
Closing work
------------
Let's now check how many work items have been generated.
>>> len(list(workItems))
4
Delegation of Work Items
========================
Modification of Work Items
==========================
Queries
=======