[Checkins] SVN: z3c.relationfield/trunk/ Improve broken relation support.

Martijn Faassen faassen at infrae.com
Mon Jan 19 14:51:43 EST 2009


Log message for revision 94856:
  Improve broken relation support.
  

Changed:
  U   z3c.relationfield/trunk/CHANGES.txt
  U   z3c.relationfield/trunk/src/z3c/relationfield/README.txt
  U   z3c.relationfield/trunk/src/z3c/relationfield/event.py
  U   z3c.relationfield/trunk/src/z3c/relationfield/interfaces.py
  U   z3c.relationfield/trunk/src/z3c/relationfield/relation.py

-=-
Modified: z3c.relationfield/trunk/CHANGES.txt
===================================================================
--- z3c.relationfield/trunk/CHANGES.txt	2009-01-19 19:50:42 UTC (rev 94855)
+++ z3c.relationfield/trunk/CHANGES.txt	2009-01-19 19:51:42 UTC (rev 94856)
@@ -4,8 +4,24 @@
 0.3 (unreleased)
 ================
 
-* ...
+* Introduce two new interfaces: ``IHasOutgoingRelations`` and
+  ``IHasIncomingRelations``. ``IHasOutgoingRelations`` should be provided
+  by objects that actually have relations set on them, so that 
+  they can be properly cataloged. ``IHasIncomingRelations`` should be
+  set on objects that can be related to, so that broken relations
+  can be properly tracked. ``IHasRelations`` now extends both,
+  so if you provide those on your object you have an object that can
+  have both outgoing as well as incoming relations.
 
+* Improve broken relations support. When you now break a relation (by
+  removing the relation target), ``to_id`` and ``to_object`` become
+  ``None``. ``to_path`` however will remain the path that the relation
+  last pointed to. ``TemporaryRelation`` objects that when realized
+  are broken relations can also be created.
+
+  You can also for broken status by calling ``isBroken`` on a
+  relation.
+
 0.2 (2009-01-08)
 ================
 

Modified: z3c.relationfield/trunk/src/z3c/relationfield/README.txt
===================================================================
--- z3c.relationfield/trunk/src/z3c/relationfield/README.txt	2009-01-19 19:50:42 UTC (rev 94855)
+++ z3c.relationfield/trunk/src/z3c/relationfield/README.txt	2009-01-19 19:51:42 UTC (rev 94856)
@@ -40,7 +40,9 @@
 
 The ``IHasRelations`` marker interface is needed to let the relations
 on ``Item`` be cataloged (when they are put in a container and removed
-from it, for instance).
+from it, for instance). It is in fact a combination of
+``IHasIncomingRelations`` and ``IHasOutgoingRelations``, which is fine
+as we want items to support both.
 
 Finally we need a test application::
 
