diff --git a/README.txt b/README.txt index 1e0721b..a22f019 100755 --- a/README.txt +++ b/README.txt @@ -1,64 +1,126 @@ -loops - Linked Objects for Organizational Process Services -========================================================== +=============================================================== +loops - Linked Objects for Organization and Processing Services +=============================================================== ($Id$) -Concepts and Relations between them -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Concepts and Relations +====================== Let's start with creating a few example concepts, putting them in a top-level loops container and a concept manager: - >>> from loops import Loops - >>> loops = Loops() - - >>> from loops.concept import ConceptManager, Concept - >>> loops['concepts'] = ConceptManager() - >>> concepts = loops['concepts'] - >>> c1 = Concept() - >>> concepts['c1'] = c1 - >>> c1.title - u'' + >>> from loops import Loops + >>> loops = Loops() - >>> c2 = Concept(u'c2', u'Second Concept') - >>> concepts['c2'] = c2 - >>> c2.title - u'Second Concept' + >>> from loops.concept import ConceptManager, Concept + >>> loops['concepts'] = ConceptManager() + >>> concepts = loops['concepts'] + >>> zope = Concept() + >>> concepts['zope'] = zope + >>> zope.title + u'' + + >>> zope3 = Concept(u'Zope 3') + >>> concepts['zope3'] = zope3 + >>> zope3.title + u'Zope 3' 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. - >>> from cybertools.relation.interfaces import IRelationsRegistry - >>> from cybertools.relation.registry import DummyRelationsRegistry - >>> from zope.app.testing import ztapi - >>> ztapi.provideUtility(IRelationsRegistry, DummyRelationsRegistry()) + >>> from cybertools.relation.interfaces import IRelationsRegistry + >>> from cybertools.relation.registry import DummyRelationsRegistry + >>> from zope.app.testing import ztapi + >>> ztapi.provideUtility(IRelationsRegistry, DummyRelationsRegistry()) -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: +Now we can assign the concept c2 to c1 (using the standard ConceptRelation): - >>> c1.assignConcept(c2) + >>> zope.assignConcept(zope3) We can now ask our concepts for their related concepts: - >>> sc1 = c1.getSubConcepts() - >>> len(sc1) - 1 - >>> c2 in sc1 - True - >>> len(c1.getParentConcepts()) - 0 + >>> sc1 = zope.getSubConcepts() + >>> len(sc1) + 1 + >>> zope3 in sc1 + True + >>> len(zope.getParentConcepts()) + 0 - >>> pc2 = c2.getParentConcepts() - >>> len(pc2) - 1 - - >>> c1 in pc2 - True - >>> len(c2.getSubConcepts()) - 0 + >>> pc2 = zope3.getParentConcepts() + >>> len(pc2) + 1 + + >>> zope in pc2 + True + >>> len(zope3.getSubConcepts()) + 0 + +TODO: Work with views... +Resources and what they have to do with Concepts +================================================ + +We first need a resource manager: + + >>> from loops.resource import ResourceManager, Document + >>> loops['resources'] = ResourceManager() + >>> resources = loops['resources'] + +A common type of resource is a Document: + + >>> zope_info = Document(u'Zope Info') + >>> resources['zope_info'] = zope_info + >>> zope_info.title + u'Zope Info' + >>> zope_info.body + u'' + >>> zope_info.format + u'text/xml' + +We can associate a resource with a concept by assigning it to the concept: + + >>> zope.assignResource(zope_info) + >>> res = zope.getResources() + >>> list(res) + [] + +The resource also provides access to the associated concepts (or views, see +below): + + >>> conc = zope_info.getClients() + >>> len(conc) + 1 + >>> conc[0] is zope + True + +Views: Menus, Menu Items, Listings, etc +======================================= + +We first need a view manager: + + >>> from loops.view import ViewManager, Node + >>> loops['views'] = ViewManager() + >>> views = loops['views'] + +The view space is typically built up with nodes; a node may be a top-level +menu that may contain other nodes as menu items: + + >>> m1 = Node(u'Menu') + >>> views['m1'] = m1 + >>> m11 = Node(u'Zope') + >>> m1['m11'] = m11 + >>> m111 = Node(u'Zope in General') + >>> m11['m111'] = m111 + >>> m112 = Node(u'Zope 3') + >>> m11['m112'] = m112 + >>> m112.title + u'Zope 3' + >>> m112.description + u'' + +We can associate a node with a concept or directly with a resource: + diff --git a/browser/concept.py b/browser/concept.py index 4043e1d..3688804 100644 --- a/browser/concept.py +++ b/browser/concept.py @@ -38,6 +38,18 @@ class Details(object): d = dc.modified or dc.created return d and d.strftime('%Y-%m-%d %H:%M') or '' + def subConcepts(self): + return [{'object': c, + 'title': c.title, + 'url': zapi.absoluteURL(c, self.request)} + for c in self.context.getSubConcepts()] + + def parentConcepts(self): + return [{'object': c, + 'title': c.title, + 'url': zapi.absoluteURL(c, self.request)} + for c in self.context.getParentConcepts()] + class ConceptRelations(Details): @@ -45,6 +57,6 @@ class ConceptRelations(Details): """ Assign a concept denoted by the 'concept_name' request parameter. """ concept = zapi.getParent(self.context)[concept_name] - self.context.assignConcept(removeSecurityProxy(concept), DyadicRelation) + self.context.assignConcept(removeSecurityProxy(concept)) self.request.response.redirect(zapi.absoluteURL(self.context, self.request)) diff --git a/browser/concept_details.pt b/browser/concept_details.pt index 58c2fcb..007e086 100644 --- a/browser/concept_details.pt +++ b/browser/concept_details.pt @@ -11,18 +11,20 @@
Sub-Concepts: - **deleted** - subtask + tal:repeat="concept view/subConcepts"> + subtask -
Parent Concepts: - parent concept + tal:repeat="concept view/parentConcepts"> + subtask -
diff --git a/concept.py b/concept.py index 33db9a5..ee06327 100644 --- a/concept.py +++ b/concept.py @@ -28,13 +28,13 @@ from zope.app.container.contained import Contained from zope.interface import implements from persistent import Persistent -from cybertools.relation.interfaces import IRelationsRegistry from cybertools.relation import DyadicRelation +from cybertools.relation.registry import IRelationsRegistry, getRelations from interfaces import IConcept from interfaces import IConceptManager, IConceptManagerContained from interfaces import ILoopsContained -from relations import ConceptRelation +from relations import ConceptRelation, ConceptResourceRelation class Concept(Contained, Persistent): @@ -46,48 +46,60 @@ class Concept(Contained, Persistent): def setTitle(self, title): self._title = title title = property(getTitle, setTitle) - def __init__(self, name=None, title=u''): + def __init__(self, title=u''): self.title = title - # concept relations: + # concept relations def getSubConcepts(self, relationships=None): + if relationships is None: + relationships = [ConceptRelation] rels = getRelations(first=self, relationships=relationships) return [r.second for r in rels] # TODO: sort... def getParentConcepts(self, relationships=None): + if relationships is None: + relationships = [ConceptRelation] rels = getRelations(second=self, relationships=relationships) return [r.first for r in rels] def assignConcept(self, concept, relationship=ConceptRelation): registry = zapi.getUtility(IRelationsRegistry) - registry.register(relationship(self, concept)) + rel = relationship(self, concept) + registry.register(rel) # TODO (?): avoid duplicates def deassignConcept(self, concept, relationships=None): - pass # TODO + registry = zapi.getUtility(IRelationsRegistry) + relations = registry.query(first=self, second=concept, + relationships=relationships) + for rel in relations: + registry.unregister(relation) + + # resource relations + + def getResources(self, relationships=None): + if relationships is None: + relationships = [ConceptResourceRelation] + rels = getRelations(first=self, relationships=relationships) + return [r.second for r in rels] + # TODO: sort... + + def assignResource(self, resource, relationship=ConceptResourceRelation): + registry = zapi.getUtility(IRelationsRegistry) + registry.register(relationship(self, resource)) + # TODO (?): avoid duplicates + + def deassignResource(self, resource, relationships=None): + registry = zapi.getUtility(IRelationsRegistry) + relations = registry.query(first=self, second=resource, + relationships=relationships) + for rel in relations: + registry.unregister(relation) class ConceptManager(BTreeContainer): implements(IConceptManager, ILoopsContained) - -# TODO: move this to the cybertools.relation package - -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 - diff --git a/interfaces.py b/interfaces.py index fd745f8..16b762c 100644 --- a/interfaces.py +++ b/interfaces.py @@ -49,7 +49,7 @@ class IConcept(Interface): required=False) def getSubConcepts(relationships=None): - """ Return a tuple of concepts related to self as sub-concepts, + """ Return a sequence of concepts related to self as sub-concepts, possibly restricted to the relationships (typically a list of relation classes) given. """ @@ -63,6 +63,8 @@ class IConcept(Interface): def assignConcept(concept, relationship): """ Assign an existing concept to self using the relationship given. The assigned concept will be a sub-concept of self. + + The relationship defaults to ConceptRelation. """ def deassignConcept(concept, relationships=None): @@ -70,6 +72,23 @@ class IConcept(Interface): restricting them to the relationships given. """ + def getResources(relationships=None): + """ Return a sequence of resources assigned to self, + possibly restricted to the relationships given. + """ + + def assignResource(resource, relationship): + """ Assign an existing resource to self using the relationship given. + + The relationship defaults to ConceptResourceRelation. + """ + + def deassignResource(resource, relationships=None): + """ Remove the relations to the resource given from self, optionally + restricting them to the relationships given. + """ + + class IConceptManager(IContainer): """ A manager/container for concepts. """ @@ -92,6 +111,11 @@ class IResource(Interface): description=_(u'Title of the document'), required=False) + def getClients(relationships=None): + """ Return a sequence of objects that are clients of the resource, + i.e. that have some relation with it. + """ + class IDocument(IResource): """ A resource containing an editable body. @@ -166,10 +190,6 @@ class INodeContained(Interface): containers(INode, IViewManager) -class IViewManagerContained(Interface): - containers(IViewManager) - - # the loops top-level container class ILoops(IFolder): diff --git a/relations.py b/relations.py index 8a0688a..d6c72ac 100644 --- a/relations.py +++ b/relations.py @@ -33,12 +33,17 @@ class ConceptRelation(DyadicRelation): """ -class ResourceRelation(DyadicRelation): - """ A relation between concept and resource objects. +class ConceptResourceRelation(DyadicRelation): + """ A relation between a concept and a resource object. """ -class ViewRelation(DyadicRelation): - """ A relation between view and concept objects. +class ViewConceptRelation(DyadicRelation): + """ A relation between a view and a concept object. + """ + + +class ViewResourceRelation(DyadicRelation): + """ A relation between a view and a resource object. """ diff --git a/resource.py b/resource.py index fc582d0..43a86c4 100644 --- a/resource.py +++ b/resource.py @@ -27,6 +27,7 @@ from zope.app.container.btree import BTreeContainer from zope.app.container.contained import Contained from zope.interface import implements from persistent import Persistent +from cybertools.relation.registry import getRelations from interfaces import IResource, IDocument from interfaces import IResourceManager, IResourceManagerContained @@ -42,9 +43,13 @@ class Resource(Contained, Persistent): def setTitle(self, title): self._title = title title = property(getTitle, setTitle) - def __init__(self, name=None, title=u''): + def __init__(self, title=u''): self.title = title + def getClients(self, relationships=None): + rels = getRelations(second=self, relationships=relationships) + return [r.first for r in rels] + class Document(Resource): diff --git a/tests.py b/tests.py index 3a02399..dd0baa8 100755 --- a/tests.py +++ b/tests.py @@ -1,6 +1,6 @@ # $Id$ -import unittest +import unittest, doctest from zope.testing.doctestunit import DocFileSuite from zope.app.testing import ztapi from zope.interface.verify import verifyClass @@ -37,9 +37,10 @@ class Test(unittest.TestCase): def test_suite(): + flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS return unittest.TestSuite(( unittest.makeSuite(Test), - DocFileSuite('README.txt'), + DocFileSuite('README.txt', optionflags=flags), )) if __name__ == '__main__': diff --git a/view.py b/view.py index 5806e7b..54a0c0e 100644 --- a/view.py +++ b/view.py @@ -30,13 +30,13 @@ from zope.interface import implements from persistent import Persistent from interfaces import IView, INode -from interfaces import IViewManager, IViewManagerContained +from interfaces import IViewManager, INodeContained from interfaces import ILoopsContained class View(object): - implements(IView, IViewManagerContained) + implements(IView, INodeContained) _title = u'' def getTitle(self): return self._title @@ -48,15 +48,16 @@ class View(object): def setDescription(self, description): self._description = description description = property(getDescription, setDescription) - def __init__(self, name=None, title=u''): + def __init__(self, title=u'', description=u''): self.title = title self.description = description + super(View, self).__init__() def getConcepts(self): return [] -class Node(OrderedContainer, View): +class Node(View, OrderedContainer): implements(INode)