I\'m wanting to redirect to a container\'s parent after deleting an item within it. To this end, I\'ve tried subscribing to zope.lifecycleevent\'s IObjectRemovedEvent<
A co-worker came up with a working solution:
import transaction
def redirect_to_trial(trans, obj=None, parent=None):
if obj.id not in parent:
request = getattr(obj, 'REQUEST', None)
if request:
trial_url = obj.__parent__.__parent__.absolute_url()
request.response.redirect(trial_url)
@grok.subscribe(ISite, IObjectRemovedEvent)
def on_site_delete(obj, event):
kwargs = dict(
obj = obj,
parent = event.oldParent,
)
transaction.get().addAfterCommitHook(redirect_to_trial, kws=kwargs)
This checks after the commit to ensure the object actually has been removed, before performing the redirection.
Some confirmation of whether this is a suitable approach would be appreciated, though.
I'm facing what I think must be a common use case as well, where a local Plone object is proxying for a remote object. Upon removal of the Plone object, but only on actual removal, I want to remove the remote object.
For me, addAfterCommitHook() didn't avoid any of the issues, so I took a custom IDataManager approach, which provides a nice generic solution to simlar use cases...
from transaction.interfaces import IDataManager
from uuid import uuid4
class FinishOnlyDataManager(object):
implements(IDataManager)
def __init__(self, callback, args=None, kwargs=None):
self.cb = callback
self.args = [] if args is None else args
self.kwargs = {} if kwargs is None else kwargs
self.transaction_manager = transaction.manager
self.key = str(uuid4())
def sortKey(self): return self.key
abort = commit = tpc_begin = tpc_vote = tpc_abort = lambda x,y: None
def tpc_finish(self, tx):
# transaction.interfaces implies that exceptions are
# a bad thing. assuming non-dire repercussions, and that
# we're not dealing with remote (non-zodb) objects,
# swallow exceptions.
try:
self.cb(*self.args, **self.kwargs)
except Exception, e:
pass
And the associated handler...
@grok.subscribe(IRemoteManaged, IObjectRemovedEvent)
def remove_plan(item, event): IRemoteManager(item).handle_remove()
class RemoteManager(object): ...
def handle_remove(self):
obj = self._retrieve_remote_object()
def _do_remove():
if obj:
obj.delete()
transaction.get().join(FinishOnlyDataManager(_do_remove))
Instead of using a event handler, you could customize the delete_confirmation
actions; these can be altered through the web even, and can be customized per type. The delete_confirmation
script is a CMF Form Controller script and there are several options to alter it's behaviour.
Currently, the actions are defined as such:
[actions]
action.success=redirect_to:python:object.aq_inner.aq_parent.absolute_url()
action.confirm=traverse_to:string:delete_confirmation_page
You could add a type specific action by defining action.success.TypeName
, for example.
To do so through-the-web, visit the ZMI and find the portal_form_controller
tool, then click the Actions
tab:
As you can see in this screenshot there is also documentation on the tool available here.
On the actions tab there is a form to add new actions:
As you can see, the context type is a drop-down with all existing type registrations to make it easier to specify a type-specific action. I've copied in the regular action (a redirect_to
action specified by a python:
expression and added an extra .aq_parent
to select the container parent.
You could also add such an action with the .addFormAction
method on the tool:
fctool = getToolByName(context, 'portal_form_controller')
fctool.addFormAction('delete_confirmation', 'success', 'Event', None,
'redirect_to',
'python:object.aq_inner.aq_parent.aq_parent.absolute_url()')
Last, but not least, you can specify such custom actions in the cmfformcontroller.xml
file in a GenericSetup profile; here is an example based on the above action:
<?xml version="1.0" ?>
<cmfformcontroller>
<action
object_id="delete_confirmation"
status="success"
context_type="Event"
action_type="redirect_to"
action_arg="python:object.aq_inner.aq_parent.aq_parent.absolute_url()"
/>
</cmfformcontroller>
This format is one of those under-documented things in Plone; I got this from the CMFFormController sourcecode for the GS import and export code.
Here's another possibility, again from the same genius co-worker:
from zope.interface import implements
from transaction.interfaces import ISavepointDataManager
from transaction._transaction import AbortSavepoint
import transaction
class RedirectDataManager(object):
implements(ISavepointDataManager)
def __init__(self, request, url):
self.request = request
self.url = url
# Use the default thread transaction manager.
self.transaction_manager = transaction.manager
def tpc_begin(self, transaction):
pass
def tpc_finish(self, transaction):
self.request.response.redirect(self.url)
def tpc_abort(self, transaction):
self.request.response.redirect(self.url)
def commit(self, transaction):
pass
def abort(self, transaction):
pass
def tpc_vote(self, transaction):
pass
def sortKey(self):
return id(self)
def savepoint(self):
"""
This is just here to make it possible to enter a savepoint with this manager active.
"""
return AbortSavepoint(self, transaction.get())
def redirect_to_trial(obj, event):
request = getattr(obj, 'REQUEST', None)
if request:
trial_url = obj.__parent__.__parent__.absolute_url()
transaction.get().join(RedirectDataManager(request, trial_url))
I'm now using zcml for subscription to more easily bind it to multiple content types:
<subscriber
zcml:condition="installed zope.lifecycleevent"
for=".schema.ISite zope.lifecycleevent.IObjectRemovedEvent"
handler=".base.redirect_to_trial"
/>
This is the solution I've ended up going with, as I find it more explicit about what's happening than doing manual checks to work out if the event I've caught is the event I really want.