Work in progress: re-building loops package as a 'concept management framework'

git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@809 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2005-12-03 09:54:11 +00:00
parent 2036521b66
commit 7a0a2e2790
9 changed files with 157 additions and 499 deletions

View file

@ -3,21 +3,21 @@ loops - Linked Objects for Organizational Process Services
($Id$)
Tasks and Subtasks
~~~~~~~~~~~~~~~~~~
Concepts and Relations
~~~~~~~~~~~~~~~~~~~~~~
Let's start with creating a few example tasks:
Let's start with creating a few example concepts:
>>> from loops.task import Task
>>> t1 = Task()
>>> t1.title
>>> from loops.concept import Concept
>>> c1 = Concept()
>>> c1.title
u''
>>> t2 = Task(u't2', u'Second Task')
>>> t2.title
u'Second Task'
>>> c2 = Concept(u'c2', u'Second Concept')
>>> c2.title
u'Second Concept'
Now we want to make the second task a subtask of the first one.
Now we want to relate the second concept to the first one.
In order to do this we first have to provide a relations registry. For
testing we use a simple dummy implementation.
@ -25,28 +25,32 @@ testing we use a simple dummy implementation.
>>> from cybertools.relation.interfaces import IRelationsRegistry
>>> from cybertools.relation.registry import DummyRelationsRegistry
>>> from zope.app.testing import ztapi
>>> ztapi.provideUtility(IRelationsRegistry, DummyRelationsRegistry())
Now we can assign the task t2 as a subtask to t1:
We also need a Relation class to be used for connecting concepts:
>>> from cybertools.relation import DyadicRelation
Now we can assign the concept c2 to c1:
>>> t1.assignSubtask(t2)
>>> c1.assignConcept(c2, DyadicRelation)
We can now ask our tasks for their subtasks and parent tasks:
We can now ask our concepts for their related concepts:
>>> st1 = t1.getSubtasks()
>>> len(st1)
>>> sc1 = c1.getSubConcepts()
>>> len(sc1)
1
>>> t2 in st1
>>> c2 in sc1
True
>>> len(t1.getParentTasks())
>>> len(c1.getParentConcepts())
0
>>> pc2 = c2.getParentConcepts()
>>> len(pc2)
1
>>> c1 in pc2
True
>>> len(c2.getSubConcepts())
0
>>> pt2 = t2.getParentTasks()
>>> len(pt2)
1
>>> t1 in pt2
True
>>> len(t2.getSubtasks())
0

View file

@ -26,7 +26,7 @@ from zope.app import zapi
from zope.app.dublincore.interfaces import ICMFDublinCore
from zope.security.proxy import removeSecurityProxy
from loops.interfaces import ITask
from loops.interfaces import IConcept
class Details(object):
@ -38,15 +38,15 @@ class Details(object):
return d and d.strftime('%Y-%m-%d %H:%M') or ''
class SubtaskAssignments(Details):
class ConceptRelations(Details):
def assignSubtask(self):
""" Add a subtask denoted by the path given in the
request variable subtaskPath.
"""
subtaskPath = self.request.get('subtaskPath')
#if subtaskPath:
subtask = zapi.traverse(zapi.getRoot(self.context), subtaskPath, None, self.request)
#if subtask:
self.context.assignSubtask(removeSecurityProxy(subtask))
conceptName = self.request.get('concept_name')
#if conceptName:
concept = zapi.getParent(self.context)[conceptName]
#if concept:
self.context.assignConcept(removeSecurityProxy(concept))
self.request.response.redirect('.')

View file

