Merge lp:~salgado/launchpad/breadcrumbs-for-leafs into lp:launchpad

Proposed by Guilherme Salgado
Status: Merged
Approved by: Paul Hummer
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~salgado/launchpad/breadcrumbs-for-leafs
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~salgado/launchpad/breadcrumbs-for-leafs
Reviewer Review Type Date Requested Status
Paul Hummer (community) Approve
Review via email: mp+11985@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Guilherme Salgado (salgado) wrote :

= Summary =

Change text of leaf breadcrumbs from the page name (+export, etc) to its
title, fixing https://bugs.edge.launchpad.net/bugs/423691

== Proposed fix ==

Previously, only objects which had a Navigation class would be appended
to request.traversed_objects upon traversal. From now on, any traversed
objects will be appended there, including the views. This fixes
https://bugs.edge.launchpad.net/bugs/423898

Since we now have the view in traversed_objects, we can just use
its .page_title (falling back to its entry on pagetitles.py) as the text
of the leaf breadcrumb. Like before, though, this leaf breadcrumb is
only added when the view is not the default one for the context.

== Implementation details ==

MultiStepViews don't define a .page_title because what actually gets
rendered is the (step) view that they contain, and those define
a .page_title. However, we don't want to do that when looking up the
text for the breadcrumbs, so MultiStepViews are now required to provide
a .page_title.

Some tests from doc/navigation.txt where moved to
webapp/tests/test_publication.py and others to
webapp/tests/test_servers.py

== Tests ==

./bin/test -vvt test_breadcrumbs

== Demo and Q/A ==

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/canonical/launchpad/webapp/publication.py
  lib/canonical/launchpad/webapp/tests/test_publication.py
  lib/lp/translations/browser/tests/test_breadcrumbs.py
  lib/canonical/launchpad/webapp/tests/breadcrumbs.py
  lib/canonical/launchpad/webapp/publisher.py
  lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py
  lib/canonical/launchpad/browser/multistep.py
  lib/lp/registry/browser/product.py
  lib/lp/soyuz/stories/soyuz/xx-builder-page.txt
  lib/canonical/launchpad/browser/launchpad.py
  lib/lp/bugs/browser/bugalsoaffects.py
  lib/lp/translations/browser/pofile.py
  lib/lp/soyuz/stories/ppa/xx-ppa-navigation.txt
  lib/lp/registry/stories/distribution/xx-distribution-packages.txt
  lib/lp/bugs/browser/cve.py
  lib/canonical/lazr/testing/menus.py
  lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt
  lib/lp/registry/browser/branding.py
  lib/canonical/launchpad/webapp/tales.py
  lib/canonical/launchpad/doc/navigation.txt
  lib/canonical/launchpad/doc/hierarchical-menu.txt
  lib/canonical/launchpad/webapp/tests/test_servers.py
  lib/lp/bugs/browser/tests/test_breadcrumbs.py
  lib/lp/soyuz/stories/soyuz/xx-buildfarm-index.txt

== Pylint notices ==

lib/canonical/launchpad/webapp/publication.py
    551: [E1002,
LaunchpadBrowserPublication.beginErrorHandlingTransaction] Use super on
an old style class

lib/lp/registry/browser/product.py
    57: [F0401] Unable to import 'lazr.delegates' (No module named
delegates)

lib/lp/bugs/browser/bugalsoaffects.py
    21: [F0401] Unable to import 'lazr.enum' (No module named enum)
    22: [F0401] Unable to import 'lazr.lifecycle.event' (No module named
lifecycle)

lib/canonical/launchpad/webapp/tales.py
    23: [F0401] Unable to import 'lazr.enum' (No module named enum)

Revision history for this message
Paul Hummer (rockstar) wrote :

