Skip to main content

Symlinks for Zope 3.

Project description

Shortcuts

Shortcuts are objects that allow other objects (their target) to appear to be located in places other than the target’s actual location. They are somewhat like a symbolic link in Unix-like operating systems.

Creating a shortcut

Shortcuts are created by calling the Shortcut class’s constructor with a target, parent, and name:

>>> from zc.shortcut.shortcut import Shortcut
>>> class MyTarget:
...     attr = 'hi'
...     __parent__ = 'Original Parent'
...     __name__ = 'Original Name'
>>> target = MyTarget()
>>> sc = Shortcut(target)
>>> sc.__parent__ = 'My Parent'
>>> sc.__name__ = 'My Name'

A shortcut provides an attribute to access its target:

>>> sc.target
<__builtin__.MyTarget instance at ...>

A shortcut’s __parent__ and __name__ are independent of their target:

>>> sc.__parent__
'My Parent'
>>> sc.target.__parent__
'Original Parent'

>>> sc.__name__
'My Name'
>>> sc.target.__name__
'Original Name'

But the target knows the traversal parent, the traversal name, and the shortcut. This allows the shortcut to have annotations that may be accessed by views and other components that render or use the target.

>>> sc.target.__traversed_parent__
'My Parent'
>>> sc.target.__traversed_name__
'My Name'
>>> sc.target.__shortcut__ is sc
True

See proxy.txt and adapters.txt for more details

Adapters

Adapters are provided to allow a shortcut to act as the target would when traversed.

ITraversable

First we have to import the interfaces we’ll be working with:

>>> from zope.publisher.interfaces import IRequest
>>> from zope.publisher.interfaces.browser import IBrowserPublisher
>>> from zope.traversing.interfaces import ITraversable
>>> from zc.shortcut.interfaces import IShortcut
>>> from zope.location.interfaces import ILocation
>>> from zc.shortcut import interfaces

If we have a target object with a root:

>>> from zope import interface, component
>>> class ISpam(interface.Interface):
...     pass

>>> class Spam:
...     interface.implements(ISpam, ILocation)
...     def __init__(self, parent, name):
...         self.__parent__ = parent
...         self.__name__ = name

>>> from zope.traversing.interfaces import IContainmentRoot
>>> class DummyContainmentRoot(object):
...     __parent__ = __name__ = None
...     interface.implements(IContainmentRoot)
...
>>> root = DummyContainmentRoot()

>>> real_parent = Spam(root, 'real_parent')
>>> target = Spam(real_parent, 'target')

The target object provides a multiadapter for the target and request to an ITraversable so it can be traversed:

>>> class SpamTraversableAdapter:
...     interface.implements(ITraversable)
...     component.adapts(ISpam, IRequest)
...     def __init__(self, spam, request):
...         self.spam = spam
>>> component.provideAdapter(SpamTraversableAdapter, name='view')

There is an adapter to return the target object adapted to ITraversable when a shortcut and request is adapted to ITraversable. For example if we create a shortcut to our target:

>>> from zc.shortcut.shortcut import Shortcut
>>> shortcut = Shortcut(target)
>>> shortcut_parent = Spam(root, 'shortcut_parent')
>>> shortcut.__parent__ = shortcut_parent
>>> shortcut.__name__ = 'shortcut'

And call the adapter with a request:

>>> from zope.publisher.browser import TestRequest
>>> from zc.shortcut.adapters import ShortcutTraversalAdapterFactory

>>> request = TestRequest()
>>> adapter = ShortcutTraversalAdapterFactory(shortcut, request)

The result is the target’s ITraversal adapter:

>>> adapter
<...SpamTraversableAdapter instance at...>

>>> adapter.spam
<...Spam instance at...>

Shortcut traversal

Shortcut traversal is unpleasantly tricky. First consider the case of traversing a shortcut and then traversing to get the default view (‘index.html’). In that case, the shortcut will be available to the view, and breadcrumbs and other view elements that care about how the object was traversed will merely need to look at the shortcut’s __parent__, or the target proxy’s __traversed_parent__. This is not too bad.