@ -6,24 +6,24 @@
<body>
<div metal:fill-slot="body">
<h1 tal:content="context/title">Task Title</h1>
<h1 tal:content="context/title">Concept Title</h1>
<div class="row">
<span class="label">Subtasks</span>:
<span class="label">Sub-Concepts</span>:
<span class="field"
tal:repeat="task context/getSubtasks">
<span tal:condition="python: task is None">**deleted**</span>
<span tal:condition="python: task is not None"
tal:content="task/title">subtask</span>
<span class="field" tal:condition="not:repeat/task/end"> - </span>
tal:repeat="concept context/getSubConcepts">
<span tal:condition="python: concept is None">**deleted**</span>
<span tal:condition="python: concept is not None"
tal:content="concept/title">subconcept</span>
<span class="field" tal:condition="not:repeat/concept/end"> - </span>
</span>
</div>
<div class="row">
<span class="label">Parent Tasks</span>:
<span class="label">Parent Concepts</span>:
<span class="field"
tal:repeat="task context/getParentTasks">
<span tal:content="task/title">parent task</span>
<span class="field" tal:condition="not:repeat/task/end"> - </span>
tal:repeat="concept context/getParentConcepts">
<span tal:content="concept/title">parent concept</span>
<span class="field" tal:condition="not:repeat/concept/end"> - </span>
</span>
</div>
<div class="row">
@ -34,13 +34,13 @@
<form action="." method="post"
tal:attributes="action context/@@absolute_url">
<div class="row">
<span class="label">Subtasks</span>:
<span class="label">Concept Name</span>:
<span class="field">
<input type="test" name="subtaskPath" />
<input type="test" name="concept_name" />
</span>
</div>
<div class="row">
<input type="submit" name="subtask_assign:method" value="Assign Subtask" />
<input type="submit" name="concept_assign:method" value="Assign Concept" />
</div>
</form>

View file

@ -6,24 +6,24 @@
<body>
<div metal:fill-slot="body">
<h1 tal:content="context/title">Task Title</h1>
<h1 tal:content="context/title">Concept Title</h1>
<div class="row">
<span class="label">Subtasks</span>:
<span class="label">Sub-Concepts</span>:
<span class="field"
tal:repeat="task context/getSubtasks">
<span tal:condition="python: task is None">**deleted**</span>
<span tal:condition="python: task is not None"
tal:content="task/title">subtask</span>
<span class="field" tal:condition="not:repeat/task/end"> - </span>
tal:repeat="concept context/getSubConcepts">
<span tal:condition="python: concept is None">**deleted**</span>
<span tal:condition="python: concept is not None"
tal:content="concept/title">subtask</span>
<span class="field" tal:condition="not:repeat/concept/end"> - </span>
</span>
</div>
<div class="row">
<span class="label">Parent Tasks</span>:
<span class="label">Parent Concepts</span>:
<span class="field"
tal:repeat="task context/getParentTasks">
<span tal:content="task/title">parent task</span>
<span class="field" tal:condition="not:repeat/task/end"> - </span>
tal:repeat="concept context/getParentConcepts">
<span tal:content="concept/title">parent concept</span>
<span class="field" tal:condition="not:repeat/concept/end"> - </span>
</span>
</div>
<div class="row">

View file

@ -7,67 +7,60 @@
>
<addform
label="Add Task"
name="AddTask.html"
schema="loops.interfaces.ITask"
content_factory="loops.task.Task"
label="Add Concept"
name="AddLoopsConcept.html"
schema="loops.interfaces.IConcept"
content_factory="loops.concept.Concept"
fields="title"
permission="zope.ManageContent"
/>
<addMenuItem
class="loops.task.Task"
title="Task"
description="A Task is a piece of work or something else a resource may be allocated to"
class="loops.concept.Concept"
title="Concept"
description="A Concept is a Concept is a Concept..."
permission="zope.ManageContent"
view="AddTask.html"
view="AddLoopsConcept.html"
/>
<editform
label="Edit Task"
label="Edit Concept"
name="edit.html"
schema="loops.interfaces.ITask"
for="loops.interfaces.ITask"
schema="loops.interfaces.IConcept"
for="loops.interfaces.IConcept"
permission="zope.ManageContent"
menu="zmi_views" title="Edit"
/>
<containerViews
for="loops.interfaces.ITask"
index="zope.View"
contents="zope.View"
add="zope.ManageContent"
/>
<defaultView
for="loops.interfaces.ITask"
for="loops.interfaces.IConcept"
name="details.html"
/>
<page
name="details.html"
for="loops.interfaces.ITask"
class=".task.Details"
template="task_details.pt"
for="loops.interfaces.IConcept"
class=".concept.Details"
template="concept_details.pt"
permission="zope.View"
menu="zmi_views" title="Details"
/>
<pages
for="loops.interfaces.ITask"
class=".task.SubtaskAssignments"
for="loops.interfaces.IConcept"
class=".concept.ConceptRelations"
permission="zope.ManageContent"
>
<page
name="subtasks.html"
template="subtasks_assign.pt"
menu="zmi_views" title="Subtasks"
name="assign.html"
template="concept_assign.pt"
menu="zmi_views" title="Assign Concept"
/>
<page
name="subtask_assign"
attribute="assignSubtask"
name="concept_assign"
attribute="assignConcept"
/>
</pages>