As discussed in IRC, please convert the one XXX comment to a regular comment, and file a bug for the second XXX comment.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/browser/launchpad.py'
--- lib/canonical/launchpad/browser/launchpad.py 2009-09-16 19:56:45 +0000
+++ lib/canonical/launchpad/browser/launchpad.py 2009-09-17 14:29:22 +0000
@@ -103,6 +103,7 @@
103from canonical.launchpad.webapp.publisher import RedirectionView103from canonical.launchpad.webapp.publisher import RedirectionView
104from canonical.launchpad.webapp.authorization import check_permission104from canonical.launchpad.webapp.authorization import check_permission
105from lazr.uri import URI105from lazr.uri import URI
106from canonical.launchpad.webapp.tales import PageTemplateContextsAPI
106from canonical.launchpad.webapp.url import urlappend107from canonical.launchpad.webapp.url import urlappend
107from canonical.launchpad.webapp.vhosts import allvhosts108from canonical.launchpad.webapp.vhosts import allvhosts
108from canonical.widgets.project import ProjectScopeWidget109from canonical.widgets.project import ProjectScopeWidget
@@ -263,16 +264,27 @@
263 URL and the page's name (i.e. the last path segment of the URL).264 URL and the page's name (i.e. the last path segment of the URL).
264265
265 If the requested page (as specified in self.request) is the default266 If the requested page (as specified in self.request) is the default
266 one for the last traversed object, return None.267 one for our parent view's context, return None.
267 """268 """
268 url = self.request.getURL()269 url = self.request.getURL()
269 last_segment = URI(url).path.split('/')[-1]270 from zope.security.proxy import removeSecurityProxy
270 default_view_name = zapi.getDefaultViewName(271 view = removeSecurityProxy(self.request.traversed_objects[-1])
271 self.request.traversed_objects[-1], self.request)272 obj = self.request.traversed_objects[-2]
272 if last_segment.startswith('+') and last_segment != default_view_name:273 default_view_name = zapi.getDefaultViewName(obj, self.request)
274 if view.__name__ != default_view_name:
275 title = getattr(view, 'page_title', None)
276 if title is None:
277 template = getattr(view, 'template', None)
278 if template is None:
279 template = view.index
280 template_api = PageTemplateContextsAPI(
281 dict(context=obj, template=template, view=view))
282 title = template_api.pagetitle()
283 if len(title) > 30:
284 title = "%s..." % title[:30]
273 breadcrumb = Breadcrumb(None)285 breadcrumb = Breadcrumb(None)
274 breadcrumb._url = url286 breadcrumb._url = url
275 breadcrumb.text = last_segment287 breadcrumb.text = title
276 return breadcrumb288 return breadcrumb
277 else:289 else:
278 return None290 return None
279291
=== modified file 'lib/canonical/launchpad/browser/multistep.py'
--- lib/canonical/launchpad/browser/multistep.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/browser/multistep.py 2009-09-10 20:54:19 +0000
@@ -57,6 +57,14 @@
57 """57 """
58 raise NotImplementedError58 raise NotImplementedError
5959
60 # XXX: salgado, 2009-09-10: A page title should not be needed here, but
61 # our auto generated breadcrumbs will expect it to exist, so it must be
62 # defined in subclasses.
63 @property
64 def page_title(self):
65 """Must override in subclasses for breadcrumbs to work."""
66 raise NotImplementedError
67
60 def initialize(self):68 def initialize(self):
61 """Initialize the view and handle stepping through sub-views."""69 """Initialize the view and handle stepping through sub-views."""
62 view = self.first_step(self.context, self.request)70 view = self.first_step(self.context, self.request)
6371
=== modified file 'lib/canonical/launchpad/doc/hierarchical-menu.txt'
--- lib/canonical/launchpad/doc/hierarchical-menu.txt 2009-08-26 09:33:33 +0000
+++ lib/canonical/launchpad/doc/hierarchical-menu.txt 2009-09-17 15:14:02 +0000
@@ -82,6 +82,12 @@
82breadcrumbs starting with the breadcrumb closest to the hierarchy root.82breadcrumbs starting with the breadcrumb closest to the hierarchy root.
8383
84 >>> from canonical.launchpad.webapp.breadcrumb import Breadcrumb84 >>> from canonical.launchpad.webapp.breadcrumb import Breadcrumb
85 >>> from canonical.launchpad.browser.launchpad import Hierarchy
86 # Monkey patch Hierarchy.makeBreadcrumbForRequestedPage so that we don't
87 # have to create fake views and other stuff to test breadcrumbs here. The
88 # functionality provided by that method is tested in
89 # webapp/tests/test_breadcrumbs.py.
90 >>> Hierarchy.makeBreadcrumbForRequestedPage = lambda self: None
8591
86 # Note that the Hierarchy assigns the breadcrumb's URL, but we need to92 # Note that the Hierarchy assigns the breadcrumb's URL, but we need to
87 # give it a valid .text attribute.93 # give it a valid .text attribute.
@@ -171,7 +177,7 @@
171 text='Joy of cooking'>177 text='Joy of cooking'>
172178
173Breadcrumbs may have icons. The icon is only set for a breadcrumb if179Breadcrumbs may have icons. The icon is only set for a breadcrumb if
174the builder's context has an IPathAdapter registration.180the adapter's context has an IPathAdapter registration.
175181
176 >>> from zope.traversing.interfaces import IPathAdapter182 >>> from zope.traversing.interfaces import IPathAdapter
177 >>> from canonical.launchpad.webapp.tales import (183 >>> from canonical.launchpad.webapp.tales import (
@@ -220,7 +226,6 @@
220itself. It should let the IBreadcrumbBuilder handle it: this ensures226itself. It should let the IBreadcrumbBuilder handle it: this ensures
221consistency across the site.227consistency across the site.
222228
223 >>> from canonical.launchpad.browser.launchpad import Hierarchy
224 >>> class CustomHierarchy(Hierarchy):229 >>> class CustomHierarchy(Hierarchy):
225 ... @property230 ... @property
226 ... def objects(self):231 ... def objects(self):
227232
=== modified file 'lib/canonical/launchpad/doc/navigation.txt'
--- lib/canonical/launchpad/doc/navigation.txt 2009-06-11 01:28:55 +0000
+++ lib/canonical/launchpad/doc/navigation.txt 2009-09-17 15:16:46 +0000
@@ -635,11 +635,11 @@
635 >>> class DupeNames(Navigation):635 >>> class DupeNames(Navigation):
636 ...636 ...
637 ... @stepto('foo')637 ... @stepto('foo')
638 ... def doit(self):638 ... def doit_foo(self):
639 ... return 'foo'639 ... return 'foo'
640 ...640 ...
641 ... @stepto('bar')641 ... @stepto('bar')
642 ... def doit(self):642 ... def doit_bar(self):
643 ... return 'bar'643 ... return 'bar'
644644
645 >>> instance_of_dupenames = DupeNames(thingset, request)645 >>> instance_of_dupenames = DupeNames(thingset, request)
@@ -652,85 +652,3 @@
652652
653 >>> instance_of_dupenames.doit()653 >>> instance_of_dupenames.doit()
654 'bar'654 'bar'
655
656
657== Inspecting the traversed objects ==
658
659As the object heirarchy is traversed, a list of the traversed objects
660is built up in the request.
661
662Create a navigation class for IThing's:
663
664 >>> class ThingNavigation(Navigation):
665 ... usedfor = IThing
666 ...
667 ... @stepto('foo')
668 ... def traverse_foo(self):
669 ... return 'bar'
670 ...
671 ... @stepto('something')
672 ... def traverse_something(self):
673 ... return Thing('something')
674
675
676Traverse from the thingset to a particular thing:
677
678 >>> request = Request()
679 >>> navigation = ThingSetNavigation(thingset, request)
680 >>> thing = navigation.publishTraverse(request, 'ttt')
681 >>> navigation2 = ThingNavigation(thing, request)
682 >>> navigation2.publishTraverse(request, 'foo')
683 'bar'
684
685
686The traversed objects are available, in the order they were traversed:
687
688 >>> request.traversed_objects
689 [<ThingSet>, <Thing 'TTT'>]
690
691We can ask the request for the nearest traversed object that
692implements a particular interface:
693
694 >>> request.getNearest(IThingSet)
695 (<ThingSet>, <InterfaceClass __builtin__.IThingSet>)
696 >>> request.getNearest(IThing)
697 (<Thing 'TTT'>, <InterfaceClass __builtin__.IThing>)
698
699The second argument is the matched interface. This is useful when
700multiple interfaces are passed to getNearest():
701
702 >>> request.getNearest(IThingSet, IThing)
703 (<Thing 'TTT'>, <InterfaceClass __builtin__.IThing>)
704
705If more than one object of a particular interface type has been
706traversed, the most recently traversed one is returned:
707
708 >>> request = Request()
709 >>> navigation = ThingSetNavigation(thingset, request)
710 >>> thing = navigation.publishTraverse(request, 'ttt')
711 >>> navigation2 = ThingNavigation(thing, request)
712 >>> thing2 = navigation2.publishTraverse(request, 'something')
713 >>> navigation3 = ThingNavigation(thing2, request)
714 >>> navigation3.publishTraverse(request, 'foo')
715 'bar'
716
717 >>> request.traversed_objects
718 [<ThingSet>, <Thing 'TTT'>, <Thing 'something'>]
719 >>> request.getNearest(IThing)
720 (<Thing 'something'>, <InterfaceClass __builtin__.IThing>)
721
722If a particular interface has not been traversed, the tuple
723(None, None) is returned:
724
725 >>> request = Request()
726 >>> navigation = ThingSetNavigation(thingset, request)
727 >>> thing = navigation.publishTraverse(request, 'xyz')
728 Traceback (most recent call last):
729 ...
730 NotFound: ...ThingSet..., name: 'xyz'
731 >>> request.getNearest(IThing)
732 (None, None)
733
734Note that a tuple is returned in the error case so that the result can
735unpacked unconditionally, rather than needing to check for an error
736return before unpacking.
737655
=== modified file 'lib/canonical/launchpad/webapp/publication.py'
--- lib/canonical/launchpad/webapp/publication.py 2009-09-15 10:28:23 +0000
+++ lib/canonical/launchpad/webapp/publication.py 2009-09-17 14:29:22 +0000
@@ -428,6 +428,11 @@
428 def callTraversalHooks(self, request, ob):428 def callTraversalHooks(self, request, ob):
429 """ We don't want to call _maybePlacefullyAuthenticate as does429 """ We don't want to call _maybePlacefullyAuthenticate as does
430 zopepublication """430 zopepublication """
431 # In some cases we seem to be called more than once for a given
432 # traversed object, so we need to be careful here and only append an
433 # object the first time we see it.
434 if ob not in request.traversed_objects:
435 request.traversed_objects.append(ob)
431 notify(BeforeTraverseEvent(ob, request))436 notify(BeforeTraverseEvent(ob, request))
432437
433 def afterTraversal(self, request, ob):438 def afterTraversal(self, request, ob):
434439
=== modified file 'lib/canonical/launchpad/webapp/publisher.py'
--- lib/canonical/launchpad/webapp/publisher.py 2009-08-20 07:15:35 +0000
+++ lib/canonical/launchpad/webapp/publisher.py 2009-09-10 20:30:44 +0000
@@ -697,10 +697,6 @@
697 if self.newlayer is not None:697 if self.newlayer is not None:
698 setFirstLayer(request, self.newlayer)698 setFirstLayer(request, self.newlayer)
699699
700 # store the current context object in the request's
701 # traversed_objects list:
702 request.traversed_objects.append(self.context)
703
704 # Next, see if we're being asked to stepto somewhere.700 # Next, see if we're being asked to stepto somewhere.
705 stepto_traversals = self.stepto_traversals701 stepto_traversals = self.stepto_traversals
706 if stepto_traversals is not None:702 if stepto_traversals is not None:
707703
=== modified file 'lib/canonical/launchpad/webapp/tales.py'
--- lib/canonical/launchpad/webapp/tales.py 2009-09-14 14:48:51 +0000
+++ lib/canonical/launchpad/webapp/tales.py 2009-09-16 13:44:15 +0000
@@ -2233,7 +2233,6 @@
2233 name = name.replace('-', '_')2233 name = name.replace('-', '_')
2234 titleobj = getattr(canonical.launchpad.pagetitles, name, None)2234 titleobj = getattr(canonical.launchpad.pagetitles, name, None)
2235 if titleobj is None:2235 if titleobj is None:
2236 # sabdfl 25/0805 page titles are now mandatory hence the assert
2237 raise AssertionError(2236 raise AssertionError(
2238 "No page title in canonical.launchpad.pagetitles "2237 "No page title in canonical.launchpad.pagetitles "
2239 "for %s" % name)2238 "for %s" % name)
22402239
=== modified file 'lib/canonical/launchpad/webapp/tests/breadcrumbs.py'
--- lib/canonical/launchpad/webapp/tests/breadcrumbs.py 2009-08-27 20:00:05 +0000
+++ lib/canonical/launchpad/webapp/tests/breadcrumbs.py 2009-09-17 14:23:16 +0000
@@ -3,9 +3,11 @@
33
4__metaclass__ = type4__metaclass__ = type
55
6from zope.component import getMultiAdapter6from zope.app import zapi
7from zope.component import ComponentLookupError, getMultiAdapter
78
8from canonical.lazr.testing.menus import make_fake_request9from canonical.lazr.testing.menus import make_fake_request
10from canonical.launchpad.layers import setFirstLayer
9from canonical.launchpad.webapp.publisher import RootObject11from canonical.launchpad.webapp.publisher import RootObject
10from canonical.testing import DatabaseFunctionalLayer12from canonical.testing import DatabaseFunctionalLayer
11from lp.testing import TestCaseWithFactory13from lp.testing import TestCaseWithFactory
@@ -14,13 +16,14 @@
14class BaseBreadcrumbTestCase(TestCaseWithFactory):16class BaseBreadcrumbTestCase(TestCaseWithFactory):
1517
16 layer = DatabaseFunctionalLayer18 layer = DatabaseFunctionalLayer
19 request_layer = None
1720
18 def setUp(self):21 def setUp(self):
19 super(BaseBreadcrumbTestCase, self).setUp()22 super(BaseBreadcrumbTestCase, self).setUp()
20 self.root = RootObject()23 self.root = RootObject()
2124
22 def _getHierarchyView(self, url, traversed_objects):25 def _getHierarchyView(self, url, traversed_objects):
23 request = make_fake_request(url, traversed_objects)26 request = self._make_request(url, traversed_objects)
24 return getMultiAdapter((self.root, request), name='+hierarchy')27 return getMultiAdapter((self.root, request), name='+hierarchy')
2528
26 def _getBreadcrumbs(self, url, traversed_objects):29 def _getBreadcrumbs(self, url, traversed_objects):
@@ -28,10 +31,38 @@
28 return view.items31 return view.items
2932
30 def _getBreadcrumbsTexts(self, url, traversed_objects):33 def _getBreadcrumbsTexts(self, url, traversed_objects):
31 return [crumb.text34 crumbs = self._getBreadcrumbs(url, traversed_objects)
32 for crumb in self._getBreadcrumbs(url, traversed_objects)]35 return [crumb.text for crumb in crumbs]
3336
34 def _getBreadcrumbsURLs(self, url, traversed_objects):37 def _getBreadcrumbsURLs(self, url, traversed_objects):
35 return [crumb.url38 crumbs = self._getBreadcrumbs(url, traversed_objects)
36 for crumb in self._getBreadcrumbs(url, traversed_objects)]39 return [crumb.url for crumb in crumbs]
3740
41 def _make_request(self, url, traversed_objects):
42 """Create and return a LaunchpadTestRequest.
43
44 Set the given list of traversed objects as request.traversed_objects,
45 also appending the view that the given URL points to, to mimic how
46 request.traversed_objects behave in a real request.
47
48 XXX: salgado, 2009-09-17: Instead of setting request.traversed_objects
49 manually, we should duplicate parts of zope.publisher.publish.publish
50 here (or in make_fake_request()) so that tests don't have to specify
51 the list of traversed objects for us to set here.
52 """
53 request = make_fake_request(url, traversed_objects=traversed_objects)
54 if self.request_layer is not None:
55 setFirstLayer(request, self.request_layer)
56 last_segment = request._traversed_names[-1]
57 if traversed_objects:
58 obj = traversed_objects[-1]
59 # Assume the last_segment is the name of the view on the last
60 # traversed object, and if we fail to find a view with that name,
61 # use the default view.
62 try:
63 view = getMultiAdapter((obj, request), name=last_segment)
64 except ComponentLookupError:
65 default_view_name = zapi.getDefaultViewName(obj, request)
66 view = getMultiAdapter((obj, request), name=default_view_name)
67 request.traversed_objects.append(view)
68 return request
3869
=== modified file 'lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py'
--- lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 2009-08-31 17:40:56 +0000
+++ lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 2009-09-03 13:25:04 +0000
@@ -48,6 +48,25 @@
48 self.product = self.factory.makeProduct(name='crumb-tester')48 self.product = self.factory.makeProduct(name='crumb-tester')
49 self.product_url = canonical_url(self.product)49 self.product_url = canonical_url(self.product)
5050
51 def test_breadcrumb_text_for_page_with_short_title(self):
52 # When the page title is less than 30 characters long, we use the
53 # complete title as the breadcrumb's text.
54 downloads_url = "%s/+download" % self.product_url
55 texts = self._getBreadcrumbsTexts(
56 downloads_url, [self.root, self.product])
57 self.assertEquals(texts[-1],
58 '%s project files' % self.product.displayname)
59
60 def test_breadcrumb_text_for_page_with_long_title(self):
61 # When the page title is more than 30 characters long, we use only the
62 # first 30 as the breadcrumb's text.
63 downloads_url = "%s/+purchase-subscription" % self.product_url
64 texts = self._getBreadcrumbsTexts(
65 downloads_url, [self.root, self.product])
66 self.assertEquals(
67 texts[-1],
68 'Purchase Subscription for %s...' % self.product.displayname[:4])
69
51 def test_default_page(self):70 def test_default_page(self):
52 urls = self._getBreadcrumbsURLs(71 urls = self._getBreadcrumbsURLs(
53 self.product_url, [self.root, self.product])72 self.product_url, [self.root, self.product])
5473
=== modified file 'lib/canonical/launchpad/webapp/tests/test_publication.py'
--- lib/canonical/launchpad/webapp/tests/test_publication.py 2009-09-15 10:28:23 +0000
+++ lib/canonical/launchpad/webapp/tests/test_publication.py 2009-09-17 14:29:22 +0000
@@ -10,12 +10,10 @@
1010
11from contrib.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT11from contrib.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT
1212
13from psycopg2.extensions import TransactionRollbackError
14from storm.exceptions import DisconnectionError13from storm.exceptions import DisconnectionError
15from zope.component import getUtility14from zope.component import getUtility
16from zope.error.interfaces import IErrorReportingUtility15from zope.error.interfaces import IErrorReportingUtility
17from zope.publisher.interfaces import Retry16from zope.publisher.interfaces import Retry
18from zope.security.management import endInteraction
1917
20from canonical.testing import DatabaseFunctionalLayer18from canonical.testing import DatabaseFunctionalLayer
21from canonical.launchpad.interfaces.oauth import IOAuthConsumerSet19from canonical.launchpad.interfaces.oauth import IOAuthConsumerSet
@@ -23,11 +21,37 @@
23from lp.testing import TestCase, TestCaseWithFactory21from lp.testing import TestCase, TestCaseWithFactory
24import canonical.launchpad.webapp.adapter as da22import canonical.launchpad.webapp.adapter as da
25from canonical.launchpad.webapp.interfaces import OAuthPermission23from canonical.launchpad.webapp.interfaces import OAuthPermission
26from canonical.launchpad.webapp.publication import is_browser24from canonical.launchpad.webapp.publication import (
25 is_browser, LaunchpadBrowserPublication)
27from canonical.launchpad.webapp.servers import (26from canonical.launchpad.webapp.servers import (
28 LaunchpadTestRequest, WebServicePublication)27 LaunchpadTestRequest, WebServicePublication)
2928
3029
30class TestLaunchpadBrowserPublication(TestCase):
31
32 def test_callTraversalHooks_appends_to_traversed_objects(self):
33 # Traversed objects are appended to request.traversed_objects in the
34 # order they're traversed.
35 obj1 = object()
36 obj2 = object()
37 request = LaunchpadTestRequest()
38 publication = LaunchpadBrowserPublication(None)
39 publication.callTraversalHooks(request, obj1)
40 publication.callTraversalHooks(request, obj2)
41 self.assertEquals(request.traversed_objects, [obj1, obj2])
42
43 def test_callTraversalHooks_appends_only_once_to_traversed_objects(self):
44 # callTraversalHooks() may be called more than once for a given
45 # traversed object, but if that's the case we won't add the same
46 # object twice to traversed_objects.
47 obj1 = obj2 = object()
48 request = LaunchpadTestRequest()
49 publication = LaunchpadBrowserPublication(None)
50 publication.callTraversalHooks(request, obj1)
51 publication.callTraversalHooks(request, obj2)
52 self.assertEquals(request.traversed_objects, [obj1])
53
54
31class TestWebServicePublication(TestCaseWithFactory):55class TestWebServicePublication(TestCaseWithFactory):
32 layer = DatabaseFunctionalLayer56 layer = DatabaseFunctionalLayer
3357
3458
=== modified file 'lib/canonical/launchpad/webapp/tests/test_servers.py'
--- lib/canonical/launchpad/webapp/tests/test_servers.py 2009-07-27 14:08:17 +0000
+++ lib/canonical/launchpad/webapp/tests/test_servers.py 2009-09-17 14:23:16 +0000
@@ -8,6 +8,9 @@
88
9from zope.publisher.base import DefaultPublication9from zope.publisher.base import DefaultPublication
10from zope.testing.doctest import DocTestSuite, NORMALIZE_WHITESPACE, ELLIPSIS10from zope.testing.doctest import DocTestSuite, NORMALIZE_WHITESPACE, ELLIPSIS
11from zope.interface import implements, Interface
12
13from lp.testing import TestCase
1114
12from canonical.launchpad.webapp.servers import (15from canonical.launchpad.webapp.servers import (
13 AnswersBrowserRequest, ApplicationServerSettingRequestFactory,16 AnswersBrowserRequest, ApplicationServerSettingRequestFactory,
@@ -15,10 +18,10 @@
15 TranslationsBrowserRequest, VHostWebServiceRequestPublicationFactory,18 TranslationsBrowserRequest, VHostWebServiceRequestPublicationFactory,
16 VirtualHostRequestPublicationFactory, WebServiceRequestPublicationFactory,19 VirtualHostRequestPublicationFactory, WebServiceRequestPublicationFactory,
17 WebServiceClientRequest, WebServicePublication, WebServiceTestRequest)20 WebServiceClientRequest, WebServicePublication, WebServiceTestRequest)
18
19from canonical.launchpad.webapp.tests import DummyConfigurationTestCase21from canonical.launchpad.webapp.tests import DummyConfigurationTestCase
2022
21class SetInWSGIEnvironmentTestCase(unittest.TestCase):23
24class SetInWSGIEnvironmentTestCase(TestCase):
2225
23 def test_set(self):26 def test_set(self):
24 # Test that setInWSGIEnvironment() can set keys in the WSGI27 # Test that setInWSGIEnvironment() can set keys in the WSGI
@@ -61,7 +64,7 @@
61 self.assertEqual(new_request._orig_env['key'], 'second value')64 self.assertEqual(new_request._orig_env['key'], 'second value')
6265
6366
64class TestApplicationServerSettingRequestFactory(unittest.TestCase):67class TestApplicationServerSettingRequestFactory(TestCase):
65 """Tests for the ApplicationServerSettingRequestFactory."""68 """Tests for the ApplicationServerSettingRequestFactory."""
6669
67 def test___call___should_set_HTTPS_env_on(self):70 def test___call___should_set_HTTPS_env_on(self):
@@ -274,7 +277,7 @@
274 "request traversal stack: %r" % stack)277 "request traversal stack: %r" % stack)
275278
276279
277class TestWebServiceRequest(unittest.TestCase):280class TestWebServiceRequest(TestCase):
278281
279 def test_application_url(self):282 def test_application_url(self):
280 """Requests to the /api path should return the original request's283 """Requests to the /api path should return the original request's
@@ -300,7 +303,7 @@
300 'Cookie, Authorization, Accept')303 'Cookie, Authorization, Accept')
301304
302305
303class TestBasicLaunchpadRequest(unittest.TestCase):306class TestBasicLaunchpadRequest(TestCase):
304 """Tests for the base request class"""307 """Tests for the base request class"""
305308
306 def test_baserequest_response_should_vary(self):309 def test_baserequest_response_should_vary(self):
@@ -318,7 +321,56 @@
318 'Cookie, Authorization')321 'Cookie, Authorization')
319322
320323
321class TestAnswersBrowserRequest(unittest.TestCase):324class IThingSet(Interface):
325 """Marker interface for a set of things."""
326
327
328class IThing(Interface):
329 """Marker interface for a thing."""
330
331
332class Thing:
333 implements(IThing)
334
335
336class ThingSet:
337 implements(IThingSet)
338
339
340class TestLaunchpadBrowserRequest_getNearest(TestCase):
341
342 def setUp(self):
343 super(TestLaunchpadBrowserRequest_getNearest, self).setUp()
344 self.request = LaunchpadBrowserRequest('', {})
345 self.thing_set = ThingSet()
346 self.thing = Thing()
347
348 def test_return_value(self):
349 # .getNearest() returns a two-tuple with the object and the interface
350 # that matched. The second item in the tuple is useful when multiple
351 # interfaces are passed to getNearest().
352 request = self.request
353 request.traversed_objects.extend([self.thing_set, self.thing])
354 self.assertEquals(request.getNearest(IThing), (self.thing, IThing))
355 self.assertEquals(
356 request.getNearest(IThingSet), (self.thing_set, IThingSet))
357
358 def test_multiple_traversed_objects_with_common_interface(self):
359 # If more than one object of a particular interface type has been
360 # traversed, the most recently traversed one is returned.
361 thing2 = Thing()
362 self.request.traversed_objects.extend(
363 [self.thing_set, self.thing, thing2])
364 self.assertEquals(self.request.getNearest(IThing), (thing2, IThing))
365
366 def test_interface_not_traversed(self):
367 # If a particular interface has not been traversed, the tuple
368 # (None, None) is returned.
369 self.request.traversed_objects.extend([self.thing_set])
370 self.assertEquals(self.request.getNearest(IThing), (None, None))
371
372
373class TestAnswersBrowserRequest(TestCase):
322 """Tests for the Answers request class."""374 """Tests for the Answers request class."""
323375
324 def test_response_should_vary_based_on_language(self):376 def test_response_should_vary_based_on_language(self):
@@ -328,7 +380,7 @@
328 'Cookie, Authorization, Accept-Language')380 'Cookie, Authorization, Accept-Language')
329381
330382
331class TestTranslationsBrowserRequest(unittest.TestCase):383class TestTranslationsBrowserRequest(TestCase):
332 """Tests for the Translations request class."""384 """Tests for the Translations request class."""
333385
334 def test_response_should_vary_based_on_language(self):386 def test_response_should_vary_based_on_language(self):
@@ -338,7 +390,7 @@
338 'Cookie, Authorization, Accept-Language')390 'Cookie, Authorization, Accept-Language')
339391
340392
341class TestLaunchpadBrowserRequest(unittest.TestCase):393class TestLaunchpadBrowserRequest(TestCase):
342394
343 def prepareRequest(self, form):395 def prepareRequest(self, form):
344 """Return a `LaunchpadBrowserRequest` with the given form.396 """Return a `LaunchpadBrowserRequest` with the given form.
345397
=== modified file 'lib/canonical/lazr/testing/menus.py'
--- lib/canonical/lazr/testing/menus.py 2009-08-18 12:21:51 +0000
+++ lib/canonical/lazr/testing/menus.py 2009-09-17 14:23:16 +0000
@@ -61,7 +61,7 @@
61 PATH_INFO=path_info)61 PATH_INFO=path_info)
62 request._traversed_names = path_info.split('/')[1:]62 request._traversed_names = path_info.split('/')[1:]
63 if traversed_objects is not None:63 if traversed_objects is not None:
64 request.traversed_objects = traversed_objects64 request.traversed_objects = traversed_objects[:]
65 # After making the request, setup a new interaction.65 # After making the request, setup a new interaction.
66 endInteraction()66 endInteraction()
67 newInteraction(request)67 newInteraction(request)
6868
=== modified file 'lib/lp/bugs/browser/bugalsoaffects.py'
--- lib/lp/bugs/browser/bugalsoaffects.py 2009-07-30 00:42:18 +0000
+++ lib/lp/bugs/browser/bugalsoaffects.py 2009-09-09 14:32:45 +0000
@@ -53,12 +53,16 @@
5353
5454
55class BugAlsoAffectsProductMetaView(MultiStepView):55class BugAlsoAffectsProductMetaView(MultiStepView):
56 page_title = 'Record as affecting another project'
57
56 @property58 @property
57 def first_step(self):59 def first_step(self):
58 return ChooseProductStep60 return ChooseProductStep
5961
6062
61class BugAlsoAffectsDistroMetaView(MultiStepView):63class BugAlsoAffectsDistroMetaView(MultiStepView):
64 page_title = 'Record as affecting another distribution/package'
65
62 @property66 @property
63 def first_step(self):67 def first_step(self):
64 return DistroBugTaskCreationStep68 return DistroBugTaskCreationStep
6569
=== modified file 'lib/lp/bugs/browser/cve.py'
--- lib/lp/bugs/browser/cve.py 2009-09-07 19:43:17 +0000
+++ lib/lp/bugs/browser/cve.py 2009-09-16 13:44:15 +0000
@@ -105,6 +105,7 @@
105 self.request.response.addInfoNotification(105 self.request.response.addInfoNotification(
106 'CVE-%s removed.' % data['sequence'])106 'CVE-%s removed.' % data['sequence'])
107107
108 @property
108 def label(self):109 def label(self):
109 return 'Bug # %s Remove link to CVE report' % self.context.bug.id110 return 'Bug # %s Remove link to CVE report' % self.context.bug.id
110111
111112
=== modified file 'lib/lp/bugs/browser/tests/test_breadcrumbs.py'
--- lib/lp/bugs/browser/tests/test_breadcrumbs.py 2009-09-09 13:06:32 +0000
+++ lib/lp/bugs/browser/tests/test_breadcrumbs.py 2009-09-10 18:23:44 +0000
@@ -41,7 +41,6 @@
41 self.assertEquals(urls[-1], "%s/+activity" % self.bugtask_url)41 self.assertEquals(urls[-1], "%s/+activity" % self.bugtask_url)
42 self.assertEquals(urls[-2], self.bugtask_url)42 self.assertEquals(urls[-2], self.bugtask_url)
43 texts = self._getBreadcrumbsTexts(url, self.traversed_objects)43 texts = self._getBreadcrumbsTexts(url, self.traversed_objects)
44 self.assertEquals(texts[-1], "+activity")
45 self.assertEquals(texts[-2], "Bug #%d" % self.bug.id)44 self.assertEquals(texts[-2], "Bug #%d" % self.bug.id)
4645
47 def test_bugtask_private_bug(self):46 def test_bugtask_private_bug(self):
4847
=== modified file 'lib/lp/registry/browser/branding.py'
--- lib/lp/registry/browser/branding.py 2009-08-26 16:34:38 +0000
+++ lib/lp/registry/browser/branding.py 2009-09-03 13:25:04 +0000
@@ -23,6 +23,7 @@
23 (some subset of icon, logo, mugshot).23 (some subset of icon, logo, mugshot).
24 """24 """
2525
26 @property
26 def label(self):27 def label(self):
27 return ('Change the images used to represent %s in Launchpad'28 return ('Change the images used to represent %s in Launchpad'
28 % self.context.displayname)29 % self.context.displayname)
2930
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2009-09-15 20:41:36 +0000
+++ lib/lp/registry/browser/product.py 2009-09-17 15:16:46 +0000
@@ -1425,7 +1425,7 @@
1425 """View for searching products to be reviewed."""1425 """View for searching products to be reviewed."""
14261426
1427 schema = IProductReviewSearch1427 schema = IProductReviewSearch
1428 label= 'Review projects'1428 label = 'Review projects'
14291429
1430 full_row_field_names = [1430 full_row_field_names = [
1431 'search_text',1431 'search_text',
@@ -1568,7 +1568,7 @@
1568 schema = IProduct1568 schema = IProduct
1569 step_name = 'projectaddstep2'1569 step_name = 'projectaddstep2'
1570 template = ViewPageTemplateFile('../templates/product-new.pt')1570 template = ViewPageTemplateFile('../templates/product-new.pt')
1571 page_title = "Register a project in Launchpad"1571 page_title = ProjectAddStepOne.page_title
15721572
1573 product = None1573 product = None
15741574
@@ -1690,6 +1690,7 @@
1690class ProductAddView(MultiStepView):1690class ProductAddView(MultiStepView):
1691 """The controlling view for product/+new."""1691 """The controlling view for product/+new."""
16921692
1693 page_title = ProjectAddStepOne.page_title
1693 total_steps = 21694 total_steps = 2
16941695
1695 @property1696 @property
16961697
=== modified file 'lib/lp/registry/stories/distribution/xx-distribution-packages.txt'
--- lib/lp/registry/stories/distribution/xx-distribution-packages.txt 2009-09-12 01:57:22 +0000
+++ lib/lp/registry/stories/distribution/xx-distribution-packages.txt 2009-09-16 13:44:15 +0000
@@ -70,7 +70,7 @@
70 >>> field.value = 'commercialpackage'70 >>> field.value = 'commercialpackage'
71 >>> browser.getControl('Search', index=0).click()71 >>> browser.getControl('Search', index=0).click()
72 >>> extract_text(find_main_content(browser.contents))72 >>> extract_text(find_main_content(browser.contents))
73 u'...commercialpackage... package...'73 u"...commercialpackage... package..."
7474
75Now try searching for text that we know to be in a change log entry, to75Now try searching for text that we know to be in a change log entry, to
76prove that FTI works on change logs. The text we're looking for is76prove that FTI works on change logs. The text we're looking for is
7777
=== modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-navigation.txt'
--- lib/lp/soyuz/stories/ppa/xx-ppa-navigation.txt 2009-09-12 02:09:47 +0000
+++ lib/lp/soyuz/stories/ppa/xx-ppa-navigation.txt 2009-09-17 15:14:02 +0000
@@ -96,14 +96,14 @@
9696
97 >>> anon_browser.getLink('View package details').click()97 >>> anon_browser.getLink('View package details').click()
98 >>> print anon_browser.title98 >>> print anon_browser.title
99 +packages : Default PPA : Celso Providelo99 Packages in...
100100
101You can see the build details of the packages in the archive by using101You can see the build details of the packages in the archive by using
102the 'View all builds' link.102the 'View all builds' link.
103103
104 >>> anon_browser.getLink('View all builds').click()104 >>> anon_browser.getLink('View all builds').click()
105 >>> print anon_browser.title105 >>> print anon_browser.title
106 +builds : Default PPA : Celso Providelo106 Builds for...
107107
108 >>> anon_browser.url108 >>> anon_browser.url
109 'http://launchpad.dev/~cprov/+archive/ppa/+builds'109 'http://launchpad.dev/~cprov/+archive/ppa/+builds'
110110
=== modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt'
--- lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt 2009-09-15 15:28:47 +0000
+++ lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt 2009-09-17 15:14:02 +0000
@@ -10,7 +10,7 @@
10 http://launchpad.dev/~cprov/+archive/ppa/+packages10 http://launchpad.dev/~cprov/+archive/ppa/+packages
1111
12 >>> print anon_browser.title12 >>> print anon_browser.title
13 +packages : PPA for Celso Providelo : Celso Providelo13 Packages in...
1414
1515
16Page structure16Page structure
@@ -20,7 +20,7 @@
20a descriptive main heading.20a descriptive main heading.
2121
22 >>> print_location(anon_browser.contents)22 >>> print_location(anon_browser.contents)
23 Hierarchy: Celso Providelo > PPA for Celso Providelo > +packages23 Hierarchy: Celso Providelo > PPA for Celso Providelo > Packages in...
24 Tabs:24 Tabs:
25 * Overview (selected) - http://launchpad.dev/~cprov25 * Overview (selected) - http://launchpad.dev/~cprov
26 * Code - http://code.launchpad.dev/~cprov26 * Code - http://code.launchpad.dev/~cprov
@@ -35,7 +35,7 @@
3535
36 >>> anon_browser.getLink('View all builds').click()36 >>> anon_browser.getLink('View all builds').click()
37 >>> print anon_browser.title37 >>> print anon_browser.title
38 +builds : PPA for Celso Providelo : Celso Providelo38 Builds for...
3939
40 >>> print anon_browser.url40 >>> print anon_browser.url
41 http://launchpad.dev/~cprov/+archive/ppa/+builds41 http://launchpad.dev/~cprov/+archive/ppa/+builds
4242
=== modified file 'lib/lp/soyuz/stories/soyuz/xx-builder-page.txt'
--- lib/lp/soyuz/stories/soyuz/xx-builder-page.txt 2009-09-12 06:49:56 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-builder-page.txt 2009-09-17 15:14:02 +0000
@@ -114,7 +114,7 @@
114 # We use backslashreplace because the page title includes smart quotes.114 # We use backslashreplace because the page title includes smart quotes.
115 >>> from canonical.launchpad.helpers import backslashreplace115 >>> from canonical.launchpad.helpers import backslashreplace
116 >>> print backslashreplace(cprov_browser.title)116 >>> print backslashreplace(cprov_browser.title)
117 +edit : Bob The Builder : Build Farm117 Change details for...
118118
119 >>> title = cprov_browser.getControl(name="field.title")119 >>> title = cprov_browser.getControl(name="field.title")
120 >>> original_title = title.value120 >>> original_title = title.value
121121
=== modified file 'lib/lp/soyuz/stories/soyuz/xx-buildfarm-index.txt'
--- lib/lp/soyuz/stories/soyuz/xx-buildfarm-index.txt 2009-09-10 22:08:36 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-buildfarm-index.txt 2009-09-17 15:14:02 +0000
@@ -115,7 +115,7 @@
115 >>> admin_browser.getLink("Register a new build machine").click()115 >>> admin_browser.getLink("Register a new build machine").click()
116116
117 >>> print admin_browser.title117 >>> print admin_browser.title
118 +new : Build Farm118 Register a new...
119119
120Registering a new builder involves setting its name, title,120Registering a new builder involves setting its name, title,
121description and corresponding location.121description and corresponding location.
122122
=== modified file 'lib/lp/translations/browser/pofile.py'
--- lib/lp/translations/browser/pofile.py 2009-09-17 08:41:05 +0000
+++ lib/lp/translations/browser/pofile.py 2009-09-17 14:29:22 +0000
@@ -509,6 +509,7 @@
509 else:509 else:
510 return self.person.displayname510 return self.person.displayname
511511
512 @property
512 def page_title(self):513 def page_title(self):
513 """See `LaunchpadView`."""514 """See `LaunchpadView`."""
514 return smartquote('Translations by %s in "%s"') % (515 return smartquote('Translations by %s in "%s"') % (
515516
=== modified file 'lib/lp/translations/browser/tests/test_breadcrumbs.py'
--- lib/lp/translations/browser/tests/test_breadcrumbs.py 2009-09-17 10:22:14 +0000
+++ lib/lp/translations/browser/tests/test_breadcrumbs.py 2009-09-17 14:29:22 +0000
@@ -7,6 +7,7 @@
77
8from canonical.lazr.utils import smartquote8from canonical.lazr.utils import smartquote
99
10from canonical.launchpad.layers import TranslationsLayer
10from canonical.launchpad.webapp.publisher import canonical_url11from canonical.launchpad.webapp.publisher import canonical_url
11from canonical.launchpad.webapp.tests.breadcrumbs import (12from canonical.launchpad.webapp.tests.breadcrumbs import (
12 BaseBreadcrumbTestCase)13 BaseBreadcrumbTestCase)
@@ -18,7 +19,10 @@
18 IProductSeriesLanguageSet)19 IProductSeriesLanguageSet)
19from lp.translations.interfaces.translationgroup import ITranslationGroupSet20from lp.translations.interfaces.translationgroup import ITranslationGroupSet
2021
22
21class BaseTranslationsBreadcrumbTestCase(BaseBreadcrumbTestCase):23class BaseTranslationsBreadcrumbTestCase(BaseBreadcrumbTestCase):
24 request_layer = TranslationsLayer
25
22 def setUp(self):26 def setUp(self):
23 super(BaseTranslationsBreadcrumbTestCase, self).setUp()27 super(BaseTranslationsBreadcrumbTestCase, self).setUp()
24 self.traversed_objects = [self.root]28 self.traversed_objects = [self.root]
@@ -103,12 +107,10 @@
103 def test_translationgroupset(self):107 def test_translationgroupset(self):
104 group_set = getUtility(ITranslationGroupSet)108 group_set = getUtility(ITranslationGroupSet)
105 url = canonical_url(group_set, rootsite='translations')109 url = canonical_url(group_set, rootsite='translations')
106 # Translation group listing is top-level, so no breadcrumbs show up.
107 # Note that the first parameter is an empty list because
108 # ITranslationGroupSet doesn't register Navigation class, and
109 # thus doesn't show up in request.traversed_objects.
110 self._testContextBreadcrumbs(110 self._testContextBreadcrumbs(
111 [], [], [],111 [group_set],
112 ['http://translations.launchpad.dev/+groups'],
113 ['Translation groups'],
112 url=url)114 url=url)
113115
114 def test_translationgroup(self):116 def test_translationgroup(self):
115117