diff --git a/compound/README.txt b/compound/README.txt index 2300efd..1f0d4ae 100644 --- a/compound/README.txt +++ b/compound/README.txt @@ -4,3 +4,73 @@ loops - Linked Objects for Organization and Processing Services ($Id$) + >>> from zope import component + >>> from zope.traversing.api import getName + +Let's set up a loops site with basic and example concepts and resources. + + >>> from zope.app.testing.setup import placefulSetUp, placefulTearDown + >>> site = placefulSetUp(True) + + >>> from loops.tests.setup import TestSite + >>> t = TestSite(site) + >>> concepts, resources, views = t.setup() + + +Compund Objects - Hierarchies with Ordered Components +===================================================== + + >>> from loops.compound.base import Compound + >>> component.provideAdapter(Compound) + + >>> tType = concepts.getTypeConcept() + >>> from loops.setup import addAndConfigureObject + >>> from loops.concept import Concept + >>> from loops.compound.interfaces import ICompound + +We first create the compound type and one instance of the newly created +type. We also need an ``ispartof`` predicate. + + >>> tCompound = addAndConfigureObject(concepts, Concept, 'compound', + ... title=u'Compound', + ... conceptType=tType, typeInterface=ICompound) + >>> c01 = addAndConfigureObject(concepts, Concept, 'c01', + ... title=u'Compound #01', conceptType=tCompound) + >>> tPredicate = concepts.getPredicateType() + >>> isPartof = addAndConfigureObject(concepts, Concept, 'ispartof', + ... title=u'is Part of', conceptType=tPredicate) + +In order to access the compound concept's attributes we have to adapt +it. + + >>> from loops.common import adapted + >>> aC01 = adapted(c01) + +Now we are able to add resources to it. + + >>> aC01.add(resources[u'd003.txt']) + >>> aC01.add(resources[u'd001.txt']) + + >>> [getName(p) for p in aC01.getParts()] + [u'd003.txt', u'd001.txt'] + + >>> aC01.add(resources[u'd001.txt'], 0) + >>> [getName(p) for p in aC01.getParts()] + [u'd001.txt', u'd003.txt', u'd001.txt'] + + >>> aC01.add(resources[u'd002.txt'], -1) + >>> [getName(p) for p in aC01.getParts()] + [u'd001.txt', u'd003.txt', u'd002.txt', u'd001.txt'] + +We can reorder the parts of a compound. + + >>> aC01.reorder([resources[u'd002.txt'], resources[u'd001.txt'], ]) + >>> [getName(p) for p in aC01.getParts()] + [u'd002.txt', u'd001.txt', u'd003.txt', u'd001.txt'] + +And remove a part from the compound. + + >>> aC01.remove(resources[u'd001.txt'], 1) + >>> [getName(p) for p in aC01.getParts()] + [u'd002.txt', u'd003.txt', u'd001.txt'] + diff --git a/compound/base.py b/compound/base.py index 6193c0b..6224ebc 100644 --- a/compound/base.py +++ b/compound/base.py @@ -22,3 +22,90 @@ Compound objects like articles, blog posts, storyboard items, ... $Id$ """ +from zope.cachedescriptors.property import Lazy +from zope.interface import implements +from zope.traversing.api import getName + +from loops.common import AdapterBase +from loops.compound.interfaces import ICompound, compoundPredicateName + + +class Compound(AdapterBase): + + implements(ICompound) + + def getParts(self): + return self.context.getChildren([self.partOf]) + + def add(self, obj, position=None): + if position is None: + order = self.getMaxOrder() + 1 + else: + order = self.getOrderForPosition(position) + self.context.assignChild(obj, self.partOf, order=order) + + def remove(self, obj, position=None): + if position is None: + self.context.deassignChild(obj, [self.partOf]) + else: + rel = self.getPartRelations()[position] + self.context.deassignChild(obj, [self.partOf], order=rel.order) + + def reorder(self, parts): + existing = list(self.getPartRelations()) + order = 1 + for p in parts: + for idx, x in enumerate(existing): + if x.second == p: + x.order = order + order += 1 + del existing[idx] + break + else: + raise ValueError("Part '%s' not in list of existing parts." + % getName(p)) + for x in existing: # position the rest at the end + x.order = order + order += 1 + + # helper methods and properties + + def getPartRelations(self): + return self.context.getChildRelations([self.partOf]) + + def getMaxOrder(self): + rels = self. getPartRelations() + if rels: + return max(r.order for r in rels) + return 1 + + def getOrderForPosition(self, pos): + rels = self. getPartRelations() + if pos < 0: + pos = len(rels) + pos + previous = 0 + value = None + for idx, r in enumerate(rels): + if idx == pos: # position found + if previous < r.order - 1: # space for a new entry + value = previous + 1 + break + value = r.order + r.order += 1 + elif idx > pos: + if previous < r.order - 1: # no renumber necessary any more + break + r.order += 1 + previous = r.order + if value is None: # pos was greater than length, use last order found + value = previous + 1 + return value + + @Lazy + def conceptManager(self): + return self.context.getConceptManager() + + @Lazy + def partOf(self): + return self.conceptManager[compoundPredicateName] + diff --git a/compound/interfaces.py b/compound/interfaces.py index 8a9a923..0964fe9 100644 --- a/compound/interfaces.py +++ b/compound/interfaces.py @@ -19,6 +19,9 @@ """ Compound objects like articles, blog posts, storyboard items, ... +This is somehow related to cybertools.composer - so maybe we should +move part of the code defined here to that more generic package. + $Id$ """ @@ -28,34 +31,55 @@ from zope import interface, component, schema from loops.util import _ +compoundPredicateName = 'ispartof' + + class ICompound(Interface): """ A compound is a concept that is built up of other objects, its - components. + parts or components. - These components are typically resources, but may also be other - concepts that should provide the ICompound interface as their - type interface. The components are assigned in an ordered way - so that the components are accessed in a reproducible order. + These parts are typically resources, but may also be other + concepts that may/should provide the ICompound interface as their + type interface. The parts are assigned in an ordered way + so that the parts are accessed in a reproducible order. - The components are bound to the compound via standard resource + The parts are bound to the compound via standard resource or concept relations using a special predicate, ``ispartof``. """ - components = Attribute('Objects (resources or other compounds) that ' - 'this object consists of') + def getParts(): + """ Return the objects (resources or other compounds) that + this object consists of in the defined order. + """ def add(obj, position=None): - """ Add a component to the compound. + """ Add a part to the compound. If the ``position`` argument is None, append it to the end - of the current list of components, otherwise insert it before + of the current list of parts, otherwise insert it before the position given. The numbering of the positions is like for Python lists, so 0 means before the first one, -1 before the last one. """ - def reorder(components): - """ Change the order settings of the relations that connect the - components given (a sequence) to the compound so that they are - ordered according to this sequence. + def remove(obj, position=None): + """ Remove the object given from the context object's list of + parts. + + If the object given is not present raise an error. If there + is more than one part relation to the object given + all of them will be removed, except if the position argument + is given in which case only the object at the given position + will be removed. + """ + + def reorder(parts): + """ Change the order settings of the relations that connect the + parts given (a sequence) to the compound so that they are + ordered according to this sequence. + + If the parts`` argument contains a value that is not + in the context objects`s list of parts raise an error. If the + context object has parts that are not in the ``parts`` argument + they will be moved to the end of the list. """ diff --git a/concept.py b/concept.py index 46dbea4..ce59774 100644 --- a/concept.py +++ b/concept.py @@ -197,10 +197,11 @@ class Concept(Contained, Persistent): def assignParent(self, concept, predicate=None, order=0, relevance=1.0): concept.assignChild(self, predicate, order, relevance) - def deassignChild(self, child, predicates=None): + def deassignChild(self, child, predicates=None, order=None): registry = component.getUtility(IRelationRegistry) for rel in self.getChildRelations(predicates, child): - registry.unregister(rel) + if order is None or rel.order == order: + registry.unregister(rel) def deassignParent(self, parent, predicates=None): parent.deassignChild(self, predicates) diff --git a/constraint/interfaces.py b/constraint/interfaces.py index a965708..a9d0552 100644 --- a/constraint/interfaces.py +++ b/constraint/interfaces.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2007 Helmut Merz helmutm@cy55.de +# Copyright (c) 2008 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by