diff --git a/README.txt b/README.txt index f0e70b4..11c4551 100755 --- a/README.txt +++ b/README.txt @@ -49,7 +49,7 @@ default predicate concept; the default name for this is 'standard'. >>> from cybertools.relation.registry import DummyRelationRegistry >>> from zope.app.testing import ztapi >>> ztapi.provideUtility(IRelationRegistry, DummyRelationRegistry()) - >>> concepts['standard'] = Concept('Default Predicate') + >>> concepts['standard'] = Concept('parent') Now we can assign the concept c2 as a child to c1 (using the standard ConceptRelation): @@ -58,20 +58,13 @@ ConceptRelation): We can now ask our concepts for their related child and parent concepts: - >>> sc1 = cc1.getChildren() - >>> len(sc1) - 1 - >>> cc2 in sc1 - True + >>> [zapi.getName(c) for c in cc1.getChildren()] + [u'cc2'] >>> len(cc1.getParents()) 0 + >>> [zapi.getName(p) for p in cc2.getParents()] + [u'cc1'] - >>> pc2 = cc2.getParents() - >>> len(pc2) - 1 - - >>> cc1 in pc2 - True >>> len(cc2.getChildren()) 0 @@ -80,7 +73,7 @@ relation to a special kind of concept object with the magic name 'type'. This type object is its own type. The type relations themselves are of a special predicate 'hasType'. - >>> concepts['hasType'] = Concept(u'Type Predicate') + >>> concepts['hasType'] = Concept(u'has type') >>> concepts['type'] = Concept(u'Type') >>> typeObject = concepts['type'] >>> typeObject.setConceptType(typeObject) @@ -117,9 +110,18 @@ Concept Views >>> from loops.browser.concept import ConceptView >>> view = ConceptView(cc1, TestRequest()) - >>> sorted([c.title for c in view.children()]) + >>> children = list(view.children()) + >>> [c.title for c in children] [u'Zope 3'] +The token attribute provided with the items returned by the children() and +parents() methods identifies identifies not only the item itself but +also the relationship to the context object using a combination +of URIs to item and the predicate of the relationship: + + >>> [c.token for c in children] + ['.loops/concepts/cc2:.loops/concepts/standard'] + The concept view allows updating the underlying context object: >>> cc3 = Concept(u'loops for Zope 3') @@ -133,7 +135,7 @@ The concept view allows updating the underlying context object: >>> view = ConceptView(cc1, ... TestRequest(action='remove', qualifier='children', - ... tokens=['.loops/concepts/cc2'])) + ... tokens=['.loops/concepts/cc2:.loops/concepts/standard'])) >>> view.update() True >>> sorted(c.title for c in cc1.getChildren()) diff --git a/browser/common.py b/browser/common.py index 67de75b..aa09e93 100644 --- a/browser/common.py +++ b/browser/common.py @@ -32,8 +32,8 @@ from zope.security.proxy import removeSecurityProxy class BaseView(object): def __init__(self, context, request): - self.context = context - #self.context = removeSecurityProxy(context) + #self.context = context + self.context = removeSecurityProxy(context) self.request = request @Lazy @@ -64,6 +64,15 @@ class BaseView(object): def value(self): return self.context + @Lazy + def typeTitle(self): + return self.context.conceptType.title + + @Lazy + def typeUrl(self): + return zapi.absoluteURL(self.context.conceptType, self.request) + + class LoopsTerms(object): """ Provide the ITerms interface, e.g. for usage in selection diff --git a/browser/concept.py b/browser/concept.py index 86f8912..c2459ed 100644 --- a/browser/concept.py +++ b/browser/concept.py @@ -40,12 +40,22 @@ from loops.browser.common import BaseView, LoopsTerms class ConceptView(BaseView): def children(self): - ch = self.context.getChildren() - return ch and self.viewIterator(ch) or [] + for r in self.context.getChildRelations(): + yield ConceptRelationView(r, self.request, contextIsSecond=True) def parents(self): - p = self.context.getParents() - return p and self.viewIterator(p) or [] + for r in self.context.getParentRelations(): + yield ConceptRelationView(r, self.request) + #rels = self.context.getParentRelations() + #result = [] + #for r in rels: + # p = r.first + #if p is None: # this should not be necessary + # print 'Warning: parents() got a None first on', \ + # zapi.getName(self.context), zapi.getName(r.predicate) + # continue + # p.predicate = r.predicate + #return result and self.viewIterator(result) or [] def viewIterator(self, objs): request = self.request @@ -61,6 +71,10 @@ class ConceptView(BaseView): return True tokens = self.request.get('tokens', []) for token in tokens: + parts = token.split(':') + token = parts[0] + if len(parts) > 1: + relToken = parts[1] concept = self.loopsRoot.loopsTraverse(token) if action == 'assign': assignAs = self.request.get('assignAs', 'child') @@ -71,11 +85,12 @@ class ConceptView(BaseView): else: raise(BadRequest, 'Illegal assignAs parameter: %s.' % assignAs) elif action == 'remove': + predicate = self.loopsRoot.loopsTraverse(relToken) qualifier = self.request.get('qualifier') if qualifier == 'parents': - self.context.deassignParents(concept) + self.context.deassignParents(concept, [predicate]) elif qualifier == 'children': - self.context.deassignChildren(concept) + self.context.deassignChildren(concept, [predicate]) else: raise(BadRequest, 'Illegal qualifier: %s.' % qualifier) else: @@ -102,12 +117,6 @@ class ConceptView(BaseView): else: raise(BadRequest, 'Illegal assignAs parameter: %s.' % assignAs) - #def getVocabularyForRelated(self): # obsolete - # source = ConceptSourceList(self.context) - # terms = zapi.getMultiAdapter((source, self.request), ITerms) - # for candidate in source: - # yield terms.getTerm(candidate) - def search(self): request = self.request if request.get('action') != 'search': @@ -120,3 +129,50 @@ class ConceptView(BaseView): result = self.loopsRoot.getConceptManager().values() return self.viewIterator(result) + +class ConceptRelationView(object): + + def __init__(self, relation, request, contextIsSecond=False): + if contextIsSecond: + self.context = relation.second + self.other = relation.first + else: + self.context = relation.first + self.other = relation.second + self.predicate = relation.predicate + self.conceptType = self.context.conceptType + self.request = request + + @Lazy + def loopsRoot(self): + return self.context.getLoopsRoot() + + @Lazy + def url(self): + return zapi.absoluteURL(self.context, self.request) + + @Lazy + def title(self): + return self.context.title + + @Lazy + def token(self): + return ':'.join((self.loopsRoot.getLoopsUri(self.context), + self.loopsRoot.getLoopsUri(self.predicate))) + + @Lazy + def typeTitle(self): + return self.conceptType.title + + @Lazy + def typeUrl(self): + return zapi.absoluteURL(self.conceptType, self.request) + + @Lazy + def predicateTitle(self): + return self.predicate.title + + @Lazy + def predicateUrl(self): + return zapi.absoluteURL(self.predicate, self.request) + diff --git a/browser/relation_macros.pt b/browser/relation_macros.pt index a116ce8..5924ffb 100644 --- a/browser/relation_macros.pt +++ b/browser/relation_macros.pt @@ -22,6 +22,8 @@   Title + Predicate + Type @@ -37,6 +39,23 @@ Title + + + + Type + + + + + + Title + + diff --git a/concept.py b/concept.py index fb6584e..ba9a646 100644 --- a/concept.py +++ b/concept.py @@ -45,13 +45,10 @@ from interfaces import ISearchableText # relation classes -class ConceptRelation(DyadicRelation): - """ A relation between concept objects. - """ - implements(IConceptRelation) +class BaseRelation(DyadicRelation): def __init__(self, first, second, predicate=None): - super(ConceptRelation, self).__init__(first, second) + super(BaseRelation, self).__init__(first, second) if predicate is None: context = first is not None and first or second cm = context.getLoopsRoot().getConceptManager() @@ -59,12 +56,18 @@ class ConceptRelation(DyadicRelation): self.predicate = predicate def getPredicateName(self): - baseName = super(ConceptRelation, self).getPredicateName() + baseName = super(BaseRelation, self).getPredicateName() id = zapi.getUtility(IRelationRegistry).getUniqueIdForObject(self.predicate) return '.'.join((baseName, str(id))) -class ResourceRelation(DyadicRelation): +class ConceptRelation(BaseRelation): + """ A relation between concept objects. + """ + implements(IConceptRelation) + + +class ResourceRelation(BaseRelation): """ A relation between a concept and a resource object. """ implements(IConceptRelation) @@ -84,16 +87,23 @@ class Concept(Contained, Persistent): title = property(getTitle, setTitle) def getConceptType(self): - cm = self.getLoopsRoot().getConceptManager() - typeRelation = ConceptRelation(None, self, cm.getTypePredicate()) - rel = getRelationSingle(self, typeRelation, forSecond=True) - return rel and rel.first or None + typePred = self.getConceptManager().getTypePredicate() + parents = self.getParents([typePred]) + #typeRelation = ConceptRelation(None, self, cm.getTypePredicate()) + #rels = getRelationSingle(self, typeRelation, forSecond=True) + # TODO (?): check for multiple types (->Error) + return parents and parents[0] or None def setConceptType(self, concept): - if self.getConceptType() != concept: - cm = self.getLoopsRoot().getConceptManager() - typeRelation = ConceptRelation(removeSecurityProxy(concept), self, - cm.getTypePredicate()) - setRelationSingle(typeRelation, forSecond=True) + current = self.getConceptType() + if current != concept: + typePred = self.getConceptManager().getTypePredicate() + if current is not None: + self.deassignParents(current, [typePred]) + self.assignParent(concept, typePred) + #cm = self.getLoopsRoot().getConceptManager() + #typeRelation = ConceptRelation(removeSecurityProxy(concept), self, + # cm.getTypePredicate()) + #setRelationSingle(typeRelation, forSecond=True) conceptType = property(getConceptType, setConceptType) def __init__(self, title=u''): @@ -102,66 +112,74 @@ class Concept(Contained, Persistent): def getLoopsRoot(self): return zapi.getParent(self).getLoopsRoot() + def getConceptManager(self): + return self.getLoopsRoot().getConceptManager() + # concept relations - def getChildren(self, relationships=None): - if relationships is None: - relationships = [ConceptRelation(self, None)] - rels = getRelations(first=self, relationships=relationships) - return [r.second for r in rels] + def getChildRelations(self, predicates=None, second=None): + predicates = predicates is None and ['*'] or predicates + relationships = [ConceptRelation(self, None, p) for p in predicates] # TODO: sort... + return getRelations(first=self, second=second, relationships=relationships) - def getParents(self, relationships=None): - if relationships is None: - relationships = [ConceptRelation(None, self)] - rels = getRelations(second=self, relationships=relationships) - return [r.first for r in rels] + def getChildren(self, predicates=None): + return [r.second for r in self.getChildRelations(predicates)] - def assignChild(self, concept, relationship=ConceptRelation): + def getParentRelations (self, predicates=None, first=None): + predicates = predicates is None and ['*'] or predicates + relationships = [ConceptRelation(None, self, p) for p in predicates] + # TODO: sort... + return getRelations(first=first, second=self, relationships=relationships) + + def getParents(self, predicates=None): + return [r.first for r in self.getParentRelations(predicates)] + + def assignChild(self, concept, predicate=None): + if predicate is None: + predicate = self.getConceptManager().getDefaultPredicate() registry = zapi.getUtility(IRelationRegistry) - rel = relationship(self, concept) + rel = ConceptRelation(self, concept, predicate) registry.register(rel) # TODO (?): avoid duplicates - def assignParent(self, concept, relationship=ConceptRelation): - concept.assignChild(self, relationship) + def assignParent(self, concept, predicate=None): + concept.assignChild(self, predicate) - def deassignChildren(self, concept, relationships=None): - if relationships is None: - relationships = [ConceptRelation(self, None)] + def deassignChildren(self, concept, predicates=None): registry = zapi.getUtility(IRelationRegistry) - relations = [] - for rs in relationships: - relations.extend(registry.query(first=self, second=concept, - relationship=rs)) - for rel in relations: + #relations = [] + #for rs in relationships: + # relations.extend(registry.query(first=self, second=concept, + # relationship=rs)) + for rel in self.getChildRelations(predicates, concept): registry.unregister(rel) - def deassignParents(self, concept, relationships=None): - concept.deassignChildren(self, relationships) + def deassignParents(self, concept, predicates=None): + concept.deassignChildren(self, predicates) # resource relations - def getResources(self, relationships=None): - if relationships is None: - relationships = [ResourceRelation] - rels = getRelations(first=self, relationships=relationships) - return [r.second for r in rels] + def getResourceRelations(self, predicates=None): + predicates = predicates is None and ['*'] or predicates + relationships = [ResourceRelation(self, None, p) for p in predicates] # TODO: sort... + return getRelations(first=self, relationships=relationships) - def assignResource(self, resource, relationship=ResourceRelation): + def getResources(self, predicates=None): + return [r.second for r in self.getResourceRelations(predicates)] + + def assignResource(self, resource, predicate=None): + if predicate is None: + predicate = self.getConceptManager().getDefaultPredicate() registry = zapi.getUtility(IRelationRegistry) - registry.register(relationship(self, resource)) + registry.register(ResourceRelation(self, resource, predicate)) # TODO (?): avoid duplicates - def deassignResource(self, resource, relationships=None): - if relationships is None: - relationships = [ResourceRelation] + def deassignResource(self, resource, predicates=None): registry = zapi.getUtility(IRelationRegistry) - relations = registry.query(first=self, second=resource, - relationships=relationships) - for rel in relations: - registry.unregister(relation) + for rel in self.getResourceRelations(predicates): + registry.unregister(rel) # concept manager diff --git a/interfaces.py b/interfaces.py index 9688c8e..981f5f1 100644 --- a/interfaces.py +++ b/interfaces.py @@ -79,56 +79,69 @@ class IConcept(ILoopsObject, IPotentialTarget): source="loops.conceptTypeSource", required=False) - def getChildren(relationships=None): + def getChildren(predicates=None): """ Return a sequence of concepts related to self as child concepts, - possibly restricted to the relationships (typically a list of - relation classes) given. + optionally restricted to the predicates given. """ - def getParents(relationships=None): + def getChildRelations(predicates=None): + """ Return a sequence of relations to other concepts assigned to self + as child concepts, optionally restricted to the predicates given. + """ + + def getParents(predicates=None): """ Return a tuple of concepts related to self as parent concepts, - possibly restricted to the relationships (typically a list of - relation classes) given. + optionally restricted to the predicates given. """ - def assignChild(concept, relationship): - """ Assign an existing concept to self using the relationship given. + def getParentRelations(predicates=None): + """ Return a sequence of relations to other concepts assigned to self + as child concepts, optionally restricted to the predicates given. + """ + + def assignChild(concept, predicate): + """ Assign an existing concept to self using the predicate given. The assigned concept will be a child concept of self. - The relationship defaults to ConceptRelation. + The predicate defaults to the concept manager's default predicate. """ - def assignParent(concept, relationship): - """ Assign an existing concept to self using the relationship given. + def assignParent(concept, predicate): + """ Assign an existing concept to self using the predicate given. The assigned concept will be a parent concept of self. - The relationship defaults to ConceptRelation. + The predicate defaults to the concept manager's default predicate. """ - def deassignChildren(concept, relationships=None): + def deassignChildren(concept, predicates=None): """ Remove the child concept relations to the concept given from self, - optionally restricting them to the relationships given. + optionally restricting them to the predicates given. """ - def deassignParents(concept, relationships=None): + def deassignParents(concept, predicates=None): """ Remove the child concept relations to the concept given from self, - optionally restricting them to the relationships given. + optionally restricting them to the predicates given. """ - def getResources(relationships=None): + def getResources(predicates=None): """ Return a sequence of resources assigned to self, - possibly restricted to the relationships given. + optionally restricted to the predicates given. """ - def assignResource(resource, relationship): - """ Assign an existing resource to self using the relationship given. + def getResourceRelations(predicates=None): + """ Return a sequence of relations to resources assigned to self, + optionally restricted to the predicates given. + """ + + def assignResource(resource, predicate): + """ Assign an existing resource to self using the predicate given. The relationship defaults to ConceptResourceRelation. """ - def deassignResource(resource, relationships=None): + def deassignResource(resource, predicates=None): """ Remove the relations to the resource given from self, optionally - restricting them to the relationships given. + restricting them to the predicates given. """