Package: infrae.plone.relations.schema
ContentsInterface definitionCreate a simple content for test purposeUtility to display relationSimple field useDirect set, and reverse accessDeletionField independenceMore ConstraintsUse of context objectMany to Many Relation InterfaceSimple test of the interfaceDirectionBigger example with transitivityAccessorDeletionChangesCreditsThe purpose of this extension is to provide a Zope 3 schema for plone
relations. This has been tested with Plone 2.5 and Plone 3.Interface definitionA new field:>>> from infrae.plone.relations.schema import PloneRelationA simple interface with an field:>>> from zope.interface import Interface, implements
>>> class IContent(Interface):
...    """Sample interface."""
...    relation = PloneRelation(relation="my relation")I can get the field from the interface:>>> r_field = IContent.get('relation')And this is a field:>>> from zope.interface.verify import verifyObject
>>> from zope.schema.interfaces import IField
>>> verifyObject(IField, r_field)
TrueNow, a field to look up reverse relations:>>> class IBackContent(Interface):
...    """Sample interface."""
...    relation = PloneRelation(relation="my relation",
...                             reverse=True)Create a simple content for test purposeAnd a simple implementation:>>> from OFS.Folder import Folder
>>> class BaseContent(Folder):
...    def __init__(self, id):
...       super(BaseContent, self).__init__()
...       self.id = id
...    def UID(self):
...       return 'uid-%s' % self.idUID are used by the context factory.Standard base class:>>> class MyContent(BaseContent):
...    implements(IContent)And:>>> class MyBackContent(BaseContent):
...    implements(IBackContent)Now, create some items:>>> for id in range(1, 5):
...    name = 'it%d' % id
...    item = MyContent(name)
...    self.portal._setObject(name, item)
'it1'
'it2'
'it3'
'it4'
>>> it1 = self.portal.it1
>>> it2 = self.portal.it2
>>> it3 = self.portal.it3
>>> it4 = self.portal.it4

>>> itb1 = MyBackContent('itb1')
>>> self.portal._setObject('itb1', itb1)
'itb1'
>>> itb1 = self.portal.itb1Utility to display relationAn helper to display a relation:>>> def display(rels):
...     for rel in rels:
...        print "Objects: %s" % list(rel['objects'])
...        if rel.has_key("context"):
...            print "Context: %s" % rel['context']
...     if not rels:
...        print "Empty"Simple field useDirect set, and reverse accessAnd try some validation on data. Data is a list of dictionary,
representing all relations of the field. In the dictionary:objects: represent a list of object for the relation;context: may be an object stored as context of the relation.Example:>>> bad_relation1 = [{'bad': None},]
>>> r_field.validate(bad_relation1)
Traceback (most recent call last):
  ...
