~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
= 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("""
    ... <configure xmlns:browser="http://namespaces.zope.org/browser">
    ...   <include file="lib/lp/services/webapp/meta.zcml" />
    ...   <browser:url
    ...       for="lp.testing.ITown"
    ...       path_expression="string:+towns/${name}"
    ...       attribute_to_parent="countryOopsTypo"
    ...       />
    ... </configure>
    ... """)
    Traceback (most recent call last):
    ...
    ZopeXMLConfigurationError: File "<string>", line ...
        AttributeError: The name "countryOopsTypo" is not in lp.testing.ITown

    >>> zcmlcontext = xmlconfig.string("""
    ... <configure xmlns:browser="http://namespaces.zope.org/browser">
    ...   <include file="lib/lp/services/webapp/meta.zcml" />
    ...   <browser:url
    ...       for="lp.testing.ITown"
    ...       path_expression="string:+towns/${name}"
    ...       attribute_to_parent="country"
    ...       />
    ... </configure>
    ... """)

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("""
    ... <configure
    ...     xmlns="http://namespaces.zope.org/zope"
    ...     xmlns:browser="http://namespaces.zope.org/browser">
    ...   <include file="lib/lp/services/webapp/meta.zcml" />
    ...   <configure package="zope.app.component">
    ...       <include file="meta.zcml" />
    ...   </configure>
    ...   <utility
    ...       provides="lp.testing.ICountrySet"
    ...       component="lp.testing.countryset_instance"
    ...       />
    ...   <browser:url
    ...       for="lp.testing.ICountry"
    ...       path_expression="name"
    ...       parent_utility="lp.testing.ICountrySet"
    ...       />
    ... </configure>
    ... """)

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("""
    ... <configure xmlns:browser="http://namespaces.zope.org/browser">
    ...   <include file="lib/lp/services/webapp/meta.zcml" />
    ...   <browser:url
    ...       for="lp.testing.ICountrySet"
    ...       urldata="lp.testing.CountrySetUrl"
    ...       />
    ... </configure>
    ... """)

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("""
    ... <configure
    ...     xmlns="http://namespaces.zope.org/zope"
    ...     xmlns:browser="http://namespaces.zope.org/browser">
    ...   <include file="lib/lp/services/webapp/meta.zcml" />
    ...   <configure package="zope.app.component">
    ...       <include file="meta.zcml" />
    ...   </configure>
    ...   <utility
    ...       provides="lp.testing.ICountrySet"
    ...       component="lp.testing.countryset_instance"
    ...       />
    ...   <browser:url
    ...       for="lp.testing.ICountry"
    ...       path_expression="name"
    ...       parent_utility="lp.testing.ICountrySet"
    ...       rootsite="bugs"
    ...       />
    ... </configure>
    ... """)

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