Source code for nti.site.subscribers

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Subscribers for traversal events to ensure that the the proper
site is installed, including respecting global registered components.

.. caution:: :func:`zope.site.site.threadSiteSubscriber` also exists and is
   configured by some packages like :mod:`zope.app.publication`. Care must be used to
   ensure that it is *not* configured in place of our own :func:`threadSiteSubscriber`.
"""

# turn off warning for not calling superclass, calling indirect superclass and
# accessing protected methods. we're deliberately doing both
# pylint: disable=W0233,W0231,W0212

from __future__ import print_function, unicode_literals, absolute_import, division
__docformat__ = "restructuredtext en"

logger = __import__('logging').getLogger(__name__)

from zope import component

from zope.component.hooks import getSite
from zope.component.hooks import setSite

from zope.interface.interfaces import IComponents

from zope.component.interfaces import ISite

from zope.lifecycleevent.interfaces import IObjectRemovedEvent

from zope.location.interfaces import LocationError

from zope.proxy import ProxyBase
from zope.proxy import non_overridable

from zope.site.interfaces import IRootFolder
from zope.site.interfaces import INewLocalSite

from zope.traversing.interfaces import IBeforeTraverseEvent

from nti.site.interfaces import IHostPolicyFolder
from nti.site.interfaces import IMainApplicationFolder

from nti.site.transient import BasedSiteManager

from nti.site.utils import unregisterUtility


class _ProxyTraversedSite(ProxyBase):
    """
    We need to be able to control the site manager used
    by sites we traverse to in order to ensure that host
    configuration is at the right place in the resolution order.
    But a site can be literally any type of object. So we fake out the
    siteManager methods but proxy everything else.
    """

    def __new__(cls, base, site_manager): # pylint:disable=unused-argument
        return ProxyBase.__new__(cls, base)

    def __init__(self, base, site_manager):
        ProxyBase.__init__(self, base)
        self.__site_manager = site_manager

    @non_overridable
    def getSiteManager(self):
        return self.__site_manager

    @non_overridable
    def setSiteManager(self, new_man): # pylint:disable=unused-argument
        raise ValueError("Cannot set site manager on proxy")


[docs]@component.adapter(ISite, IBeforeTraverseEvent) def threadSiteSubscriber(new_site, _event): """ Set the current ``zope.component.hooks`` site to the ``new_site`` object found during traversal, being careful to maintain any previously installed host (site-name) configurations as lower priority than the new site. Sites encountered during traversal are expected to have the main application site (e.g., ``nti.dataserver``) in their base chain so we have access to its configuration and persistent utilities. This implies that sites encountered during traversal are either synthetic (generated by a traversal adapter to use some particular ``IComponents``) or themselves persistent. Because of this, when we encounter the root or dataserver folders as sites, we take no action (unless there is no site presently installed; in that case we install them). We expect that something else takes care of clearing the site. .. important:: Clearing the site is important, especially if the site was persistent. You can use a subscriber to "end request" style events or otherwise make it part of request lifecycle. When that's not possible, please do so manually. .. versionchanged:: 2.3.0 Always install the *new_site* if there is no current site. Previously, if *new_site* provided `~.IMainApplicationFolder` or `zope.site.interfaces.IRootFolder`, it was always ignored. No longer raises a ``LocationError`` if an unknown type of site is encountered. Instead, simply installs it, replacing the current site. """ current_site = getSite() if current_site is None: # Nothing to do setSite(new_site) return if current_site is new_site: # This is typically the case when we traverse directly # into utilities registered with the site, for example # /dataserver2/++etc++hostsites/janux.ou.edu/++etc++site/SOMEUTILITY/... # with the current host being janux.ou.edu. return if IMainApplicationFolder.providedBy(new_site) or IRootFolder.providedBy(new_site): # TODO: Since we get these events, we could actually replace # nti.appserver.tweens.zope_site_tween with this. That's # probably the longterm answer. # # The complication with that is that the tween uses information in the Pyramid request # to find ``nti.site.site.get_site_for_site_names`` and install that site # before traversal. The "right" solution to this is probably something to do # with "virtual hosts" and setting the path up to traverse through that site # first, and then into the real traversal? return if IHostPolicyFolder.providedBy(current_site) and \ IHostPolicyFolder.providedBy(new_site): # This is typically the case when we traverse directly # into utilities registered with the site, for example # /dataserver2/++etc++hostsites/janux.ou.edu/++etc++site/SOMEUTILITY/... # with the current host NOT being janux.ou.edu. # We do not want to switch host configurations here, but we do # want to allow traversal, so we take no action. # TODO: We might want to only allow this if there is some # inheritance relationship between the two sites? return if hasattr(current_site.getSiteManager(), 'host_components'): # A site synthesized by get_site_for_site_names # OR one previously synthesized by this function. In either case, # we always want to proxy, putting the preserved host components # at the end of the new proxy RO. host_components = current_site.getSiteManager().host_components # We need to keep host_components in the bases # for the new site. Where to put it is tricky # if we want to support multiple layers of overriding # of host registrations. Fortunately, the zope.interface.ro # machinery does exactly the right thing if we tack host # components (which are probably not already in the list) # on to the end. If they are in the list already, they # stay where they were. new_bases = new_site.getSiteManager().__bases__ + (host_components,) # TODO: We don't need to proxy the site manager, right? # it's almost never special by itself... new_site_manager = BasedSiteManager(new_site.__parent__, new_site.__name__, new_bases) new_site_manager.host_components = host_components new_fake_site = _ProxyTraversedSite(new_site, new_site_manager) setSite(new_fake_site) return # We're out of special cases we understand. Hopefully the # application knows what it is doing. setSite(new_site)
[docs]@component.adapter(INewLocalSite) def new_local_site_dispatcher(event): """ Dispatches just like an object event, that way we can do things based on the type of the site manager. Note that if the containing ISite is (re)moved, an ObjectEvent will be fired for (sitemanager, site-event); that is, you subscribe to the site manager and the object moved event, but the event will have the ISite as the object property. """ component.handle(event.manager, event)
@component.adapter(IHostPolicyFolder, IObjectRemovedEvent) def _on_site_removed(site, unused_event=None): """ Unregister the ``IBaseComponents`` for a removed site. .. versionadded:: 1.4.0 """ name = site.__name__ site_components = component.queryUtility(IComponents, name=name) if site_components is not None: unregisterUtility(component.getSiteManager(), site_components, IComponents, name=name)