Plone: reacting to object removal

做~自己de王妃 提交于 2019-11-30 17:23:33

问题


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:

@grok.subscribe(ISite, IObjectRemovedEvent)
def redirect_to_trial_on_delete(obj, event):
    request = getattr(obj, 'REQUEST', None)
    if request:
        trial_url = obj.aq_parent.aq_parent.absolute_url()
        request.response.redirect(trial_url)

Deletion is triggered by clicking on container/id/delete_confirmation however this fires more events than I was expecting. My subscribed function is called twice: once when I click on the link, then again when I confirm the deletion. Even more confusing, it's also called if I cancel the deletion. I was expecting the event to only be raised if an object was, y'know, removed from a container.

In all three cases the event object is the same, with the same property values for oldName, oldParent etc.

How can I distinguish between asking to delete an item, cancelling that request, and actually deleting an item?

Update: so it seems the initial event is called because the object is removed from the container in order to check link integrity, at which point there's a rollback.


回答1:


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.




回答2:


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.




回答3:


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.




回答4:


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))


来源:https://stackoverflow.com/questions/11218272/plone-reacting-to-object-removal

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!