From d1110c410ec86cbb1d1f9a55dc7cfc9a789258e5 Mon Sep 17 00:00:00 2001 From: helmutm Date: Thu, 14 Jan 2010 12:12:30 +0000 Subject: [PATCH] include slightly modified copy of five.intid git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@3685 fd906abe-77d9-0310-91a1-e0d9ade77398 --- z2/intid/README.txt | 336 +++++++++++++++++++++++++++++++++ z2/intid/__init__.py | 1 + z2/intid/base.zcml | 51 +++++ z2/intid/browser.py | 5 + z2/intid/cmfdirectoryview.zcml | 49 +++++ z2/intid/configure.zcml | 15 ++ z2/intid/install.pt | 15 ++ z2/intid/intid.py | 135 +++++++++++++ z2/intid/keyreference.py | 115 +++++++++++ z2/intid/lsm.py | 13 ++ z2/intid/overrides.zcml | 10 + z2/intid/registrations.pt | 24 +++ z2/intid/site.py | 133 +++++++++++++ z2/intid/subscriber.zcml | 18 ++ z2/intid/test.zcml | 16 ++ z2/intid/tracking.txt | 168 +++++++++++++++++ z2/intid/unreferenceable.py | 31 +++ z2/intid/utils.py | 23 +++ z2/intid/xx_ftests.py | 28 +++ z2/intid/xx_tests.py | 60 ++++++ 20 files changed, 1246 insertions(+) create mode 100644 z2/intid/README.txt create mode 100644 z2/intid/__init__.py create mode 100644 z2/intid/base.zcml create mode 100644 z2/intid/browser.py create mode 100644 z2/intid/cmfdirectoryview.zcml create mode 100644 z2/intid/configure.zcml create mode 100644 z2/intid/install.pt create mode 100644 z2/intid/intid.py create mode 100644 z2/intid/keyreference.py create mode 100644 z2/intid/lsm.py create mode 100644 z2/intid/overrides.zcml create mode 100644 z2/intid/registrations.pt create mode 100644 z2/intid/site.py create mode 100644 z2/intid/subscriber.zcml create mode 100644 z2/intid/test.zcml create mode 100644 z2/intid/tracking.txt create mode 100644 z2/intid/unreferenceable.py create mode 100644 z2/intid/utils.py create mode 100644 z2/intid/xx_ftests.py create mode 100644 z2/intid/xx_tests.py diff --git a/z2/intid/README.txt b/z2/intid/README.txt new file mode 100644 index 0000000..6a07741 --- /dev/null +++ b/z2/intid/README.txt @@ -0,0 +1,336 @@ +Usage +===== + +First, let make sure the ofs utility provides the interface:: + + >>> from zope.app.intid.interfaces import IIntIds + >>> from five.intid import site + >>> import five.intid.tests as tests + >>> from zope.interface.verify import verifyObject + >>> from zope.component import getAllUtilitiesRegisteredFor + >>> from zope.app.component.hooks import setSite + >>> tests.setUp(self.app) + +Content added before the utility won't be registered(until explicitly +called to). We'll set some up now for later + + >>> tests.manage_addSimpleContent(self.folder, 'mycont1', "My Content") + >>> content1 = self.folder.mycont1 + +`five.intid.site` has convenience functions for adding, get and +removing an IntId utility: `add_intid`, `get_intid`, `del_intid`. + +You can install the utility in a specific location:: + + >>> site.add_intids(self.folder) + >>> folder_intids = site.get_intids(self.folder) + >>> verifyObject(IIntIds, folder_intids) + True + +You can tell `add_intids` to find the site root, and install there. +It will be available everywhere:: + + >>> site.add_intids(self.folder, findroot=True) + >>> root_intids = site.get_intids(self.app) + >>> root_intids + <...IntIds ...> + >>> folder_intids is root_intids + False + +And finally, do a remove:: + + >>> site.del_intids(self.folder, findroot=True) + >>> site.get_intids(self.app) + Traceback (most recent call last): + ... + ComponentLookupError: (, '') + +Before we look at intid events, we need to set the traversal +hook. Once we have done this, when we ask for all registered Intids, +we will get the utility from test folder:: + + >>> setSite(self.folder) + >>> tuple(getAllUtilitiesRegisteredFor(IIntIds)) + (<...IntIds ...>,) + + +When we add content, event will be fired to add keyreference for said +objects the utilities (currently, our content and the utility are +registered):: + + >>> from five.intid.lsm import USE_LSM + >>> tests.manage_addSimpleContent(self.folder, 'mycont2', "My Content") + >>> content2 = self.folder.mycont2 + >>> intid = site.get_intids(self.folder) + >>> if USE_LSM: + ... len(intid.items()) == 1 + ... else: + ... len(intid.items()) == 2 + True + +Pre-existing content will raise a keyerror if passed to the intid +utility:: + + >>> intid.getId(content1) + Traceback (most recent call last): + ... + KeyError: + +We can call the keyreferences, and get the objects back:: + + >>> if USE_LSM: + ... intid.items()[0][1]() + ... else: + ... intid.items()[1][1]() + + +we can get an object's `intid` from the utility like so:: + + >>> ob_id = intid.getId(content2) + +and get an object back like this:: + + >>> intid.getObject(ob_id) + + +these objects are aquisition wrapped on retrieval:: + + >>> type(intid.getObject(ob_id)) + + +We can even turn an unwrapped object into a wrapped object by +resolving it from it's intid, also the intid utility should work +even if it is unwrapped:: + + >>> from Acquisition import aq_base + >>> resolved = intid.getObject(intid.getId(aq_base(content2))) + >>> type(resolved) + + >>> unwrapped = aq_base(intid) + >>> unwrapped.getObject(ob_id) == resolved + True + >>> unwrapped.getId(content2) == ob_id + True + + + + +When an object is added or removed, subscribers add it to the intid +utility, and fire an event is fired +(zope.app.intid.interfaces.IIntIdAddedEvent, +zope.app.intid.interfaces.IIntIdRemovedEvent respectively). + +`five.intid` hooks up these events to redispatch as object events. The +tests hook up a simple subscriber to verify that the intid object +events are fired (these events are useful for catalogish tasks). + + >>> tests.NOTIFIED[0] + ' <...IntIdAddedEvent instance at ...' + +Registering and unregistering objects does not fire these events:: + + >>> tests.NOTIFIED[0] = "No change" + >>> uid = intid.register(content1) + >>> intid.getObject(uid) + + + >>> tests.NOTIFIED[0] + 'No change' + + >>> intid.unregister(content1) + >>> intid.getObject(uid) + Traceback (most recent call last): + ... + KeyError: ... + + >>> tests.NOTIFIED[0] + 'No change' + +Renaming an object should not break the rewrapping of the object: + + >>> self.setRoles(['Manager']) + >>> folder.mycont2.meta_type = 'Folder' # We need a metatype to move + >>> folder.manage_renameObject('mycont2','mycont_new') + >>> moved = intid.getObject(ob_id) + >>> moved + + +Nor should moving it: + + >>> from OFS.Folder import manage_addFolder + >>> manage_addFolder(self.folder, 'folder2', "folder 2") + >>> cut = folder.manage_cutObjects(['mycont_new']) + >>> ignore = folder.folder2.manage_pasteObjects(cut) + >>> moved = intid.getObject(ob_id) + >>> moved + + >>> moved.aq_parent + + +Let's move it back: + + >>> cut = folder.folder2.manage_cutObjects(['mycont_new']) + >>> ignore = folder.manage_pasteObjects(cut) + >>> folder.manage_renameObject('mycont_new','mycont2') + +We can create an object without acquisition so we can be able to +add intid to it : + + >>> from five.intid.tests import DemoPersistent + >>> demo1 = DemoPersistent() + >>> demo1.__parent__ = self.app + >>> from zope.event import notify + >>> from zope.app.container.contained import ObjectAddedEvent + >>> notify(ObjectAddedEvent(demo1)) + >>> nowrappid = intid.getId(demo1) + >>> demo1 == intid.getObject(nowrappid) + True + +This is a good time to take a look at keyreferences, the core part of +this system. + + +Key References in Zope2 +======================= + +Key references are hashable objects returned by IKeyReference. The +hash produced is a unique identifier for whatever the object is +referencing(another zodb object, a hook for sqlobject, etc). + +object retrieval in intid occurs by calling a key reference. This +implementation is slightly different than the zope3 due to +acquisition. + +The factories returned by IKeyReference must persist and this dictates +being especially careful about references to acquisition wrapped +objects as well as return acq wrapped objects as usually expected in +zope2. + + >>> ref = intid.refs[ob_id] + >>> ref + + +The reference object holds a reference to the unwrapped target object +and a property to fetch the app(also, not wrapped ie ):: + + >>> ref.object + + + >>> type(ref.object) + + + >>> ref.root + + +Calling the reference object (or the property wrapped_object) will +return an acquisition object wrapped object (wrapped as it was +created):: + + >>> ref.wrapped_object == ref() + True + + >>> ref() + + + >>> type(ref()) + + + +The resolution mechanism tries its best to end up with the current +request at the end of the acquisition chain, just as it would be +under noraml circumstances:: + + >>> ref.wrapped_object.aq_chain[-1] + + + +The hash calculation is a combination of the database name and the +object's persistent object id(oid):: + + >>> ref.dbname + 'unnamed' + + >>> hash((ref.dbname, ref.object._p_oid)) == hash(ref) + True + + >>> tests.tearDown() + +Acquisition Loops +================= + +five.intid detects loops in acquisition chains in both aq_parent and +__parent__. + +Setup a loop:: + + >>> import Acquisition + >>> class Acq(Acquisition.Acquirer): pass + >>> foo = Acq() + >>> foo.bar = Acq() + >>> foo.__parent__ = foo.bar + +Looking for the root on an object with an acquisition loop will raise +an error:: + + >>> from five.intid import site + >>> site.get_root(foo.bar) + Traceback (most recent call last): + ... + AttributeError: __parent__ loop found + +Looking for the connection on an object with an acquisition loop will +simply return None:: + + >>> from five.intid import keyreference + >>> keyreference.connectionOfPersistent(foo.bar) + +Unreferenceable +=============== + +Some objects implement IPersistent but are never actually persisted, or +contain references to such objects. Specifically, CMFCore directory views +contain FSObjects that are never persisted, and DirectoryViewSurrogates +that contain references to such objects. Because FSObjects are never actually +persisted, five.intid's assumption that it can add a + +For such objects, the unreferenceable module provides no-op subcribers and +adapters to omit such objects from five.intid handling. + + >>> from zope import interface, component + >>> from five.intid import unreferenceable + + >>> from Products.CMFCore import FSPythonScript + >>> foo = FSPythonScript.FSPythonScript('foo', __file__) + >>> self.app._setObject('foo', foo) + 'foo' + + >>> keyref = unreferenceable.KeyReferenceNever(self.app.foo) + Traceback (most recent call last): + ... + NotYet + >>> foo in self.app._p_jar._registered_objects + False + +Objects with no id +================== + +It is possible to attempt to get a key reference for an object that has not +yet been properly added to a container, but would otherwise have a path. +In this case, we raise the NotYet exception to let the calling code defer +as necessary, since the key reference would otherwise resolve the wrong +object (the parent, to be precise) from an incorrect path. + + >>> from zope.app.keyreference.interfaces import IKeyReference + >>> from five.intid.keyreference import KeyReferenceToPersistent + >>> from zope.component import provideAdapter + >>> provideAdapter(KeyReferenceToPersistent) + + >>> from OFS.SimpleItem import SimpleItem + >>> item = SimpleItem('').__of__(self.folder) + >>> '/'.join(item.getPhysicalPath()) + '/test_folder_1_/' + + >>> IKeyReference(item) + Traceback (most recent call last): + ... + NotYet: \ No newline at end of file diff --git a/z2/intid/__init__.py b/z2/intid/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/z2/intid/__init__.py @@ -0,0 +1 @@ +# diff --git a/z2/intid/base.zcml b/z2/intid/base.zcml new file mode 100644 index 0000000..a97d980 --- /dev/null +++ b/z2/intid/base.zcml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + diff --git a/z2/intid/browser.py b/z2/intid/browser.py new file mode 100644 index 0000000..f4d93f9 --- /dev/null +++ b/z2/intid/browser.py @@ -0,0 +1,5 @@ +from zope.app.intid.browser import IntIdsView +from Products.Five import BrowserView + +class FiveIntIdsView(IntIdsView, BrowserView): + """ utility view for five """ diff --git a/z2/intid/cmfdirectoryview.zcml b/z2/intid/cmfdirectoryview.zcml new file mode 100644 index 0000000..c05030a --- /dev/null +++ b/z2/intid/cmfdirectoryview.zcml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + diff --git a/z2/intid/configure.zcml b/z2/intid/configure.zcml new file mode 100644 index 0000000..37692c3 --- /dev/null +++ b/z2/intid/configure.zcml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/z2/intid/install.pt b/z2/intid/install.pt new file mode 100644 index 0000000..022ef5c --- /dev/null +++ b/z2/intid/install.pt @@ -0,0 +1,15 @@ + + + + +
the intids is installed
+
+
+ +
+
+ + diff --git a/z2/intid/intid.py b/z2/intid/intid.py new file mode 100644 index 0000000..b28c608 --- /dev/null +++ b/z2/intid/intid.py @@ -0,0 +1,135 @@ +from Globals import InitializeClass +from persistent import Persistent +from Acquisition import Explicit +from zope.app import zapi +from zope.app.intid import IntIds as z3IntIds +from zope.app.intid.interfaces import IIntIds +from zope.app.intid.interfaces import IntIdAddedEvent, IntIdRemovedEvent +from zope.app.container.interfaces import IObjectAddedEvent, IObjectRemovedEvent +from zope.app.keyreference.interfaces import IKeyReference, NotYet +from zope.event import notify +from zope.interface import implements + +_marker = [] + +class IntIds(z3IntIds): + """ zope2ish intid utility """ + implements(IIntIds) + + meta_type="IntId Utility" + + def __init__(self, id_=IIntIds.__name__): + self.id = self.__name__ = id_ + super(IntIds, self).__init__() + + def getId(self, ob=_marker): + # Compatibility with SimpleItem + if ob is _marker: + return self.__name__ + return z3IntIds.getId(self, ob) + + def register(self, ob): + key = IKeyReference(ob) + res = self.ids.get(key, None) + if res is not None: + return res + uid = self._generateId() + self.refs[uid] = key + self.ids[key] = uid + return uid + + def unregister(self, ob): + key = IKeyReference(ob, None) + if key is None: + return + + uid = self.ids[key] + del self.refs[uid] + del self.ids[key] + +InitializeClass(IntIds) + +class OFSIntIds(IntIds, Explicit): + """Mixin acquisition for non-lsm sites""" + + def manage_fixupOwnershipAfterAdd(self): + pass + def wl_isLocked(self): + return False + +InitializeClass(OFSIntIds) + +# @@ these are "sloppy" subscribers that let objects that have not +# been properly added to the db by +def addIntIdSubscriber(ob, event): + """A subscriber to ObjectAddedEvent + + Registers the object added in all unique id utilities and fires + an event for the catalogs. + """ + utilities = tuple(zapi.getAllUtilitiesRegisteredFor(IIntIds)) + if utilities: # assert that there are any utilites + key = None + try: + key = IKeyReference(ob, None) + except NotYet: + pass + + # Register only objects that adapt to key reference + if key is not None: + for utility in utilities: + utility.register(key) + # Notify the catalogs that this object was added. + notify(IntIdAddedEvent(ob, event)) + +def removeIntIdSubscriber(ob, event): + """A subscriber to ObjectRemovedEvent + + Removes the unique ids registered for the object in all the unique + id utilities. + """ + utilities = tuple(zapi.getAllUtilitiesRegisteredFor(IIntIds)) + if utilities: + key = None + try: + key = IKeyReference(ob, None) + except NotYet: # @@ temporary fix + pass + + # Register only objects that adapt to key reference + if key is not None: + # Notify the catalogs that this object is about to be removed. + notify(IntIdRemovedEvent(ob, event)) + for utility in utilities: + try: + utility.unregister(key) + except KeyError: + pass + + +def moveIntIdSubscriber(ob, event): + """A subscriber to ObjectMovedEvent + + Updates the stored path for the object in all the unique + id utilities. + """ + if IObjectRemovedEvent.providedBy(event) or \ + IObjectAddedEvent.providedBy(event): + return + utilities = tuple(zapi.getAllUtilitiesRegisteredFor(IIntIds)) + if utilities: + key = None + try: + key = IKeyReference(ob, None) + except NotYet: # @@ temporary fix + pass + + # Update objects that adapt to key reference + if key is not None: + for utility in utilities: + try: + uid = utility.getId(ob) + utility.refs[uid] = key + utility.ids[key] = uid + except KeyError: + pass diff --git a/z2/intid/keyreference.py b/z2/intid/keyreference.py new file mode 100644 index 0000000..d23989c --- /dev/null +++ b/z2/intid/keyreference.py @@ -0,0 +1,115 @@ +from Acquisition import aq_base, aq_chain +from ZODB.interfaces import IConnection +from ZPublisher.BaseRequest import RequestContainer +from zExceptions import NotFound +from persistent import IPersistent +from zope.component import adapter, adapts +from zope.app.component.hooks import getSite +from zope.interface import implements, implementer +from zope.app.keyreference.interfaces import IKeyReference, NotYet +from zope.app.keyreference.persistent import KeyReferenceToPersistent +from site import get_root, aq_iter +from zope.app.container.interfaces import IObjectAddedEvent + + +@adapter(IPersistent) +@implementer(IConnection) +def connectionOfPersistent(obj): + """ zope2 cxn fetcher for wrapped items """ + for parent in aq_iter(obj): + conn = getattr(parent, '_p_jar', None) + if conn is not None: + return conn + + +@adapter(IPersistent, IObjectAddedEvent) +def add_object_to_connection(ob, event): + """Pre-add new objects to their persistence connection""" + connection = IConnection(ob, None) + if None is not connection: + connection.add(aq_base(ob)) + + +class KeyReferenceToPersistent(KeyReferenceToPersistent): + """a zope2ish implementation of keyreferences that unwraps objects + that have Acquisition wrappers + + These references compare by _p_oids of the objects they reference. + + @@ cache IConnection as a property and volative attr? + """ + implements(IKeyReference) + adapts(IPersistent) + + key_type_id = 'five.intid.keyreference' + + def __init__(self, wrapped_obj): + # make sure our object is wrapped by containment only + try: + self.path = '/'.join(wrapped_obj.getPhysicalPath()) + except AttributeError: + self.path = None + + # If the path ends with /, it means the object had an empty id. + # This means it's not yet added to the container, and so we have + # to defer. + if self.path is not None and self.path.endswith('/'): + raise NotYet(wrapped_obj) + self.object = aq_base(wrapped_obj) + connection = IConnection(wrapped_obj, None) + + if not getattr(self.object, '_p_oid', None): + if connection is None: + raise NotYet(wrapped_obj) + connection.add(self.object) + + try: + self.root_oid = get_root(wrapped_obj)._p_oid + except AttributeError: + # If the object is unwrapped we can try to use the Site from the + # threadlocal as our acquisition context, hopefully it's not + # something odd. + self.root_oid = get_root(getSite())._p_oid + self.oid = self.object._p_oid + self.dbname = connection.db().database_name + + @property + def root(self): + return IConnection(self.object)[self.root_oid] + + @property + def wrapped_object(self): + if self.path is None: + return self.object + try: + obj = self.root.unrestrictedTraverse(self.path) + except (NotFound, AttributeError,): + return self.object + chain = aq_chain(obj) + # Try to ensure we have a request at the acquisition root + # by using the one from getSite + if not len(chain) or not isinstance(chain[-1], RequestContainer): + site = getSite() + site_chain = aq_chain(site) + if len(site_chain) and isinstance(site_chain[-1], + RequestContainer): + req = site_chain[-1] + new_obj = req + # rebuld the chain with the request at the bottom + for item in reversed(chain): + new_obj = aq_base(item).__of__(new_obj) + obj = new_obj + return obj + + def __call__(self): + return self.wrapped_object + + def __hash__(self): + return hash((self.dbname, + self.object._p_oid, + )) + + def __cmp__(self, other): + if self.key_type_id == other.key_type_id: + return cmp((self.dbname,self.oid), (other.dbname, other.oid)) + return cmp(self.key_type_id, other.key_type_id) diff --git a/z2/intid/lsm.py b/z2/intid/lsm.py new file mode 100644 index 0000000..957e497 --- /dev/null +++ b/z2/intid/lsm.py @@ -0,0 +1,13 @@ +""" +five.localsitemanager/PersistentComponents compatibility support. +""" + +try: + from five.localsitemanager import ( + make_objectmanager_site as make_site, ) +except ImportError: + USE_LSM = False + from Products.Five.site.localsite import ( + enableLocalSiteHook as make_site, ) +else: + USE_LSM = True diff --git a/z2/intid/overrides.zcml b/z2/intid/overrides.zcml new file mode 100644 index 0000000..9d3ccb5 --- /dev/null +++ b/z2/intid/overrides.zcml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/z2/intid/registrations.pt b/z2/intid/registrations.pt new file mode 100644 index 0000000..63d83ea --- /dev/null +++ b/z2/intid/registrations.pt @@ -0,0 +1,24 @@ + + +
+
+

