mirror plone content transparently in a plone site
This package is provides a way to have a instance of a Plone content mirrored transparently into one or more locations. To do so, we basically need to:
- locate content to be mirrored on traversal
- insert the located oject into the traversed context’s acquisition chain
To do so, we’ll implement a adapter for the Zope 3 traversal mechanism, do a lookup for content to be mirrored in this adapter, and insert the object in the adapted contexts acquisition chain.
Because we do add the mirrored object like this, plone will eventually reindex the object multiple times (for example if one edits the object when appearing on a mirrored path). This has the following consequences:
- searches – on a search, the object is listed twice, because it’s
- in the catalog twice. IMHO that’s OK, because that’s what we actually want, we want the object to appear in two places.
- edit – KSS edit and normal edit of mirrored content works as expected. The
- content will be reindexed on edit. Users which view the same object visible on another path will see the changes on page reload. Nothing unusual here.
- removal, rename – there it gets tricky. Because the object is in the
- twice, something like the folder_listing will show the object on mirrored paths despite it’s no longer there. All we do is mirroring, not copying. I’ll try to handle that with an event subscriber.
- UID catalog – because the object may be catalogued multiple times, the UID
- catalog will contain the UID multiple times. Furthermore, the catalog brains of the additional entries in the uid catalog will return None for a getObject(). The exact implications of this are unclear to me, that might well be an error in the UID catalog (why does it insert a UID twice in the first place?)
some needed imports for this doctest:
>>> from zope import interface >>> from zope import component >>> from zope.app.testing import ztapi >>> from zope.publisher.browser import TestRequest
To locate content, we provide an interface:
>>> from inquant.contentmirror.interfaces import IMirrorContentLocator
We now can define an adapter which is able to locate content from somewhere else:
>>> class TestLocator(object): ... def __init__(self, context): ... self.context = context ... def locate( self, name): ... return self.source.get(name)
So basically this adapter just has to return a object for a given name. Let’s try that. We need to setup some plone content for this:
>>> _ = self.folder.invokeFactory("Folder", "src") >>> _ = self.folder.src.invokeFactory("Document", "doc", title="Muha") >>> _ = self.folder.invokeFactory("Folder", "target")
Now we can provide the adapter:
>>> from Products.ATContentTypes.content.folder import ATFolder >>> ztapi.provideAdapter(ATFolder, ... IMirrorContentLocator, TestLocator)
And look up the adapter:
>>> locator = IMirrorContentLocator(self.folder.target) >>> locator.source = self.folder.src
now we can fetch the content by name:
>>> locator.locate("doc") <ATDocument at /plone/Members/test_user_1_/src/doc>
Ok, that worked.
Inserting (mirroring) content
Basically what we do is to strip the content to be mirrored from its acquisition context and insert it into the target context’s acquisition chain. Lets try that:
>>> from Acquisition import aq_inner, aq_base, aq_chain >>> obj = self.folder.src.doc >>> aq_chain(obj) [<ATDocument at /plone/Members/test_user_1_/src/doc>, <ATFolder at /plone/Members/test_user_1_/src>, ...
We see, that obj has a normal acquisition chain, as one would expect. Next, we’ll fake the acquisition chain such that obj_mirrored will appear to be below the target folder:
>>> obj_mirrored = aq_base(obj).__of__(self.folder.target) >>> aq_chain(obj_mirrored) [<ATDocument at /plone/Members/test_user_1_/target/doc>, <ATFolder at /plone/Members/test_user_1_/target>, ...
Now all wa have to do is to provide a way to hook into Plone’s object traversal mechanism, and to alter it such that we can return the mirrored object. The traverser we’ll provide uses the IPublishTraverse interface, which is the Zope 3 way of doing it:
>>> from zope.publisher.interfaces import IPublishTraverse
Zope 2 used to use __bobo_traverse__ to traverse objects. Nowadays, traversal is done by providing a adapter to IPublishTraverse. The default traverser is DefaultPublishTraverse, which is defined in the Zope 2 publisher:
>>> from ZPublisher.BaseRequest import DefaultPublishTraverse
This adapter does eventually call __bobo_traverse__. Thus, there’s no need to overwrite __bobo_traverse__ anymore. Yay.
Our special adapter for our mirror content woll do the following:
- try to adapt the traversed context to IMirrorContentLocator, and locate a content for the currently traversed name
- strip the located content object of its acquisition chain and insert it into the travesed context’s acquisition chain, and return it
Ok, let’s try it.
First, we need to create a IPublishTraverse adapter. Note that this is a multi adapter adapting a interface and a IHTTPRequest to IPublishTraverse:
>>> class MirrorTraverse(object): ... def __init__(self,context,request): ... self.context = context ... self.request = request ... self.locator = IMirrorContentLocator(context) ... def publishTraverse(self, request, name): ... obj = locator.locate(name) ... return aq_base(aq_inner(obj)).__of__(self.context)
Now, we want to provide the adapter. We do NOT want to overwrite the default behavior, though. That’s why we define a marker interface to adapt to IMirrorContentProvider. We provide the adapter:
>>> from inquant.contentmirror.interfaces import IMirrorContentProvider >>> from zope.publisher.interfaces.http import IHTTPRequest >>> ztapi.provideAdapter( ... (IMirrorContentProvider,IHTTPRequest), ... IPublishTraverse, ... MirrorTraverse)
Now we should be able to traverse. To call up the adapter we need a test request, though:
>>> request = TestRequest() >>> IHTTPRequest.providedBy(request) True
Query the ZCA for the adapter:
>>> traverser = component.getMultiAdapter( ... (self.folder.target, request), IPublishTraverse ) Traceback (most recent call last): ... ComponentLookupError: ...
Ouch! Ah, we need to provide the IMirrorContentProvider first:
>>> interface.alsoProvides(self.folder.target, IMirrorContentProvider) >>> IMirrorContentProvider.providedBy(self.folder.target) True
>>> traverser = component.queryMultiAdapter( ... (self.folder.target, request), IPublishTraverse )
Unfortunately, for sake of this test, we need to patch in the source manually. In reality, the locator adapter would of course determine the source itself.:
>>> traverser.locator.source = self.folder.src
Now try to traverse:
>>> traverser.publishTraverse(request, "doc") <ATDocument at /plone/Members/test_user_1_/target/doc>
Yay! Note that the returned object seems to come from the target folder, but it is located in the src folder in reality.
Remove the adapter:
>>> gsm = component.getGlobalSiteManager() >>> gsm.unregisterAdapter( ... MirrorTraverse, ... (IMirrorContentProvider,IHTTPRequest), ... IPublishTraverse) True >>> gsm.unregisterAdapter(TestLocator, (ATFolder,), ... IMirrorContentLocator) True