ValidationError: Invalid structure
>>> good_relation = [{'objects': [it2, itb1]},]
>>> r_field.validate(good_relation)And set the field:>>> r_field.set(it1, good_relation)And get data from the field:>>> relation = r_field.get(it1)
>>> relation
[{'objects': <plone.relations.relationships.IntIdSubObjectWrapper object at ...>}]
>>> display(relation)
Objects: [<MyContent at /plone/it2>, <MyBackContent at /plone/itb1>]Now, we can ask fromitb1content, which has a reverse field:>>> rb_field = IBackContent.get('relation')
>>> relation = rb_field.get(itb1)
>>> relation
[{'objects': <plone.relations.relationships.IntIdSubObjectWrapper object at ...>}]
>>> display(relation)
Objects: [<MyContent at /plone/it1>]We update the relation:>>> good_relation = [{'objects': [it2, it3]},]
>>> r_field.set(it1, good_relation)So change is reflected:>>> display(r_field.get(it1))
Objects: [<MyContent at /plone/it2>, <MyContent at /plone/it3>]And there is no more relation in the reverse field:>>> rb_field = IBackContent.get('relation')
>>> rb_field.get(itb1)
[]Now, set on reverse field:>>> good_relation = [{'objects': [it1, it2]}]
>>> rb_field.set(itb1, good_relation)
>>> display(rb_field.get(itb1))
Objects: [<MyContent at /plone/it1>, <MyContent at /plone/it2>]And on the normal:>>> display(r_field.get(it1))
Objects: [<MyContent at /plone/it2>, <MyContent at /plone/it3>]
Objects: [<MyBackContent at /plone/itb1>]DeletionYou can delete value by setting the relation to an empty list[]:>>> display(r_field.get(it2))
Objects: [<MyBackContent at /plone/itb1>]
>>> r_field.set(it2, [])
>>> display(r_field.get(it2))
Empty
>>> display(rb_field.get(itb1))
Objects: [<MyContent at /plone/it1>]And:>>> display(r_field.get(it1))
Objects: [<MyContent at /plone/it2>, <MyContent at /plone/it3>]
Objects: [<MyBackContent at /plone/itb1>]
>>> r_field.set(it1, [])
>>> display(r_field.get(it1))
Empty
>>> display(rb_field.get(itb1))
EmptyField independenceOne other relation schema:>>> class IComplexContent(Interface):
...    """A content with two relation."""
...    relation1 = PloneRelation(relation="relation1")
...    relation2 = PloneRelation(relation="relation2")And the related content:>>> class MyComplexContent(BaseContent):
...    implements(IComplexContent)Create three objects like this:>>> itcx1 = MyComplexContent("itcx1")
>>> self.portal._setObject("itcx1", itcx1)
'itcx1'
>>> itcx1 = self.portal.itcx1
>>> itcx2 = MyComplexContent("itcx2")
>>> self.portal._setObject("itcx2", itcx2)
'itcx2'
>>> itcx2 = self.portal.itcx2
>>> itcx3 = MyComplexContent("itcx3")
>>> self.portal._setObject("itcx3", itcx3)
'itcx3'
>>> itcx3 = self.portal.itcx3Now, add relation:>>> r1_field = IComplexContent.get("relation1")
>>> r1_field.set(itcx1, [{'objects': [itcx2,]}])
>>> display(r1_field.get(itcx1))
Objects: [<MyComplexContent at /plone/itcx2>]
>>> r2_field = IComplexContent.get("relation2")
>>> r2_field.set(itcx1, [{'objects': [itcx3,]}])
>>> display(r2_field.get(itcx1))
Objects: [<MyComplexContent at /plone/itcx3>]And delete one:>>> r2_field.set(itcx1, [])
>>> display(r2_field.get(itcx1))
Empty
>>> display(r1_field.get(itcx1))
Objects: [<MyComplexContent at /plone/itcx2>]More ConstraintsNow, you have to give at least 1 value, and no more than 3:>>> class ILengthContent(Interface):
...    """Sample interface with length control."""
...    relation = PloneRelation(relation="my relation",
...                             min_length=1,
...                             max_length=3)The field implementsIMinMaxLen:>>> from zope.schema.interfaces import IMinMaxLen
>>> rl_field = ILengthContent.get('relation')
>>> verifyObject(IMinMaxLen, rl_field)
TrueOk, now some bad tries:>>> bad_relation = []
>>> rl_field.validate(bad_relation)
Traceback (most recent call last):
  ...
TooSmall: Less than 1 values

>>> bad_relation = [{'objects': [it2,]},
...                 {'objects': [it3,]},
...                 {'objects': [it4,]},
...                 {'objects': [itb1,]},]
>>> rl_field.validate(bad_relation)
Traceback (most recent call last):
  ...
TooBig: More than 3 valuesAnd now, one correct:>>> good_relation = [{'objects': [it2,]},]
>>> rl_field.validate(good_relation)But we want also to have uniques objects in the relation:>>> class IUniqueContent(Interface):
...    """Sample interface only one item per relation."""
...    relation = PloneRelation(relation="my relation",
...                             unique=True)
>>> ru_field = IUniqueContent.get('relation')Some tries now:>>> bad_relation = [{'objects': [it2, it3,]}]
>>> ru_field.validate(bad_relation)
Traceback (most recent call last):
  ...
ValidationError: Not uniques values in relation