objects

+ + + + + + +
IDObject
+
+
+
+ + + diff --git a/z2/intid/site.py b/z2/intid/site.py new file mode 100644 index 0000000..abf0e50 --- /dev/null +++ b/z2/intid/site.py @@ -0,0 +1,133 @@ +from Acquisition import aq_base, aq_chain +from Products.Five import BrowserView +from zope.app.intid.interfaces import IIntIds +from zope.app.component.hooks import setSite, setHooks +from zope.app.component.interfaces import ISite +from zope.component.interfaces import ComponentLookupError +from zope.component import getUtility, getSiteManager +from OFS.interfaces import IApplication +from intid import IntIds, OFSIntIds +from lsm import make_site, USE_LSM +from utils import aq_iter + +class FiveIntIdsInstall(BrowserView): + @property + def context(self): + return self._context[0] + + def __init__(self, context, request): + self._context = context, + self.request = request + doinstall = self.request.get('install', None) + if doinstall: + self.install() + + def install(self): + add_intids(self.context, findroot=False) + + @property + def installed(self): + installed = False + try: + intids = getUtility(IIntIds) + if intids is not None: + if USE_LSM: + sm = self.context.getSiteManager() + if 'intids' in sm.objectIds(): + installed = True + else: + installed = True + except ComponentLookupError, e: + pass + return installed + +def initializeSite(site, sethook=False, **kw): + make_site(site) + if sethook: + setHooks() + setSite(site) + +def xx_get_root(app): + for parent in aq_iter(app, error=AttributeError): + if IApplication.providedBy(parent): + return parent + raise AttributeError, 'No application found' + +def get_root(obj): + for p in aq_chain(obj): + if IApplication.providedBy(p): + return p + raise AttributeError, 'No application found' + +def addUtility(site, interface, klass, name='', ofs_name='', findroot=True): + """ + add local utility in zope2 + """ + app = site + if findroot: + app = get_root(site) + + # If we have the zope Application and the utility is not yet + # registered, then register it. + assert app is not None, TypeError("app is None") + + if not ISite.providedBy(app): + initializeSite(app, sethook=False) + + sm = app.getSiteManager() + obj = None + if USE_LSM: + # Try to get the utility from OFS directly in case it is + # stored, but not registered + ofs_name = ofs_name or name + obj = getattr(aq_base(sm), ofs_name, None) + if sm.queryUtility(interface, + name=name, + default=None) is None: + # Register the utility if it is not yet registered + if obj is None: + if name: + obj = klass(name) + else: + obj = klass() + if USE_LSM: + sm.registerUtility(provided=interface, component=obj, + name=name) + else: + #sm.registerUtility(interface, obj, name=name) ###hm + sm.registerUtility(provided=interface, component=obj, + name=name) + elif USE_LSM and obj is None: + # Get the utility if registered, but not yet stored in the LSM + obj = sm.queryUtility(interface, name=name) + + # Make sure we store the utility permanently in the OFS so we don't loose + # intids on uninstall + if USE_LSM: + if ofs_name and ofs_name not in sm.objectIds(): + sm._setObject(ofs_name, aq_base(obj), set_owner=False, + suppress_events=True) + +from intid import IIntIds, OFSIntIds +from zope.component import getUtility + +def add_intids(site, findroot=False): + if USE_LSM: + klass = IntIds + else: + klass = OFSIntIds + addUtility(site, IIntIds, klass, ofs_name='intids', findroot=findroot) + +def get_intids(context=None): + return getUtility(IIntIds, context=context) + +def del_intids(context=None, findroot=False): + if findroot: + context = get_root(context) + utility = get_intids(context) + if USE_LSM: + getSiteManager(context).unregisterUtility(component=utility, + provided=IIntIds) + else: + parent = utility.aq_parent + parent.manage_delObjects([utility.__name__]) diff --git a/z2/intid/subscriber.zcml b/z2/intid/subscriber.zcml new file mode 100644 index 0000000..4129098 --- /dev/null +++ b/z2/intid/subscriber.zcml @@ -0,0 +1,18 @@ + + + + + + diff --git a/z2/intid/test.zcml b/z2/intid/test.zcml new file mode 100644 index 0000000..977e61c --- /dev/null +++ b/z2/intid/test.zcml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/z2/intid/tracking.txt b/z2/intid/tracking.txt new file mode 100644 index 0000000..e2bb11f --- /dev/null +++ b/z2/intid/tracking.txt @@ -0,0 +1,168 @@ +Tracking Object Additions, Deletions, and Moves +=============================================== + +Unique ID utilities track object add moves. Let's look at an +example. First, we'll create a unique Id utility: + + >>> print http(r""" + ... POST /++etc++site/default/@@contents.html HTTP/1.1 + ... Authorization: Basic mgr:mgrpw + ... Referer: http://localhost:8081/++etc++site/default/@@contents.html + ... + ... type_name=BrowserAdd__zope.app.intid.IntIds&new_value=""") + HTTP/1.1 303 See Other + ... + Location: http://localhost/++etc++site/default/IntIds/@@registration.html + ... + + >>> print http(r""" + ... POST /++etc++site/default/IntIds/addRegistration.html HTTP/1.1 + ... Authorization: Basic mgr:mgrpw + ... Referer: http://localhost:8081/++etc++site/default/IntIds/ + ... Content-Type: multipart/form-data; boundary=----------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ + ... + ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ + ... Content-Disposition: form-data; name="field.name" + ... + ... IntIds + ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ + ... Content-Disposition: form-data; name="field.provided" + ... + ... zope.app.intid.interfaces.IIntIds + ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ + ... Content-Disposition: form-data; name="field.provided-empty-marker" + ... + ... 1 + ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ + ... Content-Disposition: form-data; name="field.status" + ... + ... Active + ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ + ... Content-Disposition: form-data; name="field.status-empty-marker" + ... + ... 1 + ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ + ... Content-Disposition: form-data; name="field.permission" + ... + ... + ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ + ... Content-Disposition: form-data; name="field.permission-empty-marker" + ... + ... 1 + ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ + ... Content-Disposition: form-data; name="UPDATE_SUBMIT" + ... + ... Add + ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ-- + ... """) + HTTP/1.1 303 See Other + ... + Location: @@SelectedManagementView.html + ... + +Now, we'll add a few folders: + + >>> print http(r""" + ... POST /@@contents.html HTTP/1.1 + ... Authorization: Basic mgr:mgrpw + ... Content-Length: 64 + ... Content-Type: application/x-www-form-urlencoded + ... + ... type_name=BrowserAdd__zope.app.folder.folder.Folder&new_value=f1""") + HTTP/1.1 303 See Other + ... + + >>> print http(r""" + ... POST /f1/@@contents.html HTTP/1.1 + ... Authorization: Basic mgr:mgrpw + ... Content-Length: 64 + ... Content-Type: application/x-www-form-urlencoded + ... + ... type_name=BrowserAdd__zope.app.folder.folder.Folder&new_value=f1""") + HTTP/1.1 303 See Other + ... + + >>> print http(r""" + ... POST /f1/@@contents.html HTTP/1.1 + ... Authorization: Basic mgr:mgrpw + ... Content-Length: 64 + ... Content-Type: application/x-www-form-urlencoded + ... + ... type_name=BrowserAdd__zope.app.folder.folder.Folder&new_value=f2""") + HTTP/1.1 303 See Other + ... + + >>> print http(r""" + ... POST /f1/f1/@@contents.html HTTP/1.1 + ... Authorization: Basic mgr:mgrpw + ... Content-Length: 64 + ... Content-Type: application/x-www-form-urlencoded + ... + ... type_name=BrowserAdd__zope.app.folder.folder.Folder&new_value=f1""") + HTTP/1.1 303 See Other + ... + +Now, if we look at the index page for the unique id utility, we'll see +the objects we added: + + >>> print http(r""" + ... GET /++etc++site/default/IntIds/@@index.html?testing=1 HTTP/1.1 + ... Authorization: Basic mgr:mgrpw + ... Referer: http://localhost:8081/++etc++site/default/@@contents.html + ... """) + HTTP/1.1 200 Ok + ...4 objects... + .../f1... + .../f1/f1... + .../f1/f2... + .../f1/f1/f1... + +If we move the top object: + + >>> print http(r""" + ... POST /@@contents.html HTTP/1.1 + ... Authorization: Basic mgr:mgrpw + ... Content-Length: 40 + ... Content-Type: application/x-www-form-urlencoded + ... Referer: http://localhost:8081/@@contents.html + ... + ... new_value%3Alist=f2&rename_ids%3Alist=f1""") + HTTP/1.1 303 See Other + ... + +We'll see that reflected in the utility: + + >>> print http(r""" + ... GET /++etc++site/default/IntIds/@@index.html?testing=1 HTTP/1.1 + ... Authorization: Basic mgr:mgrpw + ... Referer: http://localhost:8081/++etc++site/default/@@contents.html + ... """) + HTTP/1.1 200 Ok + ...4 objects... + .../f2... + .../f2/f1... + .../f2/f2... + .../f2/f1/f1... + +And if we delete the top object: + + >>> print http(r""" + ... POST /@@contents.html HTTP/1.1 + ... Authorization: Basic mgr:mgrpw + ... Content-Length: 44 + ... Content-Type: application/x-www-form-urlencoded + ... Referer: http://localhost:8081/@@contents.html + ... + ... ids%3Alist=f2&container_delete_button=Delete""") + HTTP/1.1 303 See Other + ... + +all of the objects will go away: + + >>> print http(r""" + ... GET /++etc++site/default/IntIds/@@index.html?testing=1 HTTP/1.1 + ... Authorization: Basic mgr:mgrpw + ... Referer: http://localhost:8081/++etc++site/default/@@contents.html + ... """) + HTTP/1.1 200 Ok + ...0 objects... diff --git a/z2/intid/unreferenceable.py b/z2/intid/unreferenceable.py new file mode 100644 index 0000000..e6aa260 --- /dev/null +++ b/z2/intid/unreferenceable.py @@ -0,0 +1,31 @@ +# Sometimes persistent classes are never meant to be persisted. The most +# common example are CMFCore directory views and filesystem objects. +# Register specific handlers that are no-ops to circumvent +from zope.interface import implements +from zope.app.keyreference.interfaces import IKeyReference, NotYet + +def addIntIdSubscriber(ob, event): + return + +def removeIntIdSubscriber(ob, event): + return + +def moveIntIdSubscriber(ob, event): + return + +class KeyReferenceNever(object): + """A keyreference that is never ready""" + implements(IKeyReference) + + key_type_id = 'five.intid.cmfexceptions.keyreference' + + def __init__(self, obj): + raise NotYet() + + def __call__(self): + return None + + def __cmp__(self, other): + if self.key_type_id == other.key_type_id: + return cmp(self, other) + return cmp(self.key_type_id, other.key_type_id) diff --git a/z2/intid/utils.py b/z2/intid/utils.py new file mode 100644 index 0000000..975c5cc --- /dev/null +++ b/z2/intid/utils.py @@ -0,0 +1,23 @@ +from Acquisition import aq_base, aq_inner, IAcquirer + +def aq_iter(obj, error=None): + if not (IAcquirer.providedBy(obj) or hasattr(obj, '__parent__')): + raise TypeError("%s not acquisition wrapped" %obj) + + # adapted from alecm's 'listen' + seen = set() + # get the inner-most wrapper (maybe save some cycles, and prevent + # bogus loop detection) + cur = aq_inner(obj) + while cur is not None: + yield cur + seen.add(id(aq_base(cur))) + cur = getattr(cur, 'aq_parent', getattr(cur, '__parent__', None)) + if id(aq_base(cur)) in seen: + # avoid loops resulting from acquisition-less views + # whose __parent__ points to + # the context whose aq_parent points to the view + if error is not None: + raise error, '__parent__ loop found' + break + diff --git a/z2/intid/xx_ftests.py b/z2/intid/xx_ftests.py new file mode 100644 index 0000000..6ed73a1 --- /dev/null +++ b/z2/intid/xx_ftests.py @@ -0,0 +1,28 @@ +############################################################################## +# +# Copyright (c) 2004 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Int Id Utility Functional Tests + +$Id$ +""" +import unittest +from zope.app.testing import functional + +def test_suite(): + return unittest.TestSuite(( + functional.FunctionalDocFileSuite('tracking.txt'), + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') + diff --git a/z2/intid/xx_tests.py b/z2/intid/xx_tests.py new file mode 100644 index 0000000..d8307df --- /dev/null +++ b/z2/intid/xx_tests.py @@ -0,0 +1,60 @@ +import doctest +from zope.app.testing import placelesssetup +from persistent import Persistent +from zope.app.component.hooks import setHooks, setSite + +from Products.Five.tests.testing.simplecontent import ( + SimpleContent, + ISimpleContent, + manage_addSimpleContent, + ) +from Products.Five import zcml +from five.intid.lsm import USE_LSM +from five.intid import site + + +optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS + +class DemoPersistent(Persistent): + """ Demo persistent object """ + test = 'test object' + __name__ = 'Test Object' + +NOTIFIED=[None] + +def setNotified(event): + NOTIFIED[0] = "%s %s" %(event.object, event) + + +def setUp(app): + # enable zcml and site hooks + placelesssetup.setUp() + import Products.Five + from five import intid + zcml.load_config('meta.zcml', Products.Five) + zcml.load_config('configure.zcml', Products.Five) + zcml.load_config('test.zcml', intid) + if not USE_LSM: + # monkey in our hooks + from Products.Five.site.metaconfigure import classSiteHook + from Products.Five.site.localsite import FiveSite + from zope.interface import classImplements + from zope.app.component.interfaces import IPossibleSite + klass = app.__class__ + classSiteHook(klass, FiveSite) + classImplements(klass, IPossibleSite) + setHooks() + +def tearDown(): + placelesssetup.tearDown() + +def test_suite(): + import unittest + from Testing.ZopeTestCase import FunctionalDocFileSuite + from zope.testing.doctest import DocTestSuite + integration = FunctionalDocFileSuite( + 'README.txt', + package='five.intid', + optionflags=optionflags + ) + return unittest.TestSuite((integration,))