It becomes more interesting if one traverses through a shortcut to another content object. A naive implementation will traverse the shortcut by converting it to its target, and then traversing the target to get the contained content object. However, views for the content object will have no idea of the traversal path used to get to the content object: they will only have the __parent__ of the content object, which is the shortcut’s target without any target proxy. From there they will be able to find the target’s parent, but not the traversed shortcut’s parent. Breadcrumbs and other components that care about traversed path will be broken.

In order to solve this use case, traversing a shortcut needs to traverse the target and then wrap the resulting object in another target proxy that holds a reference to the shortcut’s target proxy as its traversed parent.

Traversing a shortcut and finding another shortcut is slightly trickier again. In this case, the shortcut’s target’s proxy should have a parent which is the shortcut’s proxy’s parent.

Two adapters are available for IPublishTraverse: one for shortcuts, and one for traversal proxies. If a traversal target doesn’t provide IPublishTraverse, then it should provide an adapter:

>>> from zc.shortcut import adapters
>>> from zope.publisher.interfaces import IPublishTraverse
>>> child_spam = Spam(real_parent, 'child_spam')
>>> child_shortcut = Shortcut(child_spam)
>>> child_shortcut.__parent__ = shortcut
>>> child_shortcut.__name__ = 'child_shortcut'
>>> class SpamPublishTraverseAdapter:
...     interface.implements(IPublishTraverse)
...     component.adapts(ISpam, IRequest)
...     def __init__(self, spam, request):
...         self.spam = spam
...     def publishTraverse(self, request, name):
...         print 'SpamPublishTraverseAdapter has been traversed.'
...         return {'child_spam': child_spam,
...                 'child_shortcut': child_shortcut}[name]
>>> component.provideAdapter(SpamPublishTraverseAdapter)

If it does, the adapter will be used to do the traversal:

>>> adapter = adapters.ShortcutPublishTraverseAdapter(shortcut, request)
>>> adapter
<...ShortcutPublishTraverseAdapter object at...>
>>> from zope.interface.verify import verifyObject
>>> verifyObject(IPublishTraverse, adapter)
True
>>> res = adapter.publishTraverse(request, 'child_spam')
SpamPublishTraverseAdapter has been traversed.

Notice that the traversed object has a traversal proxy (but not a target proxy).

>>> interfaces.ITraversalProxy.providedBy(res)
True
>>> interfaces.ITargetProxy.providedBy(res)
False
>>> res.__traversed_parent__ == shortcut.target
True
>>> res.__traversed_name__
'child_spam'
>>> res.__traversed_parent__.__shortcut__ is shortcut
True
>>> res.__traversed_parent__.__traversed_parent__ is shortcut_parent
True

To traverse further down and still keep the traversal information, we need to register the ProxyPublishTraverseAdapter. Notice that we will also traverse to a shortcut this time, and look at the traversal trail up from the shortcut and from its target.

>>> component.provideAdapter(adapters.ProxyPublishTraverseAdapter)
>>> from zope import component
>>> adapter = component.getMultiAdapter((res, request), IPublishTraverse)
>>> res = adapter.publishTraverse(request, 'child_shortcut')
SpamPublishTraverseAdapter has been traversed.
>>> res.__traversed_parent__ == child_spam
True
>>> res.__traversed_name__
'child_shortcut'
>>> res.__traversed_parent__.__traversed_parent__ == shortcut.target
True
>>> res.target.__traversed_parent__.__traversed_parent__ == shortcut.target
True

If, instead, the target implements IPublishTraverse itself…:

>>> class SpamWithPublishTraverse(Spam):
...     interface.implements(IPublishTraverse)
...     def publishTraverse(self, request, name):
...         print 'SpamWithPublishTraverse has been traversed.'
...         return {'child_spam': child_spam,
...                 'child_shortcut': child_shortcut}[name]