>>> good_relation = [{'objects': [it2,]}]
>>> ru_field.validate(good_relation)We want that every object in the relation implements a particular
interface:>>> class IConstraintContent(Interface):
...    """Sample interface with constraint on relation."""
...    relation = PloneRelation(relation="my relation",
...                             relation_schema=IUniqueContent)
>>> rs_field = IConstraintContent.get('relation')Use of context objectTwo interfaces let you work with context objects:>>> from infrae.plone.relations.schema import IPloneRelationContext
>>> from infrae.plone.relations.schema import IPloneRelationContextFactoryThis two next import are helpers, but you can use them since it’s good
content start:>>> from infrae.plone.relations.schema import BasePloneRelationContext
>>> from infrae.plone.relations.schema import BasePloneRelationContextFactoryThe following context interface:>>> class IContextObject(IPloneRelationContext):
...    """Simple context object."""And its corresponding object:>>> class MyContextObject(BasePloneRelationContext):
...    implements(IContextObject)We will declare the field like this:>>> class IContentWithContext(Interface):
...    """Simple content with a context."""
...    relation = PloneRelation(relation="context relation",
...                             context_schema=IContextObject)We want an object with this schema:>>> class MyContentWithContext(BaseContent):
...    implements(IContentWithContext)Create the object:>>> itc1 = MyContentWithContext('itc1')
>>> self.portal._setObject('itc1', itc1)
'itc1'
>>> itc1 = self.portal.itc1Prepare one context object:>>> ctxt_fac = BasePloneRelationContextFactory(MyContextObject, IContextObject)
>>> verifyObject(IPloneRelationContextFactory, ctxt_fac)
True
>>> ctxt1 = ctxt_fac(itc1, it1, dict())
>>> ctxt1
<MyContextObject at /plone/itc1/uid-it1>
>>> verifyObject(IContextObject, ctxt1)
TrueGet the field:>>> rc_field = IContentWithContext.get('relation')Now we can try this relation:>>> bad_relation = [{'objects': [it2, itb1,], 'context': it3,}]
>>> rc_field.validate(bad_relation)
Traceback (most recent call last):
  ...
ValidationError: Invalid context
>>> good_relation = [{'objects': [it2, itb1,], 'context': ctxt1,}]
>>> rc_field.validate(good_relation)
>>> rc_field.set(itc1, good_relation)If we consult the relation:>>> display(rc_field.get(itc1))
Objects: [<MyContent at /plone/it2>, <MyBackContent at /plone/itb1>]
Context: <MyContextObject at uid-it1>Many to Many Relation InterfaceThis interface provides a more generic way to edit relations than the
one provided byplone.app.relations, to let the Zope 3 schema work
in both way (normal access to the relation, and reverse access).Create simple content:>>> from OFS.SimpleItem import SimpleItem
>>> class BaseContent(SimpleItem):
...    def __init__(self, id):
...       super(BaseContent, self).__init__()
...       self.id = id


>>> for num in range(1, 20):
...    id = 'it%02d' % num
...    it = BaseContent(id)
...    _ = self.portal._setObject(id, it)

