From 2b4453545670080194b7a5c61e0d6917991a546e Mon Sep 17 00:00:00 2001 From: Wichert Akkerman Date: Wed, 24 Sep 2008 15:06:10 +0000 Subject: [PATCH] New package --- README.txt | 43 +++++++ docs/HISTORY.txt | 10 ++ docs/LICENSE.ZPL | 43 +++++++ plone/__init__.py | 6 + plone/postpublicationhook/__init__.py | 5 + plone/postpublicationhook/configure.zcml | 7 ++ plone/postpublicationhook/event.py | 13 ++ plone/postpublicationhook/hook.py | 147 +++++++++++++++++++++++ plone/postpublicationhook/interfaces.py | 8 ++ setup.cfg | 3 + setup.py | 35 ++++++ 11 files changed, 320 insertions(+) create mode 100644 README.txt create mode 100644 docs/HISTORY.txt create mode 100644 docs/LICENSE.ZPL create mode 100644 plone/__init__.py create mode 100644 plone/postpublicationhook/__init__.py create mode 100644 plone/postpublicationhook/configure.zcml create mode 100644 plone/postpublicationhook/event.py create mode 100644 plone/postpublicationhook/hook.py create mode 100644 plone/postpublicationhook/interfaces.py create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..7212c22 --- /dev/null +++ b/README.txt @@ -0,0 +1,43 @@ +Introduction +============ + +This package provides a hook into Zope's ZPublisher that is run after the +publisher has completed publication, but before the the transaction is commited +and the response is returned to the requesting browser. This is practical for +caching purposes: it is the ideal place to determine and insert caching headers +into the response. + +Hooks use `zope.eventz_'s event mechanism using the +plone.validatehook.interfaces.IPostValidationEvent. This is based on the +standard ObjectEvent from `zope.component`_. + +Example +======= + +As an example we will write a bit of code which logs the path of every published +object. This is the code for the event handler:: + + from zope.interface import Interface + from zope.component import adapter + from plone.postpublicationhook.interfaces import IAfterPublicationEvent + import logging + + logger = logging.getLogger("LogRequest") + + @adapter(Interface, IAfterPublicationEvent) + def LogRequest(object, event): + if getattr(object, "getPhysicalPath", None) is None: + path="Unknown path" + else: + path="/".join(object.getPhysicalPath() + + logger.info("Request for object %s" % path) + + +To use this code you need to register it in zcml:: + + + + +.. _zope.event: http://pypi.python.org/pypi/zope.event +.. _zope.component: http://pypi.python.org/pypi/zope.component diff --git a/docs/HISTORY.txt b/docs/HISTORY.txt new file mode 100644 index 0000000..ce2bdb7 --- /dev/null +++ b/docs/HISTORY.txt @@ -0,0 +1,10 @@ +Changelog +========= + +1.0rc1 - September 24, 2008 +--------------------------- + +* Initial release + [wichert] + + diff --git a/docs/LICENSE.ZPL b/docs/LICENSE.ZPL new file mode 100644 index 0000000..59a60e4 --- /dev/null +++ b/docs/LICENSE.ZPL @@ -0,0 +1,43 @@ +Some code carries the Zope Public License (as indicated in the comments): + +Zope Public License (ZPL) Version 2.0 + +This software is Copyright (c) Zope Corporation (tm) and Contributors. +All rights reserved. + +This license has been certified as open source. It has also been designated as +GPL compatible by the Free Software Foundation (FSF). + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions in source code must retain the above copyright notice, this + list of conditions, and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions, and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* The name Zope Corporation (tm) must not be used to endorse or promote + products derived from this software without prior written permission from + Zope Corporation. +* The right to distribute this software or to use it for any purpose does not + give you the right to use Servicemarks (sm) or Trademarks (tm) of Zope + Corporation. Use of them is covered in a separate agreement + (see http://www.zope.com/Marks). +If any files are modified, you must cause the modified files to carry prominent +notices stating that you changed the files and the date of any change. + +Disclaimer +THIS SOFTWARE IS PROVIDED BY ZOPE CORPORATION ``AS IS'' AND ANY EXPRESSED OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL ZOPE CORPORATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This software consists of contributions made by Zope Corporation and many +individuals on behalf of Zope Corporation. Specific attributions are listed +in the accompanying credits file. diff --git a/plone/__init__.py b/plone/__init__.py new file mode 100644 index 0000000..f48ad10 --- /dev/null +++ b/plone/__init__.py @@ -0,0 +1,6 @@ +# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages +try: + __import__('pkg_resources').declare_namespace(__name__) +except ImportError: + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/plone/postpublicationhook/__init__.py b/plone/postpublicationhook/__init__.py new file mode 100644 index 0000000..ab651d5 --- /dev/null +++ b/plone/postpublicationhook/__init__.py @@ -0,0 +1,5 @@ + +def initialize(context): + from plone.postpublicationhook.hook import InstallHook + + InstallHook() diff --git a/plone/postpublicationhook/configure.zcml b/plone/postpublicationhook/configure.zcml new file mode 100644 index 0000000..7cfac75 --- /dev/null +++ b/plone/postpublicationhook/configure.zcml @@ -0,0 +1,7 @@ + + + + + diff --git a/plone/postpublicationhook/event.py b/plone/postpublicationhook/event.py new file mode 100644 index 0000000..fd7862d --- /dev/null +++ b/plone/postpublicationhook/event.py @@ -0,0 +1,13 @@ +from zope.interface import implements +from zope.component.interfaces import ObjectEvent +from plone.postpublicationhook.interfaces import IAfterPublicationEvent + + +class AfterPublicationEvent(ObjectEvent): + implements(IAfterPublicationEvent) + + def __init__(self, context, request): + super(AfterPublicationEvent, self).__init__(context) + self.request=request + + diff --git a/plone/postpublicationhook/hook.py b/plone/postpublicationhook/hook.py new file mode 100644 index 0000000..203259d --- /dev/null +++ b/plone/postpublicationhook/hook.py @@ -0,0 +1,147 @@ +import logging +import sys +from ZPublisher.Publish import call_object +from ZPublisher.Publish import missing_name +from ZPublisher.Publish import dont_publish_class +from ZPublisher.Publish import get_module_info +from ZPublisher.Publish import Retry +from ZPublisher.mapply import mapply +from zope.publisher.browser import setDefaultSkin +from zope.security.management import newInteraction +from zope.security.management import endInteraction +from zExceptions import Redirect + +import zope.event +from plone.postpublicationhook import AfterPublicationEvent + + +def publish(request, module_name, after_list, debug=0, + # Optimize: + call_object=call_object, + missing_name=missing_name, + dont_publish_class=dont_publish_class, + mapply=mapply, + ): + + (bobo_before, bobo_after, object, realm, debug_mode, err_hook, + validated_hook, transactions_manager)= get_module_info(module_name) + + parents=None + response=None + + try: + # TODO pass request here once BaseRequest implements IParticipation + newInteraction() + + request.processInputs() + + request_get=request.get + response=request.response + + # First check for "cancel" redirect: + if request_get('SUBMIT','').strip().lower()=='cancel': + cancel=request_get('CANCEL_ACTION','') + if cancel: + raise Redirect, cancel + + after_list[0]=bobo_after + if debug_mode: + response.debug_mode=debug_mode + if realm and not request.get('REMOTE_USER',None): + response.realm=realm + + if bobo_before is not None: + bobo_before() + + # Get the path list. + # According to RFC1738 a trailing space in the path is valid. + path=request_get('PATH_INFO') + + request['PARENTS']=parents=[object] + + if transactions_manager: + transactions_manager.begin() + + object=request.traverse(path, validated_hook=validated_hook) + + if transactions_manager: + transactions_manager.recordMetaData(object, request) + + result=mapply(object, request.args, request, + call_object,1, + missing_name, + dont_publish_class, + request, bind=1) + + if result is not response: + response.setBody(result) + + # This is the only change from the canonical publish method + zope.event.notify(AfterPublicationEvent(request, object)) + + if transactions_manager: + transactions_manager.commit() + endInteraction() + + return response + except: + # DM: provide nicer error message for FTP + sm = None + if response is not None: + sm = getattr(response, "setMessage", None) + + if sm is not None: + from asyncore import compact_traceback + cl,val= sys.exc_info()[:2] + sm('%s: %s %s' % ( + getattr(cl,'__name__',cl), val, + debug_mode and compact_traceback()[-1] or '')) + + if err_hook is not None: + if parents: + parents=parents[0] + try: + try: + return err_hook(parents, request, + sys.exc_info()[0], + sys.exc_info()[1], + sys.exc_info()[2], + ) + except Retry: + if not request.supports_retry(): + return err_hook(parents, request, + sys.exc_info()[0], + sys.exc_info()[1], + sys.exc_info()[2], + ) + finally: + if transactions_manager: + transactions_manager.abort() + endInteraction() + + # Only reachable if Retry is raised and request supports retry. + newrequest=request.retry() + request.close() # Free resources held by the request. + # Set the default layer/skin on the newly generated request + setDefaultSkin(newrequest) + try: + return publish(newrequest, module_name, after_list, debug) + finally: + newrequest.close() + + else: + if transactions_manager: + transactions_manager.abort() + endInteraction() + raise + + + +def InstallHook(): + """Install our own publish method.""" + from plone.postpublicationhook import hook + import ZPublisher.Publish + + ZPublisher.Publish.publish=hook.publish + logging.info("Monkeypatch ZPublisher publish method to send AfterPublicationEvent") + diff --git a/plone/postpublicationhook/interfaces.py b/plone/postpublicationhook/interfaces.py new file mode 100644 index 0000000..6a35004 --- /dev/null +++ b/plone/postpublicationhook/interfaces.py @@ -0,0 +1,8 @@ +from zope.component.interfaces import IObjectEvent +from zope.interface import Attribute + +class IAfterPublicationEvent(IObjectEvent): + """An event which is fired after publication, but before the transaction is + commited.""" + + request = Attribute("The request") diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..01bb954 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[egg_info] +tag_build = dev +tag_svn_revision = true diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..aa2416c --- /dev/null +++ b/setup.py @@ -0,0 +1,35 @@ +from setuptools import setup, find_packages +import os.path + +version = '1.0' + +setup(name='plone.postpublicationhook', + version=version, + description="Zope 2 post-publication hook", + long_description=open("README.txt").read() + "\n" + + open(os.path.join("docs", "HISTORY.txt")).read(), + classifiers=[ + "Programming Language :: Python", + "Environment :: Web Environment", + "Framework :: Zope2", + "Intended Audience :: Developers", + "License :: OSI Approved :: Zope Public License", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + keywords='', + author='Wichert Akkerman', + author_email='wichert@wiggy.net', + url='', + license='ZPL', + packages=find_packages(exclude=['ez_setup']), + namespace_packages=['plone'], + include_package_data=True, + zip_safe=False, + install_requires=[ + 'setuptools', + 'zope.event', + 'zope.interface', + 'zope.security', + ], + ) +