…then it’s publishTraverse() will be called directly:

>>> spam = SpamWithPublishTraverse(real_parent, 'special_spam')
>>> shortcut = Shortcut(spam)
>>> shortcut.__parent__ = shortcut_parent
>>> shortcut.__name__ = 'special_spam_shortcut'
>>> adapter = adapters.ShortcutPublishTraverseAdapter(shortcut, request)
>>> adapter
<...ShortcutPublishTraverseAdapter object at...>

>>> another = adapter.publishTraverse(request, 'child_spam')
SpamWithPublishTraverse has been traversed.

Ending traversal at a shortcut

When a shortcut is the target of a URL traversal, rather than a node along the way, the leaf-node handling of the target object must be invoked so that the shortcut behaves in the same way as the would would when accessed directly.

When a URL from a request represents an object (rather than a view), the publisher uses the browserDefault() method of the IBrowserPublisher interface to determine how the object should be handled. This method returns an object and a sequences of path elements that should be traversed.

For shortcuts, this is handled by delegating to the target of the shortcut, substituting a proxy for the target so the traversedURL view and breadcrumbs still work correctly.

Let’s start by defining an IBrowserPublisher for ISpam objects:

>>> class SpamBrowserPublisherAdapter(SpamPublishTraverseAdapter):
...     interface.implements(IBrowserPublisher)
...     def browserDefault(self, request):
...         print "browserDefault for", repr(self.spam)
...         return self.spam, ("@@foo.html",)
>>> component.provideAdapter(SpamBrowserPublisherAdapter,
...                          provides=IBrowserPublisher)

>>> adapter.browserDefault(request)  # doctest: +ELLIPSIS
browserDefault for <...SpamWithPublishTraverse instance at 0x...>
(<...SpamWithPublishTraverse instance at 0x...>, ('@@foo.html',))

traversedURL

If shortcuts are traversed, an absolute url can lead a user to unexpected locations–to the real location of the object, rather than to the traversed location. In order to get the traversed url, the adapters module provides a traversedURL function, and the shortcut package also offers it from its __init__.py.

Given the result of the next-to-last shortcut traversal described above, for instance, traversedURL returns a URL that behaves similarly to absoluteURL except when it encounters target proxies, at which point the traversal parents are used rather than the actual parents.

>>> component.provideAdapter(adapters.TraversedURL)
>>> component.provideAdapter(adapters.FallbackTraversedURL)
>>> component.provideAdapter(adapters.RootTraversedURL)
>>> adapters.traversedURL(res, request)
'http://127.0.0.1/shortcut_parent/shortcut/child_spam/child_shortcut'

Like absoluteURL, the returned value is html escaped.

>>> shortcut_parent.__name__ = 'shortcut parent'
>>> adapters.traversedURL(res, request)
'http://127.0.0.1/shortcut%20parent/shortcut/child_spam/child_shortcut'

Also like absoluteURL, traversedURL is registered as a view so it can be used within page templates (as in context/@@traversedURL).

>>> component.provideAdapter(adapters.traversedURL, name="traversedURL")
>>> component.getMultiAdapter((res, request), name='traversedURL')
'http://127.0.0.1/shortcut%20parent/shortcut/child_spam/child_shortcut'

Shortcut IAdding

The shortcut adding has a couple of different behaviors than the standard Zope 3 adding. The differences are to support traversal proxies; and to provide more flexibility for choosing the nextURL after an add.

Supporting Traversal Proxies

Both the action method and the nextURL method redirect to the absoluteURL of the container in the zope.app implementation. In the face of shortcuts and traversal proxies, this can generate surprising behavior for users, directing their URL to a location other than where they thought they were working. The shortcut adding changes both of these methods to use traversedURL instead. As a result, adding to a shortcut of a container returns the user to the shortcut, not the absolute path of the container’s real location; and submitting the form of the default view of the adding redirects to within the context of the traversed shortcut(s), not the absoluteURL.

The action method changes are pertinent to redirecting to an adding view.