@@ -159,6 +161,8 @@
 
   >>> sorted(root['b'].rel.from_interfaces_flattened)
   [<InterfaceClass zope.app.container.interfaces.IContained>,
+   <InterfaceClass z3c.relationfield.interfaces.IHasIncomingRelations>,
+   <InterfaceClass z3c.relationfield.interfaces.IHasOutgoingRelations>,
    <InterfaceClass z3c.relationfield.interfaces.IHasRelations>,   
    <InterfaceClass __builtin__.IItem>, 
    <InterfaceClass zope.location.interfaces.ILocation>, 
@@ -166,6 +170,8 @@
    <InterfaceClass zope.interface.Interface>]
   >>> sorted(root['b'].rel.to_interfaces_flattened)
   [<InterfaceClass zope.app.container.interfaces.IContained>,
+   <InterfaceClass z3c.relationfield.interfaces.IHasIncomingRelations>,
+   <InterfaceClass z3c.relationfield.interfaces.IHasOutgoingRelations>,
    <InterfaceClass z3c.relationfield.interfaces.IHasRelations>,
    <InterfaceClass __builtin__.IItem>, 
    <InterfaceClass zope.location.interfaces.ILocation>, 
@@ -194,7 +200,10 @@
   ...   def path(self, obj):
   ...       return obj.__name__
   ...   def resolve(self, path):
-  ...       return root[path]
+  ...       try:
+  ...           return root[path]
+  ...       except KeyError:
+  ...           raise ValueError("Cannot resolve path %s" % path)
 
   >>> grok.testing.grok_component('ObjectPath', ObjectPath)
   True
@@ -382,6 +391,43 @@
   >>> l[0].from_path
   u'b'
 
+Breaking a relation
+===================
+
+We have a relation from ``b`` to ``c`` right now::
+
+  >>> sorted(catalog.findRelations({'to_id': c_id})) 
+  [<z3c.relationfield.relation.RelationValue object at ...>]
+
+The relation isn't broken::
+  
+  >>> b.rel.isBroken()
+  False
+
+We are now going to break this relation by removing ``c``::
+
+  >>> del root['c']
+
+The relation is broken now::
+
+  >>> b.rel.isBroken()
+  True
+
+The original relation still has a ``to_path``::
+
+  >>> b.rel.to_path
+  u'c'
+
+It's broken however as there is no ``to_object``::
+
+  >>> b.rel.to_object is None
+  True
+
+The ``to_id`` is also gone::
+
+  >>> b.rel.to_id is None
+  True
+
 RelationList
 ============
 
@@ -492,3 +538,26 @@
   >>> after3 = sorted(catalog.findRelations({'to_id': a_id}))
   >>> len(after3) > len(after2) 
   True
+
+Broken temporary relations
+==========================
+
+Let's create another temporary relation, this time a broken one that
+cannot be resolved::
+
+  >>> root['e'] = Item()
+  >>> root['e'].rel = TemporaryRelationValue('nonexistent')
+
+Let's try realizing this relation::
+
+  >>> realize_relations(root['e'])
+
+We end up with a broken relation::
+  
+  >>> root['e'].rel.isBroken()
+  True
+
+It's pointing to the nonexistent path::
+
+  >>> root['e'].rel.to_path
+  'nonexistent'

Modified: z3c.relationfield/trunk/src/z3c/relationfield/event.py
===================================================================
--- z3c.relationfield/trunk/src/z3c/relationfield/event.py	2009-01-19 19:50:42 UTC (rev 94855)
+++ z3c.relationfield/trunk/src/z3c/relationfield/event.py	2009-01-19 19:51:42 UTC (rev 94856)
@@ -3,19 +3,20 @@
 from zope.interface import providedBy
 from zope.schema import getFields
 from zope import component
-from zope.app.intid.interfaces import IIntIds
+from zope.app.intid.interfaces import IIntIds, IIntIdRemovedEvent
 from zope.app.container.interfaces import (IObjectAddedEvent,
                                            IObjectRemovedEvent)
 from zope.lifecycleevent.interfaces import IObjectModifiedEvent
 
 from zc.relation.interfaces import ICatalog
 
-from z3c.relationfield.interfaces import (IHasRelations,
+from z3c.relationfield.interfaces import (IHasOutgoingRelations,
+                                          IHasIncomingRelations,
                                           IRelation, IRelationList,
                                           IRelationValue,
                                           ITemporaryRelationValue)
 
- at grok.subscribe(IHasRelations, IObjectAddedEvent)
+ at grok.subscribe(IHasOutgoingRelations, IObjectAddedEvent)
 def addRelations(obj, event):
     """Register relations.
 
@@ -24,7 +25,7 @@
     for name, relation in _relations(obj):
          _setRelation(obj, name, relation)
 
- at grok.subscribe(IHasRelations, IObjectRemovedEvent)
+ at grok.subscribe(IHasOutgoingRelations, IObjectRemovedEvent)
 def removeRelations(obj, event):
     """Remove relations.
 
@@ -36,7 +37,7 @@
         if relation is not None:
             catalog.unindex(relation)
 
- at grok.subscribe(IHasRelations, IObjectModifiedEvent)
+ at grok.subscribe(IHasOutgoingRelations, IObjectModifiedEvent)
 def updateRelations(obj, event):
     """Re-register relations, after they have been changed.
     """
@@ -53,6 +54,23 @@
     # add new relations
     addRelations(obj, event)
 
+ at grok.subscribe(IIntIdRemovedEvent)
+def breakRelations(event):
+    """Break relations on any object pointing to us.
+
+    That is, store the object path on the broken relation.
+    """
+    obj = event.object
+    if not IHasIncomingRelations.providedBy(obj):
+        return
+    catalog = component.getUtility(ICatalog)
+    intids = component.getUtility(IIntIds)
+
+    # find all relations that point to us
+    rels = list(catalog.findRelations({'to_id': intids.getId(obj)}))
+    for rel in rels:
+        rel.broken(rel.to_path)
+    
 def realize_relations(obj):
     """Given an object, convert any temporary relations on it to real ones.
     """

Modified: z3c.relationfield/trunk/src/z3c/relationfield/interfaces.py
===================================================================
--- z3c.relationfield/trunk/src/z3c/relationfield/interfaces.py	2009-01-19 19:50:42 UTC (rev 94855)
+++ z3c.relationfield/trunk/src/z3c/relationfield/interfaces.py	2009-01-19 19:51:42 UTC (rev 94856)
@@ -1,13 +1,29 @@
 from zope.interface import Interface, Attribute
 from zope.schema.interfaces import IField, IList
 
-class IHasRelations(Interface):
-    """Marker interface indicating that the object has relations.
+class IHasOutgoingRelations(Interface):
+    """Marker interface indicating that the object has outgoing relations.
 
-    Use this interface to make sure that the relations get added and
-    removed from the catalog when appropriate.
+    Provide this interface on your own objects with outgoing relations
+    to make sure that the relations get added and removed from the
+    catalog when appropriate.
     """
 
+class IHasIncomingRelations(Interface):
+    """Marker interface indicating the the object has incoming relations.
+
+    Provide this interface on your own objects with incoming
+    relations. This will make sure that broken relations to that
+    object are tracked properly.
+    """
+    
+class IHasRelations(IHasIncomingRelations, IHasOutgoingRelations):
+    """Marker interface indicating that the object has relations of any kind.
+
+    Provide this interface if the object can have both outgoing as
+    well as incoming relations.
+    """
+
 class IRelation(IField):
     pass
 
@@ -34,11 +50,15 @@
     
     from_attribute = Attribute("The name of the attribute of the from object.")
     
-    to_object = Attribute("The object this relation is pointing to.")
+    to_object = Attribute("The object this relation is pointing to. "
+                          "This value is None if the relation is broken.")
 
-    to_id = Attribute("Id of the object this relation is pointing to.")
+    to_id = Attribute("Id of the object this relation is pointing to. "
+                      "This value is None if the relation is broken.")
 
-    to_path = Attribute("The path of the object this relation is pointing to.")
+    to_path = Attribute("The path of the object this relation is pointing to. "
+                        "If the relation is broken, this value will still "
+                        "point to the last path the relation pointed to.")
 
     to_interfaces = Attribute("The interfaces of the to-object.")
 
@@ -46,6 +66,21 @@
         "The interfaces of the to object, flattened. "
         "This includes all base interfaces.")
 
+    def broken(to_path):
+        """Set this relation as broken.
+
+        to_path - the (non-nonexistent) path that the relation pointed to.
+
+        The relation will be broken. If you provide
+        IHasIncomingRelations on objects that have incoming relations,
+        relations will be automatically broken when you remove an
+        object.
+        """
+
+    def isBroken():
+        """Return True if this is a broken relation.
+        """
+
 class ITemporaryRelationValue(Interface):
     """A temporary relation.
 

Modified: z3c.relationfield/trunk/src/z3c/relationfield/relation.py
===================================================================
--- z3c.relationfield/trunk/src/z3c/relationfield/relation.py	2009-01-19 19:50:42 UTC (rev 94855)
+++ z3c.relationfield/trunk/src/z3c/relationfield/relation.py	2009-01-19 19:51:42 UTC (rev 94856)
@@ -13,13 +13,15 @@
 class RelationValue(Persistent):
     implements(IRelationValue)
 
+    _broken_to_path = None
+    
     def __init__(self, to_id):
         self.to_id = to_id
-        # these will be set automatically by RelationProperty
+        # these will be set automatically by events
         self.from_object = None
         self.__parent__ = None
         self.from_attribute = None
-
+    
     @property
     def from_id(self):
         intids = component.getUtility(IIntIds)
@@ -43,6 +45,8 @@
 
     @property
     def to_path(self):
+        if self.to_object is None:
+            return self._broken_to_path
         return _path(self.to_object)
 
     @property
@@ -58,6 +62,13 @@
             return cmp(self.to_id, None)
         return cmp(self.to_id, other.to_id)
 
+    def broken(self, to_path):
+        self._broken_to_path = to_path
+        self.to_id = None
+
+    def isBroken(self):
+        return self.to_id is None
+
 class TemporaryRelationValue(Persistent):
     """A relation that isn't fully formed yet.
 
@@ -71,13 +82,20 @@
 
     def convert(self):
         object_path = component.getUtility(IObjectPath)
-        # XXX what if we have a broken relation?
-        to_object = object_path.resolve(self.to_path)
+        try:
+            to_object = object_path.resolve(self.to_path)
+        except ValueError:
+            # we have a broken relation, so create one
+            result = RelationValue(None)
+            result.broken(self.to_path)
+            return result
         intids = component.getUtility(IIntIds)
         to_id = intids.getId(to_object)
         return RelationValue(to_id)
     
 def _object(id):
+    if id is None:
+        return None
     intids = component.getUtility(IIntIds)
     try:
         return intids.getObject(id)



More information about the Checkins mailing list