cybertools/organize/work.py

350 lines
12 KiB
Python

#
# Copyright (c) 2011 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
#
"""
Planning and recording activities (work items).
$Id$
"""
from zope import component
from zope.component import adapts
from zope.interface import implementer, implements
from zope.traversing.api import getName, getParent
from cybertools.organize.interfaces import IWorkItem, IWorkItems
from cybertools.stateful.base import Stateful
from cybertools.stateful.definition import StatesDefinition
from cybertools.stateful.definition import State, Transition
from cybertools.stateful.interfaces import IStatesDefinition
from cybertools.tracking.btree import Track, getTimeStamp
from cybertools.tracking.interfaces import ITrackingStorage
_not_found = object()
@implementer(IStatesDefinition)
def workItemStates():
return StatesDefinition('workItemStates',
State('new', 'new',
('plan', 'accept', 'start', 'work', 'finish', 'delegate', 'cancel'),
color='red'),
State('planned', 'planned',
('plan', 'accept', 'start', 'work', 'finish', 'delegate',
'move', 'cancel', 'modify'), color='red'),
State('accepted', 'accepted',
('plan', 'accept', 'start', 'work', 'finish',
'move', 'cancel', 'modify'),
color='yellow'),
State('running', 'running',
('work', 'finish', 'move', 'cancel', 'modify'),
color='orange'),
State('done', 'done',
('plan', 'accept', 'start', 'work', 'finish', 'delegate',
'move', 'cancel', 'modify'), color='lightgreen'),
State('finished', 'finished',
('plan', 'accept', 'start', 'work', 'finish',
'move', 'modify', 'close'),
color='green'),
State('cancelled', 'cancelled',
('plan', 'accept', 'start', 'work', 'move', 'modify', 'close'),
color='grey'),
State('closed', 'closed', (), color='lightblue'),
# not directly reachable states:
State('delegated', 'delegated',
('plan', 'accept', 'start', 'work', 'finish', 'close', 'delegate',
'move', 'cancel', 'modify'),
color='purple'),
State('delegated_x', 'delegated', (), color='purple'),
State('moved', 'moved',
('plan', 'accept', 'start', 'work', 'finish', 'close', 'delegate',
'move', 'cancel', 'modify'),
color='grey'),
State('moved_x', 'moved', (), color='grey'),
State('replaced', 'replaced', (), color='grey'),
State('planned_x', 'planned', (), color='red'),
State('accepted_x', 'accepted', (), color='yellow'),
State('done_x', 'done', (), color='lightgreen'),
# transitions:
Transition('plan', 'plan', 'planned'),
Transition('accept', 'accept', 'accepted'),
Transition('start', 'start working', 'running'),
Transition('work', 'work', 'done'),
Transition('finish', 'finish', 'finished'),
Transition('cancel', 'cancel', 'cancelled'),
Transition('modify', 'modify', 'new'),
Transition('delegate', 'delegate', 'planned'),
Transition('move', 'move', 'planned'),
Transition('close', 'close', 'closed'),
initialState='new')
fieldNames = ['title', 'description', 'start', 'end', 'duration', 'effort',
'comment', 'party']
# meaning: - not editable, value=default
# / not editable, value=None
# + copy
# . default (may be empty)
editingRules = dict(
plan = {'*': '++.....+'},
accept = {'*': '++.....-',
'planned': '++++++.-',
'accepted': '++++++.-'},
start = {'*': '++./...-'},
work = {'*': '++.....-',
'running': '+++....-'},
finish = {'*': '++.....-',
'running': '+++....-'},
cancel = {'*': '++////./'},
modify = {'*': '++++++++'},
delegate= {'*': '++......'},
close = {'*': '++////./'},
)
class WorkItem(Stateful, Track):
""" A work item that may be stored as a track in a tracking storage.
"""
implements(IWorkItem)
statesDefinition = 'organize.workItemStates'
metadata_attributes = Track.metadata_attributes + ('state',)
index_attributes = metadata_attributes
typeName = 'WorkItem'
initAttributes = set(['party', 'title', 'description', 'start', 'end',
'duration', 'effort'])
def __init__(self, taskId, runId, userName, data):
super(WorkItem, self).__init__(taskId, runId, userName, data)
self.state = self.getState() # make initial state persistent
self.data['creator'] = userName
self.data['created'] = self.timeStamp
def getStatesDefinition(self):
return component.getUtility(IStatesDefinition, name=self.statesDefinition)
@property
def party(self):
return self.userName
@property
def title(self):
return self.data.get('title') or self.description
@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):
return self.data.get('effort') or self.duration
def __getattr__(self, attr):
if attr not in IWorkItem:
raise AttributeError(attr)
return self.data.get(attr)
@property
def currentWorkItems(self):
return list(getParent(self).query(runId=self.runId))
def doAction(self, action, userName, **kw):
if self != self.currentWorkItems[-1]:
raise ValueError("Actions are only allowed on the last item of a run.")
if action not in [t.name for t in self.getAvailableTransitions()]:
raise ValueError("Action '%s' not allowed in state '%s'" %
(action, self.state))
if action in self.specialActions:
return self.specialActions[action](self, userName, **kw)
if self.state == 'new':
self.setData(**kw)
self.doTransition(action)
self.reindex('state')
return self
new = self.createNew(action, userName, **kw)
new.userName = self.userName
if self.state == 'running':
new.replace(self)
elif self.state in ('planned', 'accepted', 'done'):
self.state = self.state + '_x'
self.reindex('state')
new.doTransition(action)
#new.reindex('state')
new.reindex()
return new
def modify(self, userName, **kw):
if self.state == 'new':
self.setData(**kw)
return self
new = self.createNew('modify', userName, **kw)
new.userName = self.userName
new.replace(self, keepState=True)
new.reindex()
return new
def delegate(self, userName, **kw):
if self.state == 'new':
delegated = self
self.setData(ignoreParty=True, **kw)
else:
if self.state in ('planned', 'accepted', 'delegated', 'moved', 'done'):
self.state = self.state + '_x'
self.reindex('state')
xkw = dict(kw)
xkw.pop('party', None)
delegated = self.createNew('delegate', userName, **xkw)
delegated.state = 'delegated'
delegated.reindex('state')
new = delegated.createNew('plan', userName, runId=0, **kw)
new.data['source'] = delegated.name
new.doTransition('plan')
#new.reindex('state')
new.reindex()
delegated.data['target'] = new.name
return new
def move(self, userName, **kw):
moved = self.createNew('move', userName, **kw)
moved.state = 'moved'
moved.reindex('state')
task = kw.pop('task', None)
new = moved.createNew(None, userName, taskId=task, runId=0, **kw)
new.userName = self.userName
new.data['source'] = moved.name
new.state = self.state
#new.reindex('state')
new.reindex()
moved.data['target'] = new.name
if self.state in ('planned', 'accepted', 'delegated', 'moved', 'done'):
self.state = self.state + '_x'
self.reindex('state')
return new
def close(self, userName, **kw):
kw['start'] = kw['end'] = getTimeStamp()
kw['duration'] = kw['effort'] = None
new = self.createNew('close', userName, copyData=('title',), **kw)
new.userName = self.userName
new.state = 'closed'
#new.reindex('state')
new.reindex()
getParent(self).stopRun(runId=self.runId, finish=True)
for item in self.currentWorkItems:
if item.state in ('planned', 'accepted', 'done', 'delegated', 'moved'):
item.state = item.state + '_x'
item.reindex('state')
return new
specialActions = dict(modify=modify, delegate=delegate, move=move,
close=close)
def setData(self, ignoreParty=False, **kw):
if self.state != 'new':
raise ValueError("Attributes may only be changed in state 'new'.")
party = kw.pop('party', None)
if not ignoreParty:
if party is not None:
self.userName = party
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():
if v is not None:
data[k] = v
start, end = data.get('start'), data.get('end')
if start and end and end < start:
data['end'] = start
def createNew(self, action, userName, taskId=None, copyData=None,
runId=None, **kw):
taskId = taskId or self.taskId
runId = runId is None and self.runId or runId
if copyData is None:
copyData = self.initAttributes
newData = {}
for k in self.initAttributes.union(set(['comment'])):
v = kw.get(k, _not_found)
if v is _not_found and k in copyData:
if action == 'start' and k in ('end',):
continue
if action in ('work', 'finish') and k in ('duration', 'effort',):
continue
v = self.data.get(k)
if v not in (None, _not_found):
newData[k] = v
workItems = IWorkItems(getParent(self))
new = workItems.add(taskId, userName, runId, **newData)
return new
def replace(self, other, keepState=False):
if keepState:
self.state = other.state
self.reindex('state')
other.state = 'replaced'
other.reindex('state')
def reindex(self, idx=None):
getParent(self).indexTrack(None, self, idx)
class WorkItems(object):
""" A tracking storage adapter managing work items.
"""
implements(IWorkItems)
adapts(ITrackingStorage)
def __init__(self, context):
self.context = context
def __getitem__(self, key):
return self.context[key]
def __iter__(self):
return iter(self.context.values())
def query(self, **criteria):
if 'task' in criteria:
criteria['taskId'] = criteria.pop('task')
if 'party' in criteria:
criteria['userName'] = criteria.pop('party')
if 'run' in criteria:
criteria['runId'] = criteria.pop('run')
return self.context.query(**criteria)
def add(self, task, userName, run=0, **kw):
if not run:
run = self.context.startRun()
trackId = self.context.saveUserTrack(task, run, userName, {})
track = self[trackId]
track.setData(**kw)
return track