>>> from zc.shortcut import adding, interfaces
>>> from zope import interface, component
>>> from zope.location.interfaces import ILocation
>>> class ISpam(interface.Interface):
...     pass
...
>>> class Spam(dict):
...     interface.implements(ISpam, ILocation)
...     def __init__(self, parent, name):
...         self.__parent__ = parent
...         self.__name__ = name
...
>>> from zope.traversing.interfaces import IContainmentRoot
>>> class DummyContainmentRoot(object):
...     interface.implements(IContainmentRoot)
...
>>> root = DummyContainmentRoot()
>>> real_parent = Spam(root, 'real_parent')
>>> target = Spam(real_parent, 'target')
>>> from zc.shortcut.shortcut import Shortcut
>>> shortcut = Shortcut(target)
>>> shortcut_parent = Spam(root, 'shortcut_parent')
>>> shortcut.__parent__ = shortcut_parent
>>> shortcut.__name__ = 'shortcut'
>>> from zc.shortcut import adapters
>>> component.provideAdapter(adapters.TraversedURL)
>>> component.provideAdapter(adapters.FallbackTraversedURL)
>>> component.provideAdapter(adapters.RootTraversedURL)
>>> from zope.publisher.interfaces import IRequest
>>> @component.adapter(interfaces.IAdding, IRequest)
... @interface.implementer(interface.Interface)
... def dummyAddingView(adding, request):
...     return 'this is a view'
...
>>> component.provideAdapter(dummyAddingView, name='foo_type')
>>> from zope.publisher.browser import TestRequest
>>> request = TestRequest()
>>> adder = adding.Adding(shortcut.target, request)
>>> adder.action('foo_type', 'foo_id')
>>> request.response.getHeader('Location')
'http://127.0.0.1/shortcut_parent/shortcut/@@+/foo_type=foo_id'

The nextURL method changes are pertinent to the default behavior.

>>> adder.contentName = 'foo_id'
>>> target['foo_id'] = Spam(target, 'foo_id')
>>> adder.nextURL()
'http://127.0.0.1/shortcut_parent/shortcut/@@contents.html'

Adding Flexibility to ‘nextURL’

The nextURL method in the zope.app implementation of an adding defines precisely what the nextURL should be: the @@contents.html view of the context. The shortcut adding recreates this behavior, but only after seeing if different behavior has been registered.

nextURL tries to find an adapter named with the constant in zc.shortcut.interfaces.NEXT_URL_NAME, providing nothing, for the adding, the new content as found in the container (so it may be a shortcut), and the context. If an adapter is registered, it should be a string of the nextURL to be used; this value will be returned. If no adapter is registered or the registered adapter returns None, the @@contents.html view of the context is returned.

>>> @component.adapter(interfaces.IAdding, ISpam, ISpam)
... @interface.implementer(interface.Interface)
... def sillyNextURL(adding, content, container):
...     return '%s class added "%s" to "%s"' % (
...         adding.__class__.__name__,
...         content.__name__,
...         container.__name__)
...
>>> component.provideAdapter(sillyNextURL, name=interfaces.NEXT_URL_NAME)
>>> adder.nextURL()
'Adding class added "foo_id" to "target"'

Shortcut factories

Shortcut factories are factories that place objects in a configured folder and then return a shortcut to the new object. Because they create objects and place them in containers, they fire an object creation event, and usually the configured folder fires an object added event.

>>> from zc.shortcut import factory, interfaces, Shortcut
>>> from zope import interface, component, event
>>> class IDummy(interface.Interface):
...     pass
...
>>> from zope.location.interfaces import ILocation
>>> class Dummy(object):
...     interface.implements(IDummy, ILocation)
...     def __init__(self, *args, **kwargs):
...         self.args = args
...         self.kwargs = kwargs
...
>>> f = factory.Factory(Dummy, 'title', 'description')
>>> from zope.interface import verify
>>> verify.verifyObject(interfaces.IShortcutFactory, f)
True

