[Zope-dev] RFC: RelationAware class for relations between objects
roche@upfrontsystems.co.za
roche@upfrontsystems.co.za
Fri, 25 Apr 2003 23:18:19 +0200
--E39vaYmALEf/7YXx
Content-Type: text/plain; charset=iso-8859-1
Content-Disposition: inline
Content-Transfer-Encoding: 8bit
There has been a lot of discussion about the need for a service that
manages relations between objects on zope3-dev lately and in the past.
I thought this would be a good time to share some code we have written
to make relations a bit easier in Zope 2 and to invite some comments on
it. The attached module provides a mixin class that collaborates with
Max M's mxmRelations product almost like CatalogAwareness collaborates
with the ZCatalog. I really hope that this can be an acceptable interim
solution until relations are better managed by the ZODB or by some
service in Zope 3.
One area of contention is the overriding of __of__ to compute relations
as attributes on objects. What kind of performance hit will this cause
if one has a long chain of relations?
I appreciate any comments.
--
Roché Compaan
Upfront Systems http://www.upfrontsystems.co.za
--E39vaYmALEf/7YXx
Content-Type: text/plain; charset=iso-8859-1
Content-Disposition: attachment; filename="RelationAware.py"
Content-Transfer-Encoding: 8bit
from Acquisition import ImplicitAcquisitionWrapper, aq_base, aq_inner
class RelationAware:
"""
The RelationAware mixin class provides an object with
transparent relations management.
An object may specify that it has one or more predefined
relations, by specifying a _relations structure in its
class:
_relations=(
{'id': 'Person_to_Organisation',
'attr': 'Organisation'¸
'cardinality': 'single'},
)
'id' refers to the id of the mxmRelation that needs to exist in the
default_relations_mngr (This is just a folder with the id
'relations' in the acquisition path of the object. It should
contain mxmRelation instances for each relation in the _relations
dictionary. Call 'manage_editRelationsMngr' to override
'default_relations_mngr').
'attr' refers to the attribute that should be computed on the object
to make references like Person.Organisation.Name possible.
'cardinality' can be either 'single' or 'multiple'. This is used to
help with the computation of the attribute. If the the 'cardinality'
is 'multiple' we return a list otherwise we return a single object.
TODO: 'cardinality' is not currently used to strictly enforce the
cardinality of relations.
To add and edit the relations for an object call
'manage_addRelations' and 'manage_changeRelations'. It is the
responsibility of the subclass to call these methods - they are not
called automatically. Both methods accept either values set on the
REQUEST or keyword arguments. The keys should match the value of
'attr' defined in '_relations' and key values should be paths to
objects eg.:
folder.manage_addProduct['MyProduct'].manage_addPerson('pete')
person = folder.pete
person.manage_changeProperties(Name='Pete', Surname='Smith')
person.manage_addRelations(
Organisation='/path/to/this_organisation')
or in the case where pete exists
person = folder.pete
person.manage_changeRelations(
Organisation='/path/to/that_organisation')
TODO: Handle copy and paste of objects
"""
_relations = ()
meta_type = 'RelationAware'
# By default we assume there is a folder with id == 'relations' in
# which we store all mxmRelations
default_relations_mngr = 'relations'
def __of__(self, parent, _iaw=ImplicitAcquisitionWrapper):
# Bound to unwrapped parent, just try again
if type(parent) is not _iaw or not hasattr(parent,'REQUEST'):
return self
# Create our canonical form
new_self = _iaw(self,parent)
if type(self) is _iaw: # Been wrapped already?
return new_self # No special handling
if not hasattr(self, '_relations'):
return new_self
dm = parent.aq_acquire(self.default_relations_mngr)
for d in self._relations:
id, attr = d['id'], d['attr']
mxmRelation = dm[id]
relations = mxmRelation.get(new_self)
spec = self.relationSpec(id)
if spec['cardinality'] == 'single' and relations:
setattr(new_self, attr, relations[0])
else:
setattr(new_self, attr, relations)
return new_self
def manage_editRelationMngr(self, mngr_id):
""" Set the relations manager
"""
self.default_relations_mngr = mngr_id
def getRelationMngr(self):
""" Return the relations data manager
TODO: should we raise an exception if not found?
"""
if hasattr(self, self.default_relations_mngr):
return getattr(self, self.default_relations_mngr)
def manage_addRelations(self, REQUEST=None, **kw):
""" Add relations for RelationAware subclasses. Values can be
passed as keyword arguments or set on the REQUEST.
Keyword arguments or REQUEST is search for keys matching the
ids of relations defined in '_relations'. Key values must be
paths.
"""
relationMngr = self.getRelationMngr()
if not relationMngr:
return
if REQUEST is None:
props={}
else: props=REQUEST
props.update(kw)
for d in self._relations:
id, attr = d['id'], d['attr']
value = props.get(attr, '')
if not value: continue
# Add relations
mxmRelation = relationMngr[id]
if type(value) == type(''):
value = [value]
for path in value:
relate_to = self.restrictedTraverse(path)
mxmRelation.relate(self, relate_to)
def manage_changeRelations(self, REQUEST=None, **kw):
""" Edit relations for RelationAware subclasses. Values can be
passed as keyword arguments or set on the REQUEST.
Keyword arguments or REQUEST is search for keys matching the
ids of relations defined in '_relations'. Key values must be
paths.
Orphaned relations are removed.
"""
relationMngr = self.getRelationMngr()
if not relationMngr:
return
if REQUEST is None:
props={}
else: props=REQUEST
props.update(kw)
for d in self._relations:
id, attr = d['id'], d['attr']
mxmRelation = relationMngr[id]
# Get existing relations
old_relations = self.relationValues(id)
value = props.get(attr, '')
if not value:
mxmRelation.unrelate(self, old_relations)
continue
# Add relations
if type(value) == type(''):
value = [value]
for path in value:
relate_to = self.unrestrictedTraverse(path)
if relate_to in old_relations:
old_relations.remove(relate_to)
else:
mxmRelation.relate(self, relate_to)
# Unrelate orphaned relations
mxmRelation.unrelate(self, old_relations)
def relationSpec(self, relation_id):
""" Return relation with id == relation_id
"""
return filter(lambda i, n=relation_id: i['id'] == n,
self._relations)[0]
def relationIds(self):
""" Return a list of relation ids
"""
return map(lambda i: i['id'], self._relations)
def relationValues(self, relation_id):
""" Return the objects for a relationship
"""
mxmRelation = self.getRelationMngr()[relation_id]
relations = mxmRelation.get(self)
spec = self.relationSpec(relation_id)
if spec['cardinality'] == 'single':
return relations[0]
else:
return relations
--E39vaYmALEf/7YXx--