View file

@ -17,193 +17,67 @@
#
"""
Definition of the Task class.
Definition of the Concept class.
$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 zope.schema.fieldproperty import FieldProperty
from zope.component.interfaces import IFactory
from zope.interface import implements
from persistent import Persistent
from cybertools.relation.interfaces import IRelationsRegistry
from cybertools.relation import DyadicRelation
#from relation import Relation, Relations
from resource import Resource
from interfaces import ITask
from copy import copy
from interfaces import IConcept
class SubtaskRelation(DyadicRelation):
""" Relation of a first task (the parent task) to a second task
(the subtask).
"""
class Concept(Persistent):
implements(IConcept)
class Task(OrderedContainer):
implements(ITask)
title = u''
qualifier = u''
priority = FieldProperty(ITask['priority'])
_title = u''
def getTitle(self): return self._title
def setTitle(self, title): self._title = title
title = property(getTitle, setTitle)
def __init__(self, name=None, title=u''):
OrderedContainer.__init__(self)
if name:
self.__name__ = name
if title:
self.title = title
self.title = title
# subtasks:
# concept relations:
def getSubtasks(self, taskTypes=None):
def getSubConcepts(self, relationships=None):
rels = getRelations(first=self, relationships=relationships)
return [r.second for r in rels]
# TODO: sort...
def getParentConcepts(self, relationships=None):
rels = getRelations(second=self, relationships=relationships)
return [r.first for r in rels]
def assignConcept(self, concept, relationship):
registry = zapi.getUtility(IRelationsRegistry)
return [r.second
for r in registry.query(relationship=SubtaskRelation,
first=self)]
# TODO: sort according to priority
def getParentTasks(self, taskTypes=None):
registry = zapi.getUtility(IRelationsRegistry)
return [r.first
for r in registry.query(relationship=SubtaskRelation,
second=self)]
def assignSubtask(self, task):
registry = zapi.getUtility(IRelationsRegistry)
registry.register(SubtaskRelation(self, task))
registry.register(relationship(self, concept))
# TODO (?): avoid duplicates
def createSubtask(self, taskType=None, container=None, name=None):
container = container or zapi.getParent(self)
task = Task()
name = name or task._createTaskName(container)
container[name] = task
self.assignSubtask(task)
return task
def deassignConcept(self, concept, relationships=None):
pass # TODO
def deassignSubtask(self, task):
hits = [ r for r in self._subtasks if r._target == task ]
if hits:
self._subtasks.remove(hits[0])
task._parentTasks.remove(hits[0])
# resource allocations:
# TODO: move this to the relation package
def getAllocatedResources(self, allocTypes=None, resTypes=None):
from sets import Set
allocs = self._resourceAllocs
res = Set()
for at in allocTypes or allocs.keys():
res.union_update(allocs[at])
if resTypes:
res = [ r for r in res if r in resTypes ]
return tuple(res)
def getRelations(first=None, second=None, third=None, relationships=None):
registry = zapi.getUtility(IRelationsRegistry)
query = {}
if first: query['first'] = first
if second: query['second'] = second
if third: query['third'] = third
if not relationships:
return registry.query(**query)
else:
result = []
for r in relationships:
query['relationship'] = r
result.extend(registry.query(**query))
return result
def allocateResource(self, resource, allocType='standard'):
allocs = self._resourceAllocs
rList = allocs.get(allocType, [])
if resource not in rList:
rList.append(resource)
allocs[allocType] = rList
resource._addAllocation(self, allocType)
def createAndAllocateResource(self, resourceType=None, allocType='standard',
container=None, name=None):
container = container or zapi.getParent(self)
rClass = resourceType or Resource
resource = rClass()
name = name or resource._createResourceName(container)
container[name] = resource
self.allocateResource(resource, allocType)
return resource
def deallocateResource(self, resource, allocTypes=None):
allocs = self._resourceAllocs
for at in allocTypes or allocs.keys():
if resource in allocs[at]:
allocs[at].remove(resource)
resource._removeAllocations(self, allocTypes)
def allocatedUserIds(self):
return ()
def getAllocTypes(self, resource):
return ('standard',)
def getAllAllocTypes(self):
return ('standard',)
# resource constraint stuff:
def isResourceAllowed(self, resource, rcDontCheck=None):
rc = self.resourceConstraints
if not rc:
return True
for c in rc:
if rcDontCheck and c == rcDontCheck: # don't check constraint already checked
continue
# that's too simple, we must check all constraints for constraintType:
if not c.isResourceAllowed(resource):
return False
return True
def getCandidateResources(self):
rc = self.resourceConstraints
if not rc:
return ()
for c in rc:
candidates = c.getAllowedResources()
if candidates is not None:
return tuple([ r for r in candidates if self.isResourceAllowed(r, c) ])
return ()
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([ r for r in candidates if self.isResourceAllowed(r) ])
def isValid(self, checkSubtasks=True):
if self.resourceConstraints is not None:
for r in self.getAllocatedResources():
if not self.isResourceAllowed(r):
return False
if checkSubtasks:
for t in self.getSubtasks():
if not t.isValid():
return False
return True
# 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
subtasks = self.getSubtasks()
newTask._subtasks.clear()
for st in subtasks:
newSt = st.copyTask(targetContainer)
newSt._parentTasks.clear()
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 [0])
return prefix + str(last+1)

View file

@ -10,22 +10,19 @@
<!-- Content declarations -->
<interface
interface=".interfaces.ITask"
interface=".interfaces.IConcept"
type="zope.app.content.interfaces.IContentType" />
<content class=".task.Task">
<content class=".concept.Concept">
<implements interface="zope.app.container.interfaces.IContentContainer" />
<implements
interface="zope.app.annotation.interfaces.IAttributeAnnotatable" />
<implements
interface="zope.app.workflow.interfaces.IProcessInstanceContainerAdaptable" />
<factory
id="loops.Task"
description="Task object" />
id="loops.Concept"
description="Concept object" />
<!-- <require
permission="zope.View"
@ -33,11 +30,11 @@
<require
permission="zope.View"
interface=".interfaces.ITask" />
interface=".interfaces.IConcept" />
<require
permission="zope.ManageContent"
set_schema=".interfaces.ITask" />
set_schema=".interfaces.IConcept" />
</content>

View file

@ -23,251 +23,42 @@ $Id$
"""
from zope.interface import Interface
from zope.app.container.interfaces import IOrderedContainer
from zope.i18nmessageid import MessageFactory
from zope import schema
from zope.schema import Text, TextLine, List, Object, Int
_ = MessageFactory('loops')
class IRelations(Interface):
""" Holds a set of relations (more precisely: ends of relations).
A simple implementation is to just use an IOSet.
class IConcept(Interface):
""" The 'concept' is the central element of the loops framework.
A concept is related to other concepts.
"""
def add(relation):
""" Add a relation.
relationClass is the class that should be used for the relation
object; it should have a default setting, e.g. Relation.
"""
def remove(relation):
"""
"""
class IRelation(Interface):
""" Represents a relation from one object to another.
"""
source = Object(Interface,
title=u'Source Object',
description=u"Object that is the source of the relation")
target = Object(Interface,
title=u'Target Object',
description=u"Object that is the target of the relation")
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: require, allow, disallow',
default=u'require',
required=True)
referenceType = TextLine(
title=u'Reference Type',
description=u'Type of reference to the resource attribute to check: '
'explicit, parent, type, selectmethod, checkmethod.',
default=u'explicit',
required=True)
referenceKey = TextLine(
title=u'Reference Key',
description=u'Key for referencing the resource attribute, '
'e.g. method or type name')
referenceValues = List(
title=u'Reference Values',
description=u'Values to check for; usually a list of references to '
'the objects to be selected (referenceType=explicit) '
'or the parent objects (referenceType=parent)',
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.
Resources may be allocated to a Task.
A Task may depend on subtasks.
"""
title = TextLine(
title=u'Title',
description=u'Name or short title of the task',
title = schema.TextLine(
title=_(u'Title'),
description=_(u'Name or short title of the concept'),
default=u'',
required=True)
qualifier = TextLine(
title=u'Qualifier',
description=u'Some string telling more specifically what this task is about',
default=u'',
required=False)
# to do: associate with a dynamically provided vocabulary
priority = Int(
title=u'Priority',
description=u'The priority is usually used for ordering the listing '
'of tasks or subtasks; 0 means no priority, lower number = higher priority',
default=0,
required=False)
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.
def getSubConcepts(relationships=None):
""" Return a tuple of concepts related to self as sub-concepts,
possibly restricted to the relationships (typically a list of
relation classes) given.
"""
def assignSubtask(task):
""" Assign an existing task to self as a subtask..
def getParentConcepts(relationships=None):
""" Return a tuple of concepts related to self as parent concepts,
possibly restricted to the relationships (typically a list of
relation classes) given.
"""
def getParentTasks():
""" Return a tuple of tasks to which self has a subtask relationship.
def assignConcept(concept, relationship):
""" Assign an existing concept to self using the relationship given.
The assigned concept will be a sub-concept of self.
"""
def createSubtask(taskType=None, container=None, name=None):
""" Create a new task and assign it to self as a subtask.
taskType is a class that implements the ITask interface and defaults to Task.
The container in which the object will be created defaults to parent of self.
The name of the object to be created will be generated if not given.
Return the new subtask.
"""
def deassignSubtask(task):
""" 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
target resource types given.
"""
def allocateResource(resource, allocType='standard'):
""" Allocate resource to self. A special allocation type may be given.
"""
def createAndAllocateResource(resourceType=None, allocType='standard',
container=None, name=None):
""" Create resource and allocate it to self.
resourceType is a class that implements the IResource interface
and defaults to Resource.
allocType defaults to 'standard'.
The container in which the object will be created defaults to parent of self.
The name of the object to be created will be generated if not given.
Return the new resource object.
"""
def deallocateResource(resource, allocTypes=None):
""" Deallocate the resource given from self. If no allocTypes
given all allocations to resource will be removed.
"""
def allocatedUserIds():
""" Returns tuple of user IDs of allocated Person objects that are portal members.
Used by catalog index 'allocUserIds'.
"""
def getAllocTypes(resource):
""" Return the allocation types with which the resource given
is allocated to self.
"""
def getAllAllocTypes():
""" Return a tuple with all available allocation types defined
in the controller object that is responsible for self.
"""
# resource constraint stuff:
def isResourceAllowed(resource, rcDontCheck=None):
""" Return True if the resource given is allowed for this task.
If rcDontChedk is given this resource constraint will be skipped
during checking.
"""
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.
"""
def isValid(checkSubtasks=True):
""" Check if currently assigned resources fulfill the resource constraints.
Default: Also check subtasks.
"""
# 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
allocated to one or more Task objects.
"""
def getTasksAllocatedTo(allocTypes=None, taskTypes=None):
""" Return a list of task to which self is allocated to,
possibly restricted to the allocation types and
source task types given.
def deassignConcept(concept, relationships=None):
""" Remove the relations to the concept given from self, optionally
restricting them to the relationships given.
"""

View file

@ -8,16 +8,15 @@ from zope.interface import implements
from zope.app import zapi
from zope.app.intid.interfaces import IIntIds
from interfaces import ITask
from task import Task
from interfaces import IConcept
from concept import Concept
class Test(unittest.TestCase):
"Basic tests for the loops package."
def testInterfaces(self):
verifyClass(ITask, Task)
self.assert_(ITask.providedBy(Task()),
'Interface ITask is not implemented by class Task.')
verifyClass(IConcept, Concept)
self.assert_(IConcept.providedBy(Concept()))
def test_suite():