diff --git a/constraint.py b/constraint.py index 42c332c..1749194 100644 --- a/constraint.py +++ b/constraint.py @@ -37,12 +37,11 @@ class ResourceConstraint(object): referenceKey = None - def __init__(self, task): + def __init__(self): self.referenceValues = [] - self._task = task - def isResourceAllowed(self, resource): + def isResourceAllowed(self, resource, task=None): if self.referenceType == 'parent': for r in self.referenceValues: m = getattr(r, self.referenceKey) @@ -55,7 +54,7 @@ class ResourceConstraint(object): return False - def getAllowedResources(self, candidates=None): + def getAllowedResources(self, candidates=None, task=None): if self.referenceType == 'parent': result = [] for r in self.referenceValues: diff --git a/interfaces.py b/interfaces.py index f78d894..a893715 100644 --- a/interfaces.py +++ b/interfaces.py @@ -28,6 +28,58 @@ from zope.app.container.interfaces import IOrderedContainer from zope.schema import Text, TextLine, List, Object +class IResourceConstraint(Interface): + """ A ResourceConstraint governs which Resource objects may be + allocated to a Task object. + """ + + explanation = Text( + title=u'Explanation', + description=u'Explanation of this constraint - ' + 'why or why not certain resources may be allowed', + required=True) + + constraintType = TextLine( + title=u'Constraint Type', + description=u'Type of the constraint: select, require, disallow', + default=u'select', + required=True) + + referenceType = TextLine( + title=u'Reference Type', + description=u'Type of reference to the resource attribute to check: ' + 'explicit, parent, type, attribute, property, method', + default=u'explicit', + required=True) + + referenceKey = TextLine( + title=u'Reference Key', + description=u'Key for referencing the resource attribute') + + referenceValues = List( + title=u'Reference Values', + description=u'Attribute values to check for; may be any kind of object', + value_type=Object(Interface, title=u'Value'), + unique=True) + + def isResourceAllowed(resource, task=None): + """ Return True if this ResourceConstraint allows the resource given. + + If a task parameter is given there may be special checks on it, e.g. + on concerning subtasks of the task's parent task (sibling constraints). + """ + + def getAllowedResources(candidates=None, task=None): + """ Return a list of resource objects that are allowed by this + ResourceConstraint. + + If given, use candidates as a list of possible resources + (candidates must implement the IResource interface). + If a task parameter is given there may be special checks on it, e.g. + on concerning subtasks of the task's parent task (sibling constraints). + """ + + class ITask(IOrderedContainer): """ A Task is a piece of work. @@ -41,6 +93,17 @@ class ITask(IOrderedContainer): default=u'', required=True) + resourceConstraints = List( + title=u'Resource Constraints', + description=u'Collection of Constraints controlling the resources ' + 'that may be allocated to a task', + default=[], + required=False, + value_type=Object(schema=IResourceConstraint, title=u'Resource Constraint'), + unique=True) + + # subtask stuff: + def getSubtasks(taskTypes=None): """ Return a tuple of subtasks of self, possibly restricted to the task types given. @@ -67,6 +130,8 @@ class ITask(IOrderedContainer): """ Remove the subtask relation to task from self. """ + # resource allocations: + def getAllocatedResources(allocTypes=None, resTypes=None): """ Return a tuple of resources allocated to self, possibly restricted to the allocation types and @@ -109,6 +174,38 @@ class ITask(IOrderedContainer): in the controller object that is responsible for self. """ + # resource constraint stuff: + + def isResourceAllowed(resource): + """ Return True if the resource given is allowed for this task. + """ + + def getCandidateResources(): + """ Return a tuple of resource objects that are allowed for this task. + + Returns empty tuple if no usable resource constraints present. + """ + + def getAllowedResources(candidates=None): + """ Return a list of resource objects that are allowed for this task. + + If given, use candidates as a list of possible resources + (candidates must implement the IResource interface). + Returns None if no usable resource constraints are present. + Falls back to getCandidateResources if candidates is None + and usable resource constraints are present. + """ + + # Task object as prototype: + + def copyTask(targetContainer=None): + """ Copy self to the target container given and return the new task. + + Also copy all subtasks. Keep the references to resources and + resource constraints without copying them. + targetContainer defaults to self.getParent(). + """ + class IResource(IOrderedContainer): """ A Resource is an object - a thing or a person - that may be @@ -122,48 +219,3 @@ class IResource(IOrderedContainer): """ -class IResourceConstraint(Interface): - """ A ResourceConstraint governs which Resource objects may be - allocated to a Task object. - """ - - explanation = Text( - title=u'Explanation', - description=u'Explanation of this constraint - ' - 'why or why not certain resources may be allowed', - required=True) - - constraintType = TextLine( - title=u'Constraint Type', - description=u'Type of the constraint: select, require, disallow', - default=u'select', - required=True) - - referenceType = TextLine( - title=u'Reference Type', - description=u'Type of reference to the resource attribute to check: ' - 'explicit, parent, type, attribute, property, method' - default=u'explicit', - required=True) - - referenceKey = TextLine( - title=u'Reference Key', - description=u'Key for referencing the resource attribute') - - referenceValues = List( - title=u'Reference Values', - description=u'Attribute values to check for; may be any kind of object', - value_type=Object(Interface, title=u'Value')) - - def isResourceAllowed(resource): - """ Return True if this ResourceConstraint allows the resource given. - """ - - def getAllowedResources(candidates=None): - """ Return a list of resource objects that are allowed by this - ResourceConstraint. - - If given, use candidates as a list of possible resources - (candidates must implement the IResource interface). - """ - diff --git a/resource.py b/resource.py index cf48443..9cf6910 100644 --- a/resource.py +++ b/resource.py @@ -69,7 +69,7 @@ class Resource(OrderedContainer): def _createResourceName(self, container=None): prefix = 'rsc' container = container or zapi.getParent(self) - last = max([ int(n[len(prefix):]) for n in container.keys() ] or [1]) + last = max([ int(n[len(prefix):]) for n in container.keys() ] or [0]) return prefix + str(last+1) - \ No newline at end of file + diff --git a/task.py b/task.py index b36ae03..cf71ca4 100644 --- a/task.py +++ b/task.py @@ -24,11 +24,14 @@ $Id$ from zope.interface import implements from zope.app.container.ordered import OrderedContainer +from zope.app.copypastemove import ObjectCopier from zope.app import zapi from resource import Resource from interfaces import ITask +from copy import copy + class Task(OrderedContainer): implements(ITask) @@ -41,6 +44,7 @@ class Task(OrderedContainer): self._subtasks = [] self._parentTasks = [] self._resourceAllocs = {} + self.resourceConstraints = [] # subtasks: @@ -112,11 +116,55 @@ class Task(OrderedContainer): def getAllAllocTypes(self): return ('standard',) + # resource constraint stuff: + + def isResourceAllowed(self, resource): + rc = self.resourceConstraints + if not rc: + return True + for c in rc: + # that's too simple, we must check all constraints for constraintType: + if c.isResourceAllowed(resource): + return True + return False + + def getCandidateResources(self): + rc = self.resourceConstraints + if not rc: + return () + result = [] + for c in rc: + result.extend(c.getAllowedResources()) + return tuple(result) + + def getAllowedResources(self, candidates=None): + rc = self.resourceConstraints + if not rc: + return None + if candidates is None: + result = self.getCandidateResources() + # Empty result means: can't tell + return result and result or None + return tuple([ c for c in candidates if self.isResourceAllowed(c) ]) + + # Task object as prototype: + + def copyTask(self, targetContainer=None): + targetContainer = targetContainer or zapi.getParent(self) + newName = self._createTaskName(targetContainer) + newTask = copy(self) + targetContainer[newName] = newTask + newTask._subtasks = [] + for st in self.getSubtasks(): + newSt = st.copyTask(targetContainer) + newSt._parentTasks.remove(self) + newTask.assignSubtask(newSt) + return newTask # Helper methods: def _createTaskName(self, container=None): prefix = 'tsk' container = container or zapi.getParent(self) - last = max([ int(n[len(prefix):]) for n in container.keys() ] or [1]) + last = max([ int(n[len(prefix):]) for n in container.keys() ] or [0]) return prefix + str(last+1) diff --git a/tests/test_constraint.py b/tests/test_constraint.py index bcb4483..f06841e 100755 --- a/tests/test_constraint.py +++ b/tests/test_constraint.py @@ -22,7 +22,7 @@ class Test(unittest.TestCase): self.f1['rsc1'] = self.r1 self.t1 = Task() self.f1['tsk1'] = self.t1 - self.rc1 = ResourceConstraint(self.t1) + self.rc1 = ResourceConstraint() def tearDown(self): pass diff --git a/tests/test_task.py b/tests/test_task.py index e237a68..d9702a2 100755 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -86,7 +86,7 @@ class TestTaskResource(unittest.TestCase): self.t1 = Task() self.r1 = Resource() self.f1['tsk1'] = self.t1 - self.r1['rsc1'] = self.r1 + self.f1['rsc1'] = self.r1 def tearDown(self): pass @@ -115,11 +115,88 @@ class TestTaskResource(unittest.TestCase): self.assertEqual((r1,), t1.getAllocatedResources()) +from loops.constraint import ResourceConstraint + +class TestTaskResourceConstraints(unittest.TestCase): + "Test methods of the Task class related to checking for allowed resources." + + def setUp(self): + self.f1 = Folder() + self.f1.__name__ = u'f1' + self.t1 = Task() + self.r1 = Resource() + self.f1['tsk1'] = self.t1 + self.r1['rsc1'] = self.r1 + self.rc1 = ResourceConstraint() + + def tearDown(self): + pass + + + # the tests... + + def testSelectExplicit(self): + t1 = self.t1 + r1 = self.r1 + rc1 = self.rc1 + + self.assertEqual(True, t1.isResourceAllowed(r1)) # no check + self.assertEqual((), t1.getCandidateResources()) # no candidates + self.assertEqual(None, t1.getAllowedResources([r1]))# can't say without constraint + + self.t1.resourceConstraints.append(self.rc1) # empty constraint + self.assertEqual(False, t1.isResourceAllowed(r1)) # does not allow + self.assertEqual((), t1.getCandidateResources()) # anything + self.assertEqual((), t1.getAllowedResources([r1])) + + rc1.referenceValues = ([r1]) + self.assertEqual(True, rc1.isResourceAllowed(r1)) + self.assertEqual((r1,), t1.getCandidateResources()) + self.assertEqual((r1,), rc1.getAllowedResources([r1])) + + +class TestTaskCopy(unittest.TestCase): + "Tests related to copying tasks e.g. when using task prototypes." + + def setUp(self): + self.f1 = Folder() + self.f1.__name__ = u'f1' + self.t1 = Task() + self.r1 = Resource() + self.f1['tsk1'] = self.t1 + self.f1['rsc1'] = self.r1 + self.rc1 = ResourceConstraint() + + def tearDown(self): + pass + + + # the tests... + + def testCopyTask(self): + t1 = self.t1 + r1 = self.r1 + rc1 = self.rc1 + ts1 = t1.createSubtask() + ts1.resourceConstraints.append(rc1) + ts1.allocateResource(r1) + t2 = t1.copyTask() + self.failIf(t1 is t2, 't1 and t2 are still the same') + st2 = t2.getSubtasks() + self.assertEquals(1, len(st2)) + ts2 = st2[0] + self.failIf(ts1 is ts2, 'ts1 and ts2 are still the same') + self.assertEquals((r1,), ts2.getAllocatedResources()) + self.assertEquals([rc1,], ts2.resourceConstraints) + + def test_suite(): return unittest.TestSuite(( # DocTestSuite('loops.tests.doctests'), unittest.makeSuite(TestTask), unittest.makeSuite(TestTaskResource), + unittest.makeSuite(TestTaskResourceConstraints), + unittest.makeSuite(TestTaskCopy), )) if __name__ == '__main__':