>>> self.portal.it01
<BaseContent at /plone/it01>Contents must beIPersistent:>>> from persistent import IPersistent
>>> from zope.interface.verify import verifyObject
>>> verifyObject(IPersistent, self.portal.it01)
TrueSimple test of the interfaceWe have a new adapter to work on your relation:>>> from infrae.plone.relations.schema import IManyToManyRelationship
>>> manager = IManyToManyRelationship(self.portal.it01)
>>> verifyObject(IManyToManyRelationship, manager)
TrueOk, try to add relation:>>> rel = manager.createRelationship((self.portal.it11, self.portal.it12,),
...                                  sources=(self.portal.it02,),
...                                  relation='test')
>>> list(rel.sources)
[<BaseContent at /plone/it01>, <BaseContent at /plone/it02>]
>>> list(rel.targets)
[<BaseContent at /plone/it11>, <BaseContent at /plone/it12>]Now, we can retrieve a list of relation:>>> list(manager.getRelationships())
[<Relationship 'test' from (<BaseContent at /plone/it01>, <BaseContent at /plone/it02>) to (<BaseContent at /plone/it11>, <BaseContent at /plone/it12>)>]DirectionYou can reverse the way a relation works, with thesetDirectionmethod:>>> rel = manager.createRelationship(self.portal.it05, relation='reverse')
>>> list(rel.targets)
[<BaseContent at /plone/it05>]
>>> manager.setDirection(False)
>>> rel = manager.createRelationship(self.portal.it04, relation='reverse')
>>> list(rel.targets)
[<BaseContent at /plone/it01>]You have also the transitivity for search:>>> manager = IManyToManyRelationship(self.portal.it04)
>>> list(manager.getRelationshipChains(relation='reverse',
...                                    target=self.portal.it05,
...                                    maxDepth=2))
[(<Relationship 'reverse' from (<BaseContent at /plone/it04>,) to (<BaseContent at /plone/it01>,)>, <Relationship 'reverse' from (<BaseContent at /plone/it01>,) to (<BaseContent at /plone/it05>,)>)]But relation are always followed from source to target. So if we
reverse the search, we won’t found a result:>>> manager.setDirection(False)
>>> list(manager.getRelationshipChains(relation='reverse',
...                                    target=self.portal.it05,
...                                    maxDepth=2))
[]Direction just change the meaning of source or target on the relation
object. It’s doesn’t change the relation itself.Bigger example with transitivityTaking back the first test, and add a suite:>>> manager = IManyToManyRelationship(self.portal.it16)
>>> manager.setDirection(False)
>>> rel = manager.createRelationship((self.portal.it12, self.portal.it14),
...                                  relation='test')
>>> manager.setDirection(True)
>>> rel = manager.createRelationship((self.portal.it17, self.portal.it18),
...                                  sources=(self.portal.it19,),
...                                  relation='test')New chain try:>>> manager = IManyToManyRelationship(self.portal.it02)
>>> list(manager.getRelationshipChains(relation='test',
...                                    target=self.portal.it18,
...                                    maxDepth=3))
[(<Relationship 'test' from (<BaseContent at /plone/it01>, <BaseContent at /plone/it02>) to (<BaseContent at /plone/it11>, <BaseContent at /plone/it12>)>, <Relationship 'test' from (<BaseContent at /plone/it12>, <BaseContent at /plone/it14>) to (<BaseContent at /plone/it16>,)>, <Relationship 'test' from (<BaseContent at /plone/it16>, <BaseContent at /plone/it19>) to (<BaseContent at /plone/it17>, <BaseContent at /plone/it18>)>)]AccessorgetTargetsreturns a lazy list of objects having a relation with
the given object as source, andgetSourcesreturns a lazy list of
objects having a relation with the given object as target:>>> manager = IManyToManyRelationship(self.portal.it16)
>>> list(manager.getTargets())
[<BaseContent at /plone/it17>, <BaseContent at /plone/it18>]
>>> list(manager.getSources())
[<BaseContent at /plone/it12>, <BaseContent at /plone/it14>]If we reverse the direction:>>> manager.setDirection(False)
>>> list(manager.getTargets())
[<BaseContent at /plone/it12>, <BaseContent at /plone/it14>]
>>> list(manager.getSources())
[<BaseContent at /plone/it17>, <BaseContent at /plone/it18>]DeletionDelete relation:>>> manager.setDirection(True)
>>> manager.deleteRelationship()
>>> list(manager.getRelationships())
[]

>>> manager = IManyToManyRelationship(self.portal.it19)
>>> list(manager.getRelationships())
[<Relationship 'test' from (<BaseContent at /plone/it19>,) to (<BaseContent at /plone/it17>, <BaseContent at /plone/it18>)>]

>>> manager.deleteRelationship(target=self.portal.it17)
>>> list(manager.getRelationships())
[<Relationship 'test' from (<BaseContent at /plone/it19>,) to (<BaseContent at /plone/it18>,)>]

>>> manager.deleteRelationship()
>>> list(manager.getRelationships())
[]

>>> manager = IManyToManyRelationship(self.portal.it01)
>>> manager.deleteRelationship(remove_all_sources=True, multiple=True)
>>> manager = IManyToManyRelationship(self.portal.it02)
>>> list(manager.getRelationships())
[]Changes1.0First release.CreditsPowered by the Flemish government of Belgium, for the application
<http://www.zonderisgezonder.be>.