The factory always returns an interface declaration for a shortcut from getInterfaces, while getTargetInterfaces returns the declaration for the created object.

>>> f.getInterfaces() == interface.implementedBy(Shortcut)
True
>>> f.getTargetInterfaces() == interface.implementedBy(Dummy)
True

factories will fail to create an object if a container has not been registered as a repository.

>>> f() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
Traceback (most recent call last):
...
ComponentLookupError: (<Dummy...>, <...IContainer>, 'shortcutTargetRepository')

If we register a repository then the factory will fire a creation event, add the object to the repository, and return a shortcut to the new object.

>>> import zope.app.container.interfaces
>>> class DummyContainer(dict):
...     interface.implements(zope.app.container.interfaces.IContainer)
...
>>> repo = DummyContainer()
>>> @component.adapter(IDummy)
... @interface.implementer(zope.app.container.interfaces.IContainer)
... def DummyRepoGetter(content):
...     return repo
...
>>> component.provideAdapter(
...     DummyRepoGetter, name=interfaces.REPOSITORY_NAME)
>>> from zope.app.container.contained import NameChooser
>>> component.provideAdapter(NameChooser, adapts=(interface.Interface,))
>>> # now, before we actually actually run the adding machinery, we'll
>>> # set up some machinery that will let us look at events firing
...
>>> heard_events = [] # we'll collect the events here
>>> event.subscribers.append(heard_events.append)
>>> import pprint
>>> from zope import interface
>>> showEventsStart = 0
>>> def iname(ob):
...     return iter(interface.providedBy(ob)).next().__name__
...
>>> def getId(ob):
...     if ob is None or isinstance(ob, (int, float, basestring, tuple)):
...         return "(%r)" % (ob,)
...     id = getattr(ob, 'id', getattr(ob, '__name__', None))
...     if not id:
...         id = "a %s (%s)" % (ob.__class__.__name__, iname(ob))
...     return id
...
>>> def showEvents(start=None): # to generate a friendly view of events
...     global showEventsStart
...     if start is None:
...         start = showEventsStart
...     res = [
...         '%s fired for %s.' % (iname(ev), getId(ev.object))
...         for ev in heard_events[start:]]
...     res.sort()
...     pprint.pprint(res)
...     showEventsStart = len(heard_events)
...
>>> sc = f(12, 'foo', 'barbaz', sloop=19)
>>> showEvents()
['IObjectCreatedEvent fired for a Dummy (IDummy).']
>>> repo['Dummy'].args
(12, 'foo', 'barbaz')
>>> repo['Dummy'].kwargs
{'sloop': 19}
>>> sc.raw_target is repo['Dummy']
True
>>> event.subscribers.pop() is not None # cleanup
True

Using alternate shortcut implementations

The shortcut factory takes an optional keyword parameter to specify the factory used to create the shortcut. By default, zc.shortcut.Shortcut is used, but more specialized shortcuts may be needed for some applications. This allows the factory to be used regardless of the specific shortcut implementation.

Let’s create an alternate class that can be used as a shortcut (it doesn’t really matter that the example class isn’t useful):

>>> class AlternateShortcut(object):
...     interface.implements(interfaces.IShortcut)
...     def __init__(self, object):
...         self.raw_target = object
...         self.target = object

Now we can create a factory that creates instances of this class instead of the default shortcut class:

>>> f = factory.Factory(Dummy, 'title', 'description',
...                     shortcut_factory=AlternateShortcut)

Using the factory returns an instance of our alternate shortcut implementation:

>>> sc = f(1, 2, 3)

>>> isinstance(sc, AlternateShortcut)
True
>>> isinstance(sc.raw_target, Dummy)
True
>>> sc.target.args
(1, 2, 3)

Project details


Release history Release notifications | RSS feed

This version

1.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zc.shortcut-1.0.tar.gz (38.1 kB view hashes)

Uploaded Source

Built Distribution

zc.shortcut-1.0-py2.4.egg (55.2 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page