From 8572156c11a067f39294b93f774601d4f9426b8c Mon Sep 17 00:00:00 2001 From: helmutm Date: Mon, 22 Dec 2008 21:49:43 +0000 Subject: [PATCH] work in progress: work item (task) management git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@3080 fd906abe-77d9-0310-91a1-e0d9ade77398 --- organize/README.txt | 38 ++++++++++++++++- organize/interfaces.py | 21 ++++++++++ organize/work.py | 93 +++++++++++++++++++++++++++++++++++++----- tracking/btree.py | 4 ++ 4 files changed, 143 insertions(+), 13 deletions(-) diff --git a/organize/README.txt b/organize/README.txt index aa22d3f..d40f3a4 100644 --- a/organize/README.txt +++ b/organize/README.txt @@ -91,5 +91,39 @@ We are now ready to set up the tracking storage. The work management only deals with the IDs or names of tasks and persons, so we do not have to set up real objects. - >>> workItems.add('001', 'john') - + >>> wi01 = workItems.add('001', 'john') + >>> wi01 + + +Properties that have not been set explicitly default to 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='jim') + >>> wi01 + + +Change work item states +----------------------- + + >>> wi01.assign() + >>> wi01.state + 'assigned' + + >>> wi01.startWork(start=1229958000) + >>> wi01 + diff --git a/organize/interfaces.py b/organize/interfaces.py index e7b167a..8a677a3 100644 --- a/organize/interfaces.py +++ b/organize/interfaces.py @@ -449,12 +449,33 @@ class IWorkItem(Interface): effort = Attribute('How much effort (time units) it took to finish the work.') # 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.') continuation = 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): + """ 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. + """ + class IWorkItems(Interface): """ A collection (manager, container) of work items. diff --git a/organize/work.py b/organize/work.py index a1561b3..a898e2c 100644 --- a/organize/work.py +++ b/organize/work.py @@ -25,13 +25,14 @@ $Id$ from zope import component from zope.component import adapts from zope.interface import implementer, implements +from zope.traversing.api import 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 +from cybertools.tracking.btree import Track, getTimeStamp from cybertools.tracking.interfaces import ITrackingStorage @@ -62,8 +63,6 @@ class WorkItem(Stateful): def getStatesDefinition(self): return component.getUtility(IStatesDefinition, name=self.statesDefinition) - # work item attributes (except state that is provided by stateful - class WorkItemTrack(WorkItem, Track): """ A work item that may be stored as a track in a tracking storage. @@ -73,21 +72,93 @@ class WorkItemTrack(WorkItem, Track): index_attributes = metadata_attributes typeName = 'WorkItem' - initAttributes = set(['description', 'predecessor', + initAttributes = set(['party', 'description', 'predecessor', 'planStart', 'planEnd', 'planDuration', 'planEffort']) def __init__(self, taskId, runId, userName, data={}): - for k in data: - if k not in initAttributes: - raise ValueError("Illegal initial attribute: '%s'." % k) - super(WorkItemTrack, self).__init__(taskId, runId, userName, data) + super(WorkItemTrack, self).__init__(taskId, runId, userName) self.state = self.getState() # make initial state persistent + self.data['creator'] = userName + self.data['created'] = self.timeStamp + self.setInitData(**data) def __getattr__(self, attr): - value = self.data.get(attr, _not_found) - if value is _not_found: + if attr not in IWorkItem: raise AttributeError(attr) - return value + return self.data.get(attr, None) + + @property + def party(self): + return self.userName + + def setInitData(self, **kw): + for k in kw: + if k not in self.initAttributes: + raise ValueError("Illegal initial attribute: '%s'." % k) + party = kw.pop('party', None) + if party is not None: + if self.state != 'created': + raise ValueError("Attribute 'party' may not be set in the state '%s'" % + self.state) + else: + self.userName = party + indexChanged = True + self.checkOverwrite(kw) + updatePlanData = False + indexChanged = False + 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.checkOverwrite(kw) + self.doTransition(transition) + self.reindex() + + def reindex(self): + getParent(self).updateTrack(self, {}) # force reindex + + def checkOverwrite(self, kw): + for k, v in kw.items(): + #old = data.get(k) + 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 class WorkItems(object): diff --git a/tracking/btree.py b/tracking/btree.py index b946805..061d1a3 100644 --- a/tracking/btree.py +++ b/tracking/btree.py @@ -74,6 +74,8 @@ class Track(Persistent): self.data = data def update(self, newData): + if not newData: + return self.timeStamp = getTimeStamp() data = self.data data.update(newData) @@ -186,6 +188,8 @@ class TrackingStorage(BTreeContainer): return self.updateTrack(track, data) trackId, trackNum = self.generateTrackId() track = self.trackFactory(taskId, runId, userName, data) + track.__parent__ = self + track.__name__ = trackId if timeStamp: track.timeStamp = timeStamp self[trackId] = track