Package: inquant.contentmirror
Author:seletzDate:2008-01-08Revision:56670AbstractThis package is provides a way to have a instance of aPlonecontent
mirrored transparently into one or more locations. To do so, we basically
need to:locate content to be mirrored on traversalinsert the located oject into the traversed context’s acquisition chainTo 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.Gotchas, LimitationsBecause 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’sin 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. Thecontent 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 thetwice, something like thefolder_listingwill 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 UIDcatalog will contain the UID multiple times. Furthermore, the catalog
brains of the additional entries in the uid catalog will returnNonefor agetObject(). 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?)Basic Setupsome needed imports for this doctest:>>> from zope import interface
>>> from zope import component
>>> from zope.app.testing import ztapi
>>> from zope.publisher.browser import TestRequestLocating contentTo locate content, we provide an interface:>>> from inquant.contentmirror.interfaces import IMirrorContentLocatorWe 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.srcnow we can fetch the content by name:>>> locator.locate("doc")
<ATDocument at /plone/Members/test_user_1_/src/doc>Ok, that worked.Inserting (mirroring) contentBasically 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, thatobjhas a normal acquisition chain, as one would expect.
Next, we’ll fake the acquisition chain such that obj_mirrored willappearto be below thetargetfolder:>>> 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>, ...Yay.TraversingNow 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 theIPublishTraverseinterface, which is the Zope 3 way of doing it:>>> from zope.publisher.interfaces import IPublishTraverseZope 2 used to use__bobo_traverse__to traverse objects. Nowadays,
traversal is done by providing a adapter toIPublishTraverse. The
default traverser isDefaultPublishTraverse, which is defined in the
Zope 2 publisher:>>> from ZPublisher.BaseRequest import DefaultPublishTraverseThis 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 toIMirrorContentLocator, and
locate a content for the currently traversed namestrip the located content object of its acquisition chain and insert it
into the travesed context’s acquisition chain, and return itOk, let’s try it.First, we need to create aIPublishTraverseadapter. Note that this is
a multi adapter adapting a interface and aIHTTPRequesttoIPublishTraverse:>>> 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
toIMirrorContentProvider. 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)
TrueQuery 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 theIMirrorContentProviderfirst:>>> interface.alsoProvides(self.folder.target, IMirrorContentProvider)
>>> IMirrorContentProvider.providedBy(self.folder.target)
TrueTry again:>>> traverser = component.queryMultiAdapter(
...     (self.folder.target, request), IPublishTraverse )A-haaa!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.srcNow 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 thetargetfolder, but it is located in thesrcfolder in reality.CleanupRemove the adapter:>>> gsm = component.getGlobalSiteManager()
>>> gsm.unregisterAdapter(
...     MirrorTraverse,
...     (IMirrorContentProvider,IHTTPRequest),
...     IPublishTraverse)
True
>>> gsm.unregisterAdapter(TestLocator, (ATFolder,),
...    IMirrorContentLocator)
TrueLinksPloneis a CMS.vim: set ft=rst tw=75 nocin nosi ai sw=4 ts=4 expandtab: