Merge lp:~salgado/launchpad/real-breadcrumbs-2 into lp:launchpad
- real-breadcrumbs-2
- Merge into devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guilherme Salgado (salgado) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Brad Crittenden (bac) wrote : | # |
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/
> --- lib/canonical/
+++ lib/canonical/
> @@ -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.
> from canonical.
> from lazr.uri import URI
> -from canonical.
> +from canonical.
> from canonical.
> from canonical.
>
> @@ -216,58 +215,50 @@
> class Hierarchy(
> """The hierarchy part of the location bar on each page."""
>
> + @property
> + def objects(self):
> + """The objects for which we want breadcrumbs."""
> + return self.request.
> +
> 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(
> - baseurl = urlunsplit(
> -
> - # Construct a list of complete URLs for each URL path segment.
> - pathurls = []
> - working_url = baseurl
> - for segment in urlparts[
> - working_url = urlappend(
> - # Segments starting with '+' should be ignored because they
> - # will never correspond to an object in navigation.
> - if segment.
> - continue
> - pathurls.
> -
> - # 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.
> - return self._breadcrum
> -
> - 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 builder in self._getBreadc
> + crumb = builder.
> if crumb is not None:
> breadcrumbs.
> return breadcrumbs...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guilherme Salgado (salgado) wrote : | # |
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/
> > --- lib/canonical/
> > +++ lib/canonical/
> > @@ -504,10 +504,10 @@
> > 'step for "%s".' % (view_name, obj.__class_
> > 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 ICanonicalUrlDa
> 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.
> > - elif request is None:
> > - root_url = allvhosts.
> > else:
> > root_url = request.
> >
>
> > === added file 'lib/canonical/
> > --- lib/canonical/
> > +++ lib/canonical/
> > @@ -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.
> > +from canonical.
> > +from lp.testing import login
> > +
> > +
> > +class TestExtraVHostB
> > + """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(TestExtra
> > + login('<email address hidden>')
> > + self.product = self.factory.
> > + self.product_bug = self.factory.
> > + self.product_
> > + self.source_package = self.factory.
> > + self.package_bug = self.factory.
> > + target=
> > + s...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guilherme Salgado (salgado) wrote : | # |
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/
--- lib/canonical/
+++ lib/canonical/
@@ -11,7 +11,7 @@
class TestExtraVHostB
- """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 @@
- self.product_bug = self.factory.
- self.product_
+ self.product_url = canonical_
+ self.product_
+ product_bug = self.factory.
+ self.product_
+ self.product_
- self.package_bug = self.factory.
+ self.package_
- self.package_
+ self.package_
def test_root_
urls = self._getBreadc
@@ -37,9 +40,8 @@
def test_product_
urls = self._getBreadc
- 'http://
- [self.root, self.product])
- self.assertEqua
+ self.product_url, [self.root, self.product])
+ self.assertEqua
def test_root_
urls = self._getBreadc
@@ -48,33 +50,31 @@
def test_product_
urls = self._getBreadc
- 'http://
- [self.root, self.product])
- self.assertEquals(
- urls, ['http://
- 'http://
+ self.product_
+ self.assertEqua
- def test_product_
+ def test_product_
urls = self._getBreadc
- self.product_
Preview Diff
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 |
= 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: code/browser/ branch. py /launchpad/ webapp/ publisher. py /launchpad/ webapp/ tests/test_ breadcrumbs. py registry/ configure. zcml /launchpad/ browser/ launchpad. py /launchpad/ webapp/ breadcrumb. py registry/ browser/ pillar. py /lazr/testing/ menus.py /launchpad/ doc/hierarchica l-menu. txt
lib/lp/
lib/canonical
lib/canonical
lib/lp/
lib/canonical
lib/canonical
lib/lp/
lib/canonical
lib/canonical
== 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>