Merge lp:~salgado/launchpad/real-breadcrumbs-2 into lp:launchpad

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

= Summary =

In 3.0 we'll have real breadcrumbs, and this branch will make it
possible to construct them.

== Proposed fix ==

Basically, what this branch does is have the URL of all breadcrumbs
default to the object's canonical_url on the mainsite, as well as
sneaking an extra breadcrumb item for the object on the vhost we are on
(in case we are on a vhost other than the mainsite).

E.g. the breadcrumbs for bugs.lp.net/ubuntu/+bug/1 will be
  Ubuntu > Bugs on Ubuntu > Bug #1

This is accomplished by looking up IBreadcrumbBuilder adapters with
name=vhost for the traversed objects, starting from the end. When one is
found, we use it to generate the extra breadcrumb and insert the
breadcrumb right after the current one.

For some types of objects we will not want the URL on their breadcrumbs
to point to the mainsite, so they can just specify the rootsite they
want in their custom IBreadcrumbBuilder adapter.

== Pre-implementation notes ==

== Implementation details ==

In lib/canonical/launchpad/webapp/publisher.py I reorganized some code
slightly to make it more readable.

The XXX I left in lib/canonical/launchpad/webapp/breadcrumb.py I'm
planning to fix as soon as I get this through review. I didn't do it
here as it would pollute the diff.

== Tests ==

./bin/test -vvt test_breadcrumbs -t hierarchical-menu.txt

== Demo and Q/A ==

https://bugs.launchpad.dev/firefox/+bug/1

= Launchpad lint =

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

Linting changed files:
  lib/lp/code/browser/branch.py
  lib/canonical/launchpad/webapp/publisher.py
  lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py
  lib/lp/registry/configure.zcml
  lib/canonical/launchpad/browser/launchpad.py
  lib/canonical/launchpad/webapp/breadcrumb.py
  lib/lp/registry/browser/pillar.py
  lib/canonical/lazr/testing/menus.py
  lib/canonical/launchpad/doc/hierarchical-menu.txt

== Pylint notices ==

lib/lp/code/browser/branch.py
    48: [F0401] Unable to import 'lazr.uri' (No module named uri)

lib/canonical/launchpad/browser/launchpad.py
    103: [F0401] Unable to import 'lazr.uri' (No module named uri)

--
Guilherme Salgado <email address hidden>

Revision history for this message
Brad Crittenden (bac) wrote :
Download full text (23.0 KiB)

Hi Salgado,

This is a very cool branch and I enjoyed looking at it. I've made a
few comments below. The things to fix are straightforward so I'm
marking it Approved but it's really merge-conditional.

I look forward to having these bread crumbs fully implemented!

> === modified file 'lib/canonical/launchpad/browser/launchpad.py'
> --- lib/canonical/launchpad/browser/launchpad.py 2009-08-11 04:35:26 +0000
+++ lib/canonical/launchpad/browser/launchpad.py 2009-08-18 14:36:16 +0000
> @@ -33,7 +33,6 @@
> import time
> import urllib
> from datetime import timedelta, datetime
> -from urlparse import urlunsplit
>
> from zope.datetime import parseDatetimetz, tzinfo, DateTimeError
> from zope.component import getUtility, queryAdapter
> @@ -102,7 +101,7 @@
> from canonical.launchpad.webapp.publisher import RedirectionView
> from canonical.launchpad.webapp.authorization import check_permission
> from lazr.uri import URI
> -from canonical.launchpad.webapp.url import urlparse, urlappend
> +from canonical.launchpad.webapp.url import urlappend
> from canonical.launchpad.webapp.vhosts import allvhosts
> from canonical.widgets.project import ProjectScopeWidget
>
> @@ -216,58 +215,50 @@
> class Hierarchy(LaunchpadView):
> """The hierarchy part of the location bar on each page."""
>
> + @property
> + def objects(self):
> + """The objects for which we want breadcrumbs."""
> + return self.request.traversed_objects
> +
> def items(self):
> """Return a list of `IBreadcrumb` objects visible in the hierarchy.
>
> The list starts with the breadcrumb closest to the hierarchy root.
> """
> - urlparts = urlparse(self.request.getURL(0, path_only=False))
> - baseurl = urlunsplit((urlparts[0], urlparts[1], '', '', ''))
> -
> - # Construct a list of complete URLs for each URL path segment.
> - pathurls = []
> - working_url = baseurl
> - for segment in urlparts[2].split('/'):
> - working_url = urlappend(working_url, segment)
> - # Segments starting with '+' should be ignored because they
> - # will never correspond to an object in navigation.
> - if segment.startswith('+'):
> - continue
> - pathurls.append(working_url)
> -
> - # We assume a 1:1 relationship between the traversed_objects list and
> - # the URL path segments. Note that there may be more segments than
> - # there are objects.
> - object_urls = zip(self.request.traversed_objects, pathurls)
> - return self._breadcrumbs(object_urls)
> -
> - def _breadcrumbs(self, object_urls):
> - """Generate the breadcrumb list.
> -
> - :param object_urls: A sequence of (object, url) pairs.
> - :return: A list of 'IBreadcrumb' objects.
> - """
> breadcrumbs = []
> - for obj, url in object_urls:
> - crumb = self.breadcrumb_for(obj, url)
> + for builder in self._getBreadcrumbBuilders():
> + crumb = builder.make_breadcrumb()
> if crumb is not None:
> breadcrumbs.append(crumb)
> return breadcrumbs...

review: Approve
Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (6.4 KiB)

On Tue, 2009-08-18 at 20:19 +0000, Brad Crittenden wrote:
> Review: Approve
> Hi Salgado,
>
> This is a very cool branch and I enjoyed looking at it. I've made a
> few comments below. The things to fix are straightforward so I'm
> marking it Approved but it's really merge-conditional.

Thanks, Brad.

>
> I look forward to having these bread crumbs fully implemented!

So do I. I think they'll be a significant improvement to LP.

> > === modified file 'lib/canonical/launchpad/webapp/publisher.py'
> > --- lib/canonical/launchpad/webapp/publisher.py 2009-07-30 08:07:21 +0000
> > +++ lib/canonical/launchpad/webapp/publisher.py 2009-08-12 20:22:21 +0000
> > @@ -504,10 +504,10 @@
> > 'step for "%s".' % (view_name, obj.__class__.__name__))
> > urlparts.insert(0, view_name)
> >
> > - if request is None and rootsite is not None:
> > + if request is None:
> > + if rootsite is None:
>
> I see rootsite is set to ICanonicalUrlDate(obj).rootsite if it is
> passed as None and it also may be None, which is the only way you'd
> get here. That's all correct?

Yep; I just re-arranged the code here to make it more readable.

>
> > + rootsite = 'mainsite'
> > root_url = allvhosts.configs[rootsite].rooturl
> > - elif request is None:
> > - root_url = allvhosts.configs['mainsite'].rooturl
> > else:
> > root_url = request.getRootURL(rootsite)
> >
>

> > === added file 'lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py'
> > --- lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 1970-01-01 00:00:00 +0000
> > +++ lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 2009-08-18 14:36:16 +0000
> > @@ -0,0 +1,81 @@
> > +# Copyright 2009 Canonical Ltd. This software is licensed under the
> > +# GNU Affero General Public License version 3 (see the file LICENSE).
> > +
> > +__metaclass__ = type
> > +
> > +import unittest
> > +
> > +from canonical.launchpad.webapp.publisher import canonical_url
> > +from canonical.launchpad.webapp.tests import BaseBreadcrumbTestCase
> > +from lp.testing import login
> > +
> > +
> > +class TestExtraVHostBreadcrumbsOnHierarchyView(BaseBreadcrumbTestCase):
> > + """How our breadcrumbs behave when using a vhost other the main one?
>
> typo: other than the

Fixed

>
> > + When we go to bugs.lp.net/ubuntu, we only traversed the Ubuntu distro, so
> > + that's what we'd have a breadcrumb for, but we also want to generate a
> > + breadcrumb for bugs on Ubuntu, given that we're on the bugs vhost.
> > +
> > + The behaviour is similar to other vhosts; read on for more.
> > + """
> > +
> > + def setUp(self):
> > + super(TestExtraVHostBreadcrumbsOnHierarchyView, self).setUp()
> > + login('<email address hidden>')
> > + self.product = self.factory.makeProduct(name='crumb-tester')
> > + self.product_bug = self.factory.makeBug(product=self.product)
> > + self.product_bug_url = canonical_url(self.product_bug)
> > + self.source_package = self.factory.makeSourcePackage()
> > + self.package_bug = self.factory.makeBugTask(
> > + target=self.source_package)
> > + s...

Read more...

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (4.3 KiB)

Also, after reading Gavin's tests for the new bugtask breadcrumbs again, I realized I was not testing some things properly; I was testing using bug URLs when in fact I want to test with bugtask URLs as that's what happens on the web UI. The diff below changes that

=== modified file 'lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py'
--- lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 2009-08-18 14:36:16 +0000
+++ lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 2009-08-19 12:35:17 +0000
@@ -11,7 +11,7 @@

 class TestExtraVHostBreadcrumbsOnHierarchyView(BaseBreadcrumbTestCase):
- """How our breadcrumbs behave when using a vhost other the main one?
+ """How our breadcrumbs behave when using a vhost other than the main one?

     When we go to bugs.lp.net/ubuntu, we only traversed the Ubuntu distro, so
     that's what we'd have a breadcrumb for, but we also want to generate a
@@ -24,12 +24,15 @@
         super(TestExtraVHostBreadcrumbsOnHierarchyView, self).setUp()
         login('<email address hidden>')
         self.product = self.factory.makeProduct(name='crumb-tester')
- self.product_bug = self.factory.makeBug(product=self.product)
- self.product_bug_url = canonical_url(self.product_bug)
+ self.product_url = canonical_url(self.product)
+ self.product_bugs_url = canonical_url(self.product, rootsite='bugs')
+ product_bug = self.factory.makeBug(product=self.product)
+ self.product_bugtask = product_bug.default_bugtask
+ self.product_bugtask_url = canonical_url(self.product_bugtask)
         self.source_package = self.factory.makeSourcePackage()
- self.package_bug = self.factory.makeBugTask(
+ self.package_bugtask = self.factory.makeBugTask(
             target=self.source_package)
- self.package_bug_url = canonical_url(self.package_bug)
+ self.package_bugtask_url = canonical_url(self.package_bugtask)

     def test_root_on_mainsite(self):
         urls = self._getBreadcrumbsURLs('http://launchpad.dev/', [self.root])
@@ -37,9 +40,8 @@

     def test_product_on_mainsite(self):
         urls = self._getBreadcrumbsURLs(
- 'http://launchpad.dev/%s' % self.product.name,
- [self.root, self.product])
- self.assertEquals(urls, ['http://launchpad.dev/crumb-tester'])
+ self.product_url, [self.root, self.product])
+ self.assertEquals(urls, [self.product_url])

     def test_root_on_vhost(self):
         urls = self._getBreadcrumbsURLs(
@@ -48,33 +50,31 @@

     def test_product_on_vhost(self):
         urls = self._getBreadcrumbsURLs(
- 'http://bugs.launchpad.dev/%s' % self.product.name,
- [self.root, self.product])
- self.assertEquals(
- urls, ['http://launchpad.dev/crumb-tester',
- 'http://bugs.launchpad.dev/crumb-tester'])
+ self.product_bugs_url, [self.root, self.product])
+ self.assertEquals(urls, [self.product_url, self.product_bugs_url])

