[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--