= Canonical URLs = https://launchpad.canonical.com/CanonicalUrls == The browser:url ZCML directive == The browser:url directive registers an ICanonicalUrlData adapter. In this test, we'll use a URL hierarchy /countries/England/+towns/London In this test, we'll use interfaces ICountrySet, ICountry and ITown, which we will put in lp.testing. >>> from lp.services.webapp.interfaces import ICanonicalUrlData >>> from zope.interface import Interface, Attribute, implements >>> class ICountrySet(Interface): ... pass >>> class ICountry(Interface): ... name = Attribute('the name of this country') >>> class ITown(Interface): ... """Dummy interface for use in browser:url tests.""" ... country = Attribute('the country the town is in') ... name = Attribute('the name of this town') Add a view for Country/+map. >>> from zope.component import provideAdapter >>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer >>> class CountryMapView(object): ... def __init__(self, country, request): ... pass >>> provideAdapter(CountryMapView, (ICountry, IDefaultBrowserLayer), ... name='+map', provides=Interface) Define a navigation for the Country URL. >>> from lp.services.webapp.publisher import ( ... Navigation, redirection, stepto, stepthrough) >>> from zope.publisher.interfaces.browser import IBrowserPublisher >>> class CountryNavigation(Navigation): ... redirection('+capital', '+towns/London') ... ... @stepthrough('+towns') ... def stepthrown_town(self, name): ... if name == 'London': ... return town_instance ... else: ... return None ... ... @stepto('+greenwich') ... def stepto_greenwhich(self): ... town = Town() ... town.name = 'Greenwich' ... return town >>> provideAdapter( ... CountryNavigation, [ICountry, IDefaultBrowserLayer], ... IBrowserPublisher) Put the interfaces into lp.testing, ensuring that there are not already objects with those names there. >>> import lp.testing >>> for interface in ICountrySet, ICountry, ITown: ... name = interface.getName() ... assert getattr(lp.testing, name, None) is None ... setattr(lp.testing, name, interface) ... interface.__module__ = 'lp.testing' We don't have ICanonicalUrlData adapters for objects that provide any of these interfaces. First, we create some countryset, country and town objects. >>> class CountrySet: ... implements(ICountrySet) >>> countryset_instance = CountrySet() >>> class Country: ... implements(ICountry) ... name = 'England' >>> country_instance = Country() >>> class Town: ... implements(ITown) ... country = country_instance ... name = 'London' >>> town_instance = Town() Next, we check there are no ICanonicalUrlData adapters for these objects. >>> for obj in countryset_instance, country_instance, town_instance: ... assert ICanonicalUrlData(obj, None) is None Configure a browser:url for ITown. Our first attempt fails because we mistyped 'countryOopsTypo', and there is no such name in ITown. >>> from zope.configuration import xmlconfig >>> zcmlcontext = xmlconfig.string(""" ... ... ... ... ... """) Traceback (most recent call last): ... ZopeXMLConfigurationError: File "", line ... AttributeError: The name "countryOopsTypo" is not in lp.testing.ITown >>> zcmlcontext = xmlconfig.string(""" ... ... ... ... ... """) Now, there is an ICanonicalUrlData registered for ITown. >>> town_urldata = ICanonicalUrlData(town_instance) >>> from lp.services.webapp.testing import verifyObject >>> verifyObject(ICanonicalUrlData, town_urldata) True >>> town_urldata.path u'+towns/London' >>> town_urldata.inside is country_instance True The parent of an object might be accessible via an attribute, or it might be a utility. This is the case for an ICountry object: its parent is the ICountrySet. I need to put the countryset_instance somewhere we can get at it from zcml. I'll put it in lp.testing. >>> assert getattr( ... lp.testing, 'countryset_instance', None) is None >>> lp.testing.countryset_instance = countryset_instance >>> zcmlcontext = xmlconfig.string(""" ... ... ... ... ... ... ... ... ... """) Now, there is an ICanonicalUrlData registered for ICountry. >>> country_urldata = ICanonicalUrlData(country_instance) >>> from lp.services.webapp.testing import verifyObject >>> verifyObject(ICanonicalUrlData, country_urldata) True >>> country_urldata.path 'England' >>> country_urldata.inside is countryset_instance True We need to specify a browser:url for ICountrySet. We'll use a variation on the zcml that allows us to directly set an ICanonicalUrlData adapter to use. The adapter will make its parent the ILaunchpadRoot utility. This is not the normal way to do this. Normally, we'd just say parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot" But, here, I want to test the variant of the zcml directive that specifes an adapter. >>> from lp.services.webapp.interfaces import ILaunchpadRoot >>> class CountrySetUrl: ... ... implements(ICanonicalUrlData) ... ... def __init__(self, context): ... self.context = context ... ... path = 'countries' ... ... rootsite = None ... ... @property ... def inside(self): ... return getUtility(ILaunchpadRoot) The CountrySetUrl class needs to be accessible from zcml. So, we put it in lp.testing. >>> lp.testing.CountrySetUrl = CountrySetUrl >>> zcmlcontext = xmlconfig.string(""" ... ... ... ... ... """) Now, there is an ICanonicalUrlData registered for ICountrySet. >>> countryset_urldata = ICanonicalUrlData(countryset_instance) >>> from lp.services.webapp.testing import verifyObject >>> verifyObject(ICanonicalUrlData, countryset_urldata) True >>> countryset_urldata.path 'countries' >>> countryset_urldata.inside is getUtility(ILaunchpadRoot) True == The Launchpad root object == The ILaunchpadRoot object has its own ICanonicalUrlData adapter. >>> root_urldata = ICanonicalUrlData(getUtility(ILaunchpadRoot)) >>> verifyObject(ICanonicalUrlData, root_urldata) True >>> root_urldata.path '' >>> root_urldata.inside is None True == The canonical_url function == The canonical_url function gives you the canonical URL for an object, by stitching together the various ICanonicalUrlData adapters for that object and the objects it is inside of (or in other words, hierarchically below). >>> from lp.services.webapp import canonical_url >>> canonical_url(getUtility(ILaunchpadRoot)) u'http://launchpad.dev/' >>> canonical_url(countryset_instance) u'http://launchpad.dev/countries' >>> canonical_url(country_instance) u'http://launchpad.dev/countries/England' >>> canonical_url(town_instance) u'http://launchpad.dev/countries/England/+towns/London' We can see that this is the mainsite rooturl as configured in launchpad.conf. >>> from lp.services.webapp.vhosts import allvhosts >>> print allvhosts.configs['mainsite'].rooturl http://launchpad.dev/ If anywhere in the chain we have an object that cannot be adapted to ICanonicalUrlData, a NoCanonicalUrl error is raised. The next few lines tests the case where the object you want a URL for cannot itself be adapted to ICanonicalUrlData. >>> object_that_has_no_url = object() >>> canonical_url(object_that_has_no_url) Traceback (most recent call last): ... NoCanonicalUrl: No url for <...object at ...> because <...object at ...> broke the chain. Now, we must test the case where the object can be adapted to ICanonicalUrlData, but its parent or its parent's parent (and so on) cannot. >>> class ObjectThatHasUrl: ... implements(ICanonicalUrlData) ... def __init__(self, name, parent): ... self.path = name ... self.inside = parent >>> unrooted_object = ObjectThatHasUrl('unrooted', object_that_has_no_url) >>> canonical_url(unrooted_object) Traceback (most recent call last): ... NoCanonicalUrl: No url for <...ObjectThatHasUrl...> because <...object...> broke the chain. The first argument to NoCanonicalUrl is the object that a canonical url was requested for. The second argument is the object that broke the chain. == The canonical_urldata_iterator function == TODO. Currently tested implicitly by the canonical_url_iterator tests. == The canonical_url_iterator function == The canonical_url_iterator function is not available from .webapp because it won't be used in the general application, just by parts of the webapp systems. >>> from lp.services.webapp.publisher import canonical_url_iterator First, let's define a helper function to help us test canonical_url_iterator. >>> def print_url_iterator(obj): ... for obj in canonical_url_iterator(obj): ... print obj.__class__.__name__ >>> print_url_iterator(getUtility(ILaunchpadRoot)) RootObject >>> print_url_iterator(countryset_instance) CountrySet RootObject >>> print_url_iterator(country_instance) Country CountrySet RootObject We have to do the tests that involve errors bit by bit, to allow the doctest to work properly. >>> iterator = canonical_url_iterator(object_that_has_no_url) >>> iterator.next().__class__.__name__ 'object' >>> iterator.next() Traceback (most recent call last): ... NoCanonicalUrl: No url for <...object...> because <...object...> broke the chain. >>> iterator = canonical_url_iterator(unrooted_object) >>> iterator.next().__class__.__name__ 'ObjectThatHasUrl' >>> iterator.next().__class__.__name__ 'object' >>> iterator.next() Traceback (most recent call last): ... NoCanonicalUrl: No url for <...ObjectThatHasUrl...> because <...object...> broke the chain. == canonical_url and requests == You can pass an http request object into canonical_url as its optional second argument. This tells canonical_url to use the protocol, host and port from the request. To get this information, canonical_url uses the operation getApplicationURL() from zope.publisher.interfaces.http.IHTTPApplicationRequest. >>> from zope.publisher.interfaces.http import IHTTPApplicationRequest >>> class FakeRequest: ... ... implements(IHTTPApplicationRequest) ... ... def __init__(self, applicationurl): ... self.applicationurl = applicationurl ... self.interaction = None ... ... def getRootURL(self, rootsite): ... if rootsite is not None: ... return allvhosts.configs[rootsite].rooturl ... else: ... return self.getApplicationURL() + '/' ... ... def getApplicationURL(self, depth=0, path_only=False): ... assert depth == 0, 'this is not a real IHTTPApplicationRequest' ... assert not path_only, 'not a real IHTTPApplicationRequest' ... return self.applicationurl >>> mandrill_request = FakeRequest('https://mandrill.example.org:23') >>> canonical_url(country_instance) u'http://launchpad.dev/countries/England' >>> canonical_url(country_instance, mandrill_request) u'https://mandrill.example.org:23/countries/England' However, if we log in, then that request should be used when none is explicitly given otherwise. >>> sesame_request = FakeRequest('http://muppet.example.com') >>> login(ANONYMOUS, sesame_request) >>> canonical_url(country_instance) u'http://muppet.example.com/countries/England' >>> canonical_url(country_instance, mandrill_request) u'https://mandrill.example.org:23/countries/England' == canonical_url and overriding rootsite == The optional parameter rootsite on the canonical_url function can be used to 'force' the url to a different rootsite. Providing a rootsite overrides the rootsite defined by either the object or the request. Here is the current country instance without the ICanonicalUrlData specifying a rootsite. Overriding the rootsite from the default request: >>> canonical_url(country_instance) u'http://muppet.example.com/countries/England' >>> canonical_url(country_instance, rootsite='code') u'http://code.launchpad.dev/countries/England' Overriding the rootsite from the specified request: >>> canonical_url(country_instance, mandrill_request) u'https://mandrill.example.org:23/countries/England' >>> canonical_url(country_instance, mandrill_request, rootsite='code') u'http://code.launchpad.dev/countries/England' And if the configuration does provide a rootsite: >>> zcmlcontext = xmlconfig.string(""" ... ... ... ... ... ... ... ... ... """) >>> canonical_url(country_instance) u'http://bugs.launchpad.dev/countries/England' >>> canonical_url(country_instance, rootsite='code') u'http://code.launchpad.dev/countries/England' >>> canonical_url(country_instance, mandrill_request, rootsite='code') u'http://code.launchpad.dev/countries/England' == canonical_url and named views == The url for a particular view of an object can be generated by specifying the view's name. >>> canonical_url(country_instance, view_name="+map") u'http://bugs.launchpad.dev/countries/England/+map' view_name also works when the view_name refers to a Navigation stepto, stepthrough, or redirection: >>> canonical_url(country_instance, view_name="+greenwich") u'http://bugs.launchpad.dev/countries/England/+greenwich' >>> canonical_url(country_instance, view_name="+capital") u'http://bugs.launchpad.dev/countries/England/+capital' >>> canonical_url(country_instance, view_name="+towns") u'http://bugs.launchpad.dev/countries/England/+towns' Giving an unregistered view name will trigger an assertion failure. >>> canonical_url(country_instance, view_name="+does-not-exist") Traceback (most recent call last): ... AssertionError: Name "+does-not-exist" is not registered as a view or navigation step for "Country" on "bugs". == The 'nearest' helper function == The `nearest(obj, *interfaces)` function returns the nearest object up the canonical url chain that provides at least one of the interfaces given. >>> from lp.services.webapp import nearest >>> from lp.registry.interfaces.person import IPerson >>> nearest(town_instance, IPerson) is None True >>> nearest(town_instance, ITown) is town_instance True >>> nearest(town_instance, IPerson, ITown) is town_instance True >>> nearest(town_instance, ICountry) is country_instance True >>> print nearest(unrooted_object, ICountry) None == canonical_url in the web service == canonical_url() is sometimes used in code that doesn't have direct access to the current request, and always wants a URL that can be used in a browser (for example e-mail notifications or XHTML representations of objects). Therefore, if no request is explicitly given, canonical_url() returns the browser URL, even if the current request is a web service request >>> from zope.app.security.principalregistry import ( ... UnauthenticatedPrincipal) >>> from lp.services.webapp.interaction import setupInteraction >>> from lp.services.webapp.servers import WebServiceTestRequest >>> from lazr.restful.utils import get_current_browser_request >>> anonymous = UnauthenticatedPrincipal(None, None, None) >>> api_request = WebServiceTestRequest() >>> setupInteraction(anonymous, participation=api_request) >>> get_current_browser_request() is api_request True >>> canonical_url(countryset_instance) u'http://launchpad.dev/countries' If an URL that can be used in the web service is required, a web service request has to be passed in explicitly. >>> canonical_url(countryset_instance, request=api_request) u'http://api.launchpad.dev/countries' It is often the case that the web application wants to provide URLs that will be written out onto the pages that the Javascript can process using the LP.client code to get access to the object entries using the API. In these cases, the "force_local_path" parameter can be passed to canonical_url to have only the relative local path returned. >>> canonical_url(countryset_instance, force_local_path=True) u'/countries' == The end == We've finished with our interfaces and utility component, so remove them from lp.testing. >>> for name in ['ICountrySet', 'ICountry', 'ITown', 'countryset_instance', ... 'CountrySetUrl']: ... delattr(lp.testing, name)