- def test_product_bug(self):
+ def test_product_bugtask(self):
         urls = self._getBreadcrumbsURLs(
- self.product_bug_url, [self.root, self.product, self.pr...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/browser/launchpad.py'
2--- lib/canonical/launchpad/browser/launchpad.py 2009-08-11 04:35:26 +0000
3+++ lib/canonical/launchpad/browser/launchpad.py 2009-08-18 14:36:16 +0000
4@@ -33,7 +33,6 @@
5 import time
6 import urllib
7 from datetime import timedelta, datetime
8-from urlparse import urlunsplit
9
10 from zope.datetime import parseDatetimetz, tzinfo, DateTimeError
11 from zope.component import getUtility, queryAdapter
12@@ -102,7 +101,7 @@
13 from canonical.launchpad.webapp.publisher import RedirectionView
14 from canonical.launchpad.webapp.authorization import check_permission
15 from lazr.uri import URI
16-from canonical.launchpad.webapp.url import urlparse, urlappend
17+from canonical.launchpad.webapp.url import urlappend
18 from canonical.launchpad.webapp.vhosts import allvhosts
19 from canonical.widgets.project import ProjectScopeWidget
20
21@@ -216,58 +215,50 @@
22 class Hierarchy(LaunchpadView):
23 """The hierarchy part of the location bar on each page."""
24
25+ @property
26+ def objects(self):
27+ """The objects for which we want breadcrumbs."""
28+ return self.request.traversed_objects
29+
30 def items(self):
31 """Return a list of `IBreadcrumb` objects visible in the hierarchy.
32
33 The list starts with the breadcrumb closest to the hierarchy root.
34 """
35- urlparts = urlparse(self.request.getURL(0, path_only=False))
36- baseurl = urlunsplit((urlparts[0], urlparts[1], '', '', ''))
37-
38- # Construct a list of complete URLs for each URL path segment.
39- pathurls = []
40- working_url = baseurl
41- for segment in urlparts[2].split('/'):
42- working_url = urlappend(working_url, segment)
43- # Segments starting with '+' should be ignored because they
44- # will never correspond to an object in navigation.
45- if segment.startswith('+'):
46- continue
47- pathurls.append(working_url)
48-
49- # We assume a 1:1 relationship between the traversed_objects list and
50- # the URL path segments. Note that there may be more segments than
51- # there are objects.
52- object_urls = zip(self.request.traversed_objects, pathurls)
53- return self._breadcrumbs(object_urls)
54-
55- def _breadcrumbs(self, object_urls):
56- """Generate the breadcrumb list.
57-
58- :param object_urls: A sequence of (object, url) pairs.
59- :return: A list of 'IBreadcrumb' objects.
60- """
61 breadcrumbs = []
62- for obj, url in object_urls:
63- crumb = self.breadcrumb_for(obj, url)
64+ for builder in self._getBreadcrumbBuilders():
65+ crumb = builder.make_breadcrumb()
66 if crumb is not None:
67 breadcrumbs.append(crumb)
68 return breadcrumbs
69
70- def breadcrumb_for(self, obj, url):
71- """Return the breadcrumb for the an object, using the supplied URL.
72-
73- :return: An `IBreadcrumb` object, or None if a breadcrumb adaptation
74- for the object doesn't exist.
75- """
76- # If the object has an IBreadcrumbBuilder adaptation then the
77- # object is intended to be shown in the hierarchy.
78- builder = queryAdapter(obj, IBreadcrumbBuilder)
79- if builder is not None:
80- # The breadcrumb builder hasn't been given a URL yet.
81- builder.url = url
82- return builder.make_breadcrumb()
83- return None
84+ def _getBreadcrumbBuilders(self):
85+ builders = []
86+ for obj in self.objects:
87+ builder = queryAdapter(obj, IBreadcrumbBuilder)
88+ if builder is not None:
89+ builders.append(builder)
90+
91+ host = URI(self.request.getURL()).host
92+ if (len(builders) == 0
93+ or host == allvhosts.configs['mainsite'].hostname):
94+ return builders
95+
96+ # If we got this far it means we have breadcrumbs and we're not on the
97+ # mainsite, so we'll sneak an extra breadcrumb for the vhost we're on.
98+ vhost = host.split('.')[0]
99+
100+ # Iterate over the context of our builders in reverse order and for
101+ # the first one we find an adapter named after the vhost we're on,
102+ # generate an extra breadcrumb and insert it in our list.
103+ for idx in reversed(xrange(len(builders))):
104+ builder = builders[idx]
105+ extra_builder = queryAdapter(
106+ builder.context, IBreadcrumbBuilder, name=vhost)
107+ if extra_builder is not None:
108+ builders.insert(idx + 1, extra_builder)
109+ break
110+ return builders
111
112 def render(self):
113 """Render the hierarchy HTML.
114
115=== modified file 'lib/canonical/launchpad/doc/hierarchical-menu.txt'
116--- lib/canonical/launchpad/doc/hierarchical-menu.txt 2009-03-24 12:43:49 +0000
117+++ lib/canonical/launchpad/doc/hierarchical-menu.txt 2009-08-17 19:22:06 +0000
118@@ -129,11 +129,9 @@
119 IBreadcrumbBuilder factory. The factory adapts a context object and
120 produces an IBreadcrumb object for that context.
121
122-The builder holds a temporary copy of a breadcrumb until the breadcrumb
123-is in a usable state. This allows us to adapt the breadcrumb context
124-object in one place, and to defer setting the breadcrumb's URL. We can
125-ask the builder for a finished breadcrumb after both the text and the
126-URL have been specified.
127+The builder holds a temporary copy of a breadcrumb until the breadcrumb is in
128+a usable state. We can ask the builder for a finished breadcrumb after both
129+the text and the URL have been specified.
130
131 >>> from canonical.launchpad.webapp.interfaces import IBreadcrumb
132 >>> from zope.interface.verify import verifyObject
133@@ -143,7 +141,6 @@
134 True
135
136 >>> builder.text = 'Joy of cooking'
137- >>> builder.url = 'http://launchpad.dev/joy-of-cooking'
138
139 >>> breadcrumb = builder.make_breadcrumb()
140 >>> verifyObject(IBreadcrumb, breadcrumb)
141@@ -153,18 +150,6 @@
142 url='http://launchpad.dev/joy-of-cooking'
143 text='Joy of cooking'>
144
145-The builder enforces the requirement for breadcrumbs to have a valid
146-URL. The builder raises an error if a URL is not explicitly given by the
147-developer.
148-
149- >>> builder = BreadcrumbBuilder(cookbook)
150- >>> builder.text = 'Oops!'
151-
152- >>> builder.make_breadcrumb()
153- Traceback (most recent call last):
154- ...
155- AssertionError: The builder has not been given a valid breadcrumb URL.
156-
157 The breadcrumb's attributes can be overridden with subclassing and
158 Python properties.
159
160@@ -238,16 +223,14 @@
161 >>> from canonical.launchpad.browser.launchpad import Hierarchy
162
163 >>> class CustomHierarchy(Hierarchy):
164- ... def items(self):
165- ... breadcrumb = self.breadcrumb_for(
166- ... recipe, "http://launchpad.dev/some-spammy-url")
167- ... # Omitted the check for 'breadcrumb is None'.
168- ... return [breadcrumb]
169+ ... @property
170+ ... def objects(self):
171+ ... return [recipe]
172
173 >>> spammy_hierarchy = CustomHierarchy(root, request)
174 >>> spammy_hierarchy.items()
175 [<Breadcrumb
176- url='http://launchpad.dev/some-spammy-url'
177+ url='http://launchpad.dev/joy-of-cooking/spam'
178 text='Spam'
179 icon='<img src="/@@/recipe"/>'>]
180
181@@ -273,7 +256,7 @@
182 >>> print_hierarchy(hierarchy.render())
183 Location: Joy of cooking > Spam
184
185-The Launchpad Homepage displays no items in it's location bar. We are
186+The Launchpad Homepage displays no items in its location bar. We are
187 considered to be on the home page if there are no breadcrumbs.
188
189 # Simulate a visit to the site root
190
191=== modified file 'lib/canonical/launchpad/webapp/breadcrumb.py'
192--- lib/canonical/launchpad/webapp/breadcrumb.py 2009-06-25 05:30:52 +0000
193+++ lib/canonical/launchpad/webapp/breadcrumb.py 2009-08-17 20:20:17 +0000
194@@ -15,6 +15,7 @@
195 from zope.component import queryAdapter
196 from zope.interface import implements
197
198+from canonical.launchpad.webapp import canonical_url
199 from canonical.launchpad.webapp.interfaces import (
200 IBreadcrumb, IBreadcrumbBuilder)
201
202@@ -38,6 +39,9 @@
203 self.__class__.__name__, self.url, self.text, icon_repr)
204
205
206+# XXX: salgado, 2009-08-17: Since this adapter now provides a default
207+# value for the 'url' attribute, we could easily convert it into an
208+# adapter for IBreadcrumb, just changing the Hierarchy view.
209 class BreadcrumbBuilder:
210 """See `IBreadcrumbBuilder`.
211
212@@ -45,13 +49,17 @@
213 """
214 implements(IBreadcrumbBuilder)
215
216+ rootsite = 'mainsite'
217 text = None
218- url = None
219
220 def __init__(self, context):
221 self.context = context
222
223 @property
224+ def url(self):
225+ return canonical_url(self.context, rootsite=self.rootsite)
226+
227+ @property
228 def icon(self):
229 """See `IBreadcrumb`."""
230 # Get the <img> tag from the path adapter.
231
232=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
233--- lib/canonical/launchpad/webapp/interfaces.py 2009-08-10 21:07:02 +0000
234+++ lib/canonical/launchpad/webapp/interfaces.py 2009-08-18 14:36:16 +0000
235@@ -264,7 +264,10 @@
236 # We subclass IBreadcrumb to minimize interface drift.
237
238 def make_breadcrumb():
239- """Return an object implementing the `IBreadcrumb` interface."""
240+ """Return an object implementing the `IBreadcrumb` interface.
241+
242+ If for any reason no IBreadcrumb object can be created, return None.
243+ """
244
245
246 #
247
248=== modified file 'lib/canonical/launchpad/webapp/publisher.py'
249--- lib/canonical/launchpad/webapp/publisher.py 2009-07-30 08:07:21 +0000
250+++ lib/canonical/launchpad/webapp/publisher.py 2009-08-12 20:22:21 +0000
251@@ -504,10 +504,10 @@
252 'step for "%s".' % (view_name, obj.__class__.__name__))
253 urlparts.insert(0, view_name)
254
255- if request is None and rootsite is not None:
256+ if request is None:
257+ if rootsite is None:
258+ rootsite = 'mainsite'
259 root_url = allvhosts.configs[rootsite].rooturl
260- elif request is None:
261- root_url = allvhosts.configs['mainsite'].rooturl
262 else:
263 root_url = request.getRootURL(rootsite)
264
265
266=== modified file 'lib/canonical/launchpad/webapp/tests/__init__.py'
267--- lib/canonical/launchpad/webapp/tests/__init__.py 2009-06-25 05:39:50 +0000
268+++ lib/canonical/launchpad/webapp/tests/__init__.py 2009-08-18 14:36:16 +0000
269@@ -10,6 +10,10 @@
270
271 from lp.testing import TestCase
272
273+from canonical.launchpad.webapp.tests._breadcrumbs import (
274+ BaseBreadcrumbTestCase)
275+
276+
277 class DummyWebServiceConfiguration:
278 """A totally vanilla web service configuration."""
279 implements(IWebServiceConfiguration)
280
281=== added file 'lib/canonical/launchpad/webapp/tests/_breadcrumbs.py'
282--- lib/canonical/launchpad/webapp/tests/_breadcrumbs.py 1970-01-01 00:00:00 +0000
283+++ lib/canonical/launchpad/webapp/tests/_breadcrumbs.py 2009-08-18 14:36:16 +0000
284@@ -0,0 +1,33 @@
285+# Copyright 2009 Canonical Ltd. This software is licensed under the
286+# GNU Affero General Public License version 3 (see the file LICENSE).
287+
288+__metaclass__ = type
289+
290+from zope.component import getMultiAdapter
291+
292+from canonical.lazr.testing.menus import make_fake_request
293+from canonical.launchpad.webapp.publisher import RootObject
294+from canonical.testing import DatabaseFunctionalLayer
295+from lp.testing import TestCaseWithFactory
296+
297+
298+class BaseBreadcrumbTestCase(TestCaseWithFactory):
299+
300+ layer = DatabaseFunctionalLayer
301+
302+ def setUp(self):
303+ super(BaseBreadcrumbTestCase, self).setUp()
304+ self.root = RootObject()
305+
306+ def _getHierarchyView(self, url, traversed_objects):
307+ request = make_fake_request(url, traversed_objects)
308+ return getMultiAdapter((self.root, request), name='+hierarchy')
309+
310+ def _getBreadcrumbsTexts(self, url, traversed_objects):
311+ view = self._getHierarchyView(url, traversed_objects)
312+ return [crumb.text for crumb in view.items()]
313+
314+ def _getBreadcrumbsURLs(self, url, traversed_objects):
315+ view = self._getHierarchyView(url, traversed_objects)
316+ return [crumb.url for crumb in view.items()]
317+
318
319=== added file 'lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py'
320--- lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 1970-01-01 00:00:00 +0000
321+++ lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 2009-08-18 14:36:16 +0000
322@@ -0,0 +1,81 @@
323+# Copyright 2009 Canonical Ltd. This software is licensed under the
324+# GNU Affero General Public License version 3 (see the file LICENSE).
325+
326+__metaclass__ = type
327+
328+import unittest
329+
330+from canonical.launchpad.webapp.publisher import canonical_url
331+from canonical.launchpad.webapp.tests import BaseBreadcrumbTestCase
332+from lp.testing import login
333+
334+
335+class TestExtraVHostBreadcrumbsOnHierarchyView(BaseBreadcrumbTestCase):
336+ """How our breadcrumbs behave when using a vhost other the main one?
337+
338+ When we go to bugs.lp.net/ubuntu, we only traversed the Ubuntu distro, so
339+ that's what we'd have a breadcrumb for, but we also want to generate a
340+ breadcrumb for bugs on Ubuntu, given that we're on the bugs vhost.
341+
342+ The behaviour is similar to other vhosts; read on for more.
343+ """
344+
345+ def setUp(self):
346+ super(TestExtraVHostBreadcrumbsOnHierarchyView, self).setUp()
347+ login('test@canonical.com')
348+ self.product = self.factory.makeProduct(name='crumb-tester')
349+ self.product_bug = self.factory.makeBug(product=self.product)
350+ self.product_bug_url = canonical_url(self.product_bug)
351+ self.source_package = self.factory.makeSourcePackage()
352+ self.package_bug = self.factory.makeBugTask(
353+ target=self.source_package)
354+ self.package_bug_url = canonical_url(self.package_bug)
355+
356+ def test_root_on_mainsite(self):
357+ urls = self._getBreadcrumbsURLs('http://launchpad.dev/', [self.root])
358+ self.assertEquals(urls, [])
359+
360+ def test_product_on_mainsite(self):
361+ urls = self._getBreadcrumbsURLs(
362+ 'http://launchpad.dev/%s' % self.product.name,
363+ [self.root, self.product])
364+ self.assertEquals(urls, ['http://launchpad.dev/crumb-tester'])
365+
366+ def test_root_on_vhost(self):
367+ urls = self._getBreadcrumbsURLs(
368+ 'http://bugs.launchpad.dev/', [self.root])
369+ self.assertEquals(urls, [])
370+
371+ def test_product_on_vhost(self):
372+ urls = self._getBreadcrumbsURLs(
373+ 'http://bugs.launchpad.dev/%s' % self.product.name,
374+ [self.root, self.product])
375+ self.assertEquals(
376+ urls, ['http://launchpad.dev/crumb-tester',
377+ 'http://bugs.launchpad.dev/crumb-tester'])
378+
379+ def test_product_bug(self):
380+ urls = self._getBreadcrumbsURLs(
381+ self.product_bug_url, [self.root, self.product, self.product_bug])
382+ self.assertEquals(
383+ urls, ['http://launchpad.dev/crumb-tester',
384+ 'http://bugs.launchpad.dev/crumb-tester'])
385+
386+ def test_package_bug(self):
387+ target = self.package_bug.target
388+ distro_url = canonical_url(target.distribution)
389+ distroseries_url = canonical_url(target.distroseries)
390+ package_url = canonical_url(target)
391+ package_bugs_url = canonical_url(target, rootsite='bugs')
392+ urls = self._getBreadcrumbsURLs(
393+ self.package_bug_url,
394+ [self.root, target.distribution, target.distroseries, target,
395+ self.package_bug])
396+ self.assertEquals(
397+ urls,
398+ [distro_url, distroseries_url, package_url, package_bugs_url,
399+ self.package_bug_url])
400+
401+
402+def test_suite():
403+ return unittest.TestLoader().loadTestsFromName(__name__)
404
405=== modified file 'lib/canonical/lazr/testing/menus.py'
406--- lib/canonical/lazr/testing/menus.py 2009-08-13 00:51:50 +0000
407+++ lib/canonical/lazr/testing/menus.py 2009-08-18 12:21:51 +0000
408@@ -60,7 +60,8 @@
409 SERVER_URL=server_url,
410 PATH_INFO=path_info)
411 request._traversed_names = path_info.split('/')[1:]
412- request.traversed_objects = traversed_objects
413+ if traversed_objects is not None:
414+ request.traversed_objects = traversed_objects
415 # After making the request, setup a new interaction.
416 endInteraction()
417 newInteraction(request)
418
419=== modified file 'lib/lp/bugs/browser/bugtarget.py'
420--- lib/lp/bugs/browser/bugtarget.py 2009-08-17 17:09:23 +0000
421+++ lib/lp/bugs/browser/bugtarget.py 2009-08-18 14:36:16 +0000
422@@ -56,6 +56,7 @@
423 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
424 from canonical.launchpad.interfaces.temporaryblobstorage import (
425 ITemporaryStorageManager)
426+from canonical.launchpad.webapp.breadcrumb import BreadcrumbBuilder
427 from canonical.launchpad.webapp.interfaces import ILaunchBag, NotFoundError
428 from lp.bugs.interfaces.bug import (
429 CreateBugParams, IBugAddForm, IFrontPageBugAddForm, IProjectBugAddForm)
430@@ -1344,3 +1345,10 @@
431 """The URL the user is sent to when clicking the "cancel" link."""
432 return canonical_url(self.context)
433
434+
435+class BugTargetOnBugsVHostBreadcrumbBuilder(BreadcrumbBuilder):
436+ rootsite = 'bugs'
437+
438+ @property
439+ def text(self):
440+ return 'Bugs on %s' % self.context.name
441
442=== modified file 'lib/lp/bugs/browser/bugtask.py'
443--- lib/lp/bugs/browser/bugtask.py 2009-08-14 16:33:17 +0000
444+++ lib/lp/bugs/browser/bugtask.py 2009-08-18 14:36:16 +0000
445@@ -3497,6 +3497,8 @@
446 class BugTaskBreadcrumbBuilder(BreadcrumbBuilder):
447 """Builds a breadcrumb for an `IBugTask`."""
448
449+ rootsite = 'bugs'
450+
451 @property
452 def text(self):
453 return self.context.bug.displayname
454
455=== modified file 'lib/lp/bugs/configure.zcml'
456--- lib/lp/bugs/configure.zcml 2009-08-13 10:14:48 +0000
457+++ lib/lp/bugs/configure.zcml 2009-08-18 14:36:16 +0000
458@@ -871,4 +871,10 @@
459 <allow
460 interface="lp.bugs.interfaces.bugnotification.IBugNotificationSet"/>
461 </securedutility>
462+ <adapter
463+ name="bugs"
464+ provides="canonical.launchpad.webapp.interfaces.IBreadcrumbBuilder"
465+ for="lp.bugs.interfaces.bugtarget.IBugTarget"
466+ factory="lp.bugs.browser.bugtarget.BugTargetOnBugsVHostBreadcrumbBuilder"
467+ permission="zope.Public"/>
468 </configure>
469
470=== modified file 'lib/lp/bugs/stories/bug-release-management/nomination-navigation.txt'
471--- lib/lp/bugs/stories/bug-release-management/nomination-navigation.txt 2009-08-14 14:50:19 +0000
472+++ lib/lp/bugs/stories/bug-release-management/nomination-navigation.txt 2009-08-18 18:00:01 +0000
473@@ -11,7 +11,8 @@
474 ... 'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/1'
475 ... '/nominations/2/+editstatus')
476 >>> print_location(admin_browser.contents)
477- Hierarchy: Launchpad > Ubuntu > ?mozilla-firefox? package > Bug #1
478+ Hierarchy: Launchpad > Ubuntu > ?mozilla-firefox? package >
479+ Bugs on mozilla-firefox > Bug #1
480 Tabs:
481 * Overview - http://launchpad.dev/ubuntu/+source/mozilla-firefox
482 * Code - http://code.launchpad.dev/ubuntu/+source/mozilla-firefox
483
484=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-activity.txt'
485--- lib/lp/bugs/stories/bugs/xx-bug-activity.txt 2009-08-14 09:26:51 +0000
486+++ lib/lp/bugs/stories/bugs/xx-bug-activity.txt 2009-08-18 18:00:01 +0000
487@@ -12,7 +12,8 @@
488 the main heading repeats the bug number for clarity:
489
490 >>> print_location(anon_browser.contents)
491- Hierarchy: Launchpad > Debian > ?mozilla-firefox? package > Bug #3
492+ Hierarchy: Launchpad > Debian > ?mozilla-firefox? package >
493+ Bugs on mozilla-firefox > Bug #3
494 Tabs:
495 * Overview - http://launchpad.dev/debian/+source/mozilla-firefox
496 * Code - http://code.launchpad.dev/debian/+source/mozilla-firefox
497
498=== modified file 'lib/lp/bugs/tests/test_breadcrumbs.py'
499--- lib/lp/bugs/tests/test_breadcrumbs.py 2009-08-14 16:33:17 +0000
500+++ lib/lp/bugs/tests/test_breadcrumbs.py 2009-08-18 14:36:16 +0000
501@@ -5,67 +5,38 @@
502
503 import unittest
504
505-from zope.component import getMultiAdapter
506-
507-from canonical.lazr.testing.menus import make_fake_request
508-from canonical.launchpad.webapp.publisher import RootObject
509-from canonical.testing import DatabaseFunctionalLayer
510-from lp.testing import (
511- ANONYMOUS, TestCaseWithFactory, login)
512-
513-
514-class TestBugTaskBreadcrumbBuilder(TestCaseWithFactory):
515-
516- layer = DatabaseFunctionalLayer
517+from canonical.launchpad.webapp.publisher import canonical_url
518+from canonical.launchpad.webapp.tests import BaseBreadcrumbTestCase
519+from lp.testing import ANONYMOUS, login
520+
521+
522+class TestBugTaskBreadcrumbBuilder(BaseBreadcrumbTestCase):
523
524 def setUp(self):
525 super(TestBugTaskBreadcrumbBuilder, self).setUp()
526- self.product = self.factory.makeProduct(
527+ product = self.factory.makeProduct(
528 name='crumb-tester', displayname="Crumb Tester")
529- self.bug = self.factory.makeBug(product=self.product)
530- self.root = RootObject()
531-
532- def test_bugtask_on_mainsite(self):
533- request = make_fake_request(
534- 'http://launchpad.dev/%s/+bug/%d' % (
535- self.product.name, self.bug.id),
536- [self.root, self.product, self.bug.default_bugtask])
537- hierarchy = getMultiAdapter((self.root, request), name='+hierarchy')
538- self.assertEquals(
539- ['http://launchpad.dev/crumb-tester',
540- 'http://launchpad.dev/crumb-tester/+bug/%d' % self.bug.id],
541- [crumb.url for crumb in hierarchy.items()])
542- self.assertEquals(
543- ["Crumb Tester", "Bug #%d" % self.bug.id],
544- [crumb.text for crumb in hierarchy.items()])
545-
546- def test_bugtask_on_vhost(self):
547- request = make_fake_request(
548- 'http://bugs.launchpad.dev/%s/+bug/%d' % (
549- self.product.name, self.bug.id),
550- [self.root, self.product, self.bug.default_bugtask])
551- hierarchy = getMultiAdapter((self.root, request), name='+hierarchy')
552- self.assertEquals(
553- ['http://bugs.launchpad.dev/crumb-tester',
554- 'http://bugs.launchpad.dev/crumb-tester/+bug/%d' % self.bug.id],
555- [crumb.url for crumb in hierarchy.items()])
556- self.assertEquals(
557- ["Crumb Tester", "Bug #%d" % self.bug.id],
558- [crumb.text for crumb in hierarchy.items()])
559-
560- def test_bugtask_child_on_vhost(self):
561- request = make_fake_request(
562- 'http://bugs.launchpad.dev/%s/+bug/%d/+activity' % (
563- self.product.name, self.bug.id),
564- [self.root, self.product, self.bug.default_bugtask])
565- hierarchy = getMultiAdapter((self.root, request), name='+hierarchy')
566- self.assertEquals(
567- ['http://bugs.launchpad.dev/crumb-tester',
568- 'http://bugs.launchpad.dev/crumb-tester/+bug/%d' % self.bug.id],
569- [crumb.url for crumb in hierarchy.items()])
570- self.assertEquals(
571- ["Crumb Tester", "Bug #%d" % self.bug.id],
572- [crumb.text for crumb in hierarchy.items()])
573+ self.bug = self.factory.makeBug(product=product)
574+ self.bugtask_url = canonical_url(
575+ self.bug.default_bugtask, rootsite='bugs')
576+ self.traversed_objects = [
577+ self.root, product, self.bug.default_bugtask]
578+
579+ def test_bugtask(self):
580+ urls = self._getBreadcrumbsURLs(
581+ self.bugtask_url, self.traversed_objects)
582+ self.assertEquals(urls[-1], self.bugtask_url)
583+ texts = self._getBreadcrumbsTexts(
584+ self.bugtask_url, self.traversed_objects)
585+ self.assertEquals(texts[-1], "Bug #%d" % self.bug.id)
586+
587+ def test_bugtask_child(self):
588+ url = canonical_url(
589+ self.bug.default_bugtask, rootsite='bugs', view_name='+activity')
590+ urls = self._getBreadcrumbsURLs(url, self.traversed_objects)
591+ self.assertEquals(urls[-1], self.bugtask_url)
592+ texts = self._getBreadcrumbsTexts(url, self.traversed_objects)
593+ self.assertEquals(texts[-1], "Bug #%d" % self.bug.id)
594
595 def test_bugtask_private_bug(self):
596 # A breadcrumb is not generated for a bug that the user does
597@@ -73,16 +44,15 @@
598 login('foo.bar@canonical.com')
599 self.bug.setPrivate(True, self.bug.owner)
600 login(ANONYMOUS)
601- request = make_fake_request(
602- 'http://bugs.launchpad.dev/%s/+bug/%d/+activity' % (
603- self.product.name, self.bug.id),
604- [self.root, self.product, self.bug.default_bugtask])
605- hierarchy = getMultiAdapter((self.root, request), name='+hierarchy')
606- self.assertEquals(
607- ['http://bugs.launchpad.dev/crumb-tester'],
608- [crumb.url for crumb in hierarchy.items()])
609- self.assertEquals(
610- ["Crumb Tester"], [crumb.text for crumb in hierarchy.items()])
611+ url = canonical_url(
612+ self.bug.default_bugtask, rootsite='bugs', view_name='+activity')
613+ self.assertEquals(
614+ ['http://launchpad.dev/crumb-tester',
615+ 'http://bugs.launchpad.dev/crumb-tester'],
616+ self._getBreadcrumbsURLs(url, self.traversed_objects))
617+ self.assertEquals(
618+ ["Crumb Tester", "Bugs on crumb-tester"],
619+ self._getBreadcrumbsTexts(url, self.traversed_objects))
620
621
622 def test_suite():
623
624=== modified file 'lib/lp/code/browser/branch.py'
625--- lib/lp/code/browser/branch.py 2009-08-13 07:29:51 +0000
626+++ lib/lp/code/browser/branch.py 2009-08-18 12:21:51 +0000
627@@ -115,11 +115,10 @@
628 class BranchHierarchy(Hierarchy):
629 """The hierarchy for a branch should be the product if there is one."""
630
631- def items(self):
632+ @property
633+ def objects(self):
634 """See `Hierarchy`."""
635- return self._breadcrumbs(
636- (obj, canonical_url(obj))
637- for obj in IHasBranchTarget(self.context).target.components)
638+ return IHasBranchTarget(self.context).target.components
639
640
641 class BranchNavigation(Navigation):
642
643=== modified file 'lib/lp/soyuz/stories/packaging/package-pages-navigation.txt'
644--- lib/lp/soyuz/stories/packaging/package-pages-navigation.txt 2009-07-06 21:52:17 +0000
645+++ lib/lp/soyuz/stories/packaging/package-pages-navigation.txt 2009-08-18 18:00:01 +0000
646@@ -31,7 +31,7 @@
647
648 >>> anon_browser.open('http://bugs.launchpad.dev/ubuntu/+source/alsa-utils')
649 >>> print_location(anon_browser.contents)
650- Hierarchy: Launchpad > Ubuntu > ?alsa-utils? package
651+ Hierarchy: Launchpad > Ubuntu > ?alsa-utils? package > Bugs on alsa-utils
652 Tabs:
653 * Overview - http://launchpad.dev/ubuntu/+source/alsa-utils
654 * Code - http://code.launchpad.dev/ubuntu/+source/alsa-utils
655
656=== modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-navigation.txt'
657--- lib/lp/soyuz/stories/ppa/xx-ppa-navigation.txt 2009-07-01 13:16:44 +0000
658+++ lib/lp/soyuz/stories/ppa/xx-ppa-navigation.txt 2009-08-18 18:00:01 +0000
659@@ -109,7 +109,7 @@
660 The user could return to the 'PPA' overview by using the breadcrumb link.
661
662 >>> print anon_browser.getLink('Default PPA').url
663- http://launchpad.dev/%7Ecprov/+archive/ppa
664+ http://launchpad.dev/~cprov/+archive/ppa
665
666 The user can navigate to an individual build details:
667
668