Merge lp:~salgado/launchpad/real-breadcrumbs into lp:launchpad
- real-breadcrumbs
- Merge into devel
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Aaron Bentley | ||||||||
Approved revision: | no longer in the source branch. | ||||||||
Merged at revision: | not available | ||||||||
Proposed branch: | lp:~salgado/launchpad/real-breadcrumbs | ||||||||
Merge into: | lp:launchpad | ||||||||
Diff against target: | None lines | ||||||||
To merge this branch: | bzr merge lp:~salgado/launchpad/real-breadcrumbs | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Aaron Bentley (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)
Aaron Bentley (abentley) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
status approved
The only think I see that could be improved is in Heirarchy.items, noted
below.
Guilherme Salgado wrote:
> Guilherme Salgado has proposed merging lp:~salgado/launchpad/real-breadcrumbs into lp:launchpad/devel.
>
> Requested reviews:
> Canonical Launchpad Engineering (launchpad)
>
> = Summary =
>
> When I redesigned our breadcrumb infrastructure I made the
> BreadcbrumbBuilder abstraction unnecessary, but my branch was already
> too big so I left it to be removed later.
>
> == Proposed fix ==
>
> Remove BreadcrumbBuilder by directly adapting objects into IBreadcrumbs,
> instead of adapting them into IBreadcrumbBuilder and calling
> make_breadcrumb() on it.
>
> In order to actually complete this I had to change all existing adapters
> so that they inherit from Breadcrumb rather than BreadcrumbBuilder, but
> that brought the diff to 1.4KLOC, so I'll ask for a review of that
> later.
>
> == Pre-implementation notes ==
>
> == Implementation details ==
>
> == Tests ==
>
> ./bin/test -vvt hierarchical-
>
> == 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/
> lib/canonical/
> lib/canonical/
> lib/canonical/
> lib/canonical/
> lib/canonical/
>
>
> == Pyflakes Doctest notices ==
>
> lib/canonical/
> 144: 'canonical_url' imported but unused
>
> This has been removed already.
>
>
> == Pylint notices ==
>
> lib/canonical/
> 103: [F0401] Unable to import 'lazr.uri' (No module named uri)
>
>
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -96,7 +96,7 @@
> StandardLaunchp
> stepto)
> from canonical.
> - IBreadcrumbBuilder, ILaunchBag, ILaunchpadRoot, INavigationMenu,
> + IBreadcrumb, ILaunchBag, ILaunchpadRoot, INavigationMenu,
> NotFoundError, POSTToNonCanoni
> from canonical.
> from canonical.
> @@ -226,39 +226,31 @@
> The list starts with the breadcrumb closest to the hierarchy root.
> """
> breadcrumbs = []
> - for builder in self._getBreadc
> - crumb = builder.
> - if crumb is not None:
> - breadcrumbs.
> - return breadcrumbs
> -
> - def _getBreadcrumbB
> - builders = []
> for obj in self.objects:
> - builder = queryAdapter(obj, IBreadcrumbBuilder)
> -...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guilherme Salgado (salgado) wrote : | # |
Hi Aaron,
I did the change you suggested, but I need to cast the result of enumerate(
Also, https:/
Preview Diff
1 | === modified file 'lib/canonical/launchpad/browser/launchpad.py' |
2 | --- lib/canonical/launchpad/browser/launchpad.py 2009-08-20 05:06:34 +0000 |
3 | +++ lib/canonical/launchpad/browser/launchpad.py 2009-08-24 17:47:34 +0000 |
4 | @@ -96,7 +96,7 @@ |
5 | StandardLaunchpadFacets, canonical_name, canonical_url, custom_widget, |
6 | stepto) |
7 | from canonical.launchpad.webapp.interfaces import ( |
8 | - IBreadcrumbBuilder, ILaunchBag, ILaunchpadRoot, INavigationMenu, |
9 | + IBreadcrumb, ILaunchBag, ILaunchpadRoot, INavigationMenu, |
10 | NotFoundError, POSTToNonCanonicalURL) |
11 | from canonical.launchpad.webapp.publisher import RedirectionView |
12 | from canonical.launchpad.webapp.authorization import check_permission |
13 | @@ -226,39 +226,31 @@ |
14 | The list starts with the breadcrumb closest to the hierarchy root. |
15 | """ |
16 | breadcrumbs = [] |
17 | - for builder in self._getBreadcrumbBuilders(): |
18 | - crumb = builder.make_breadcrumb() |
19 | - if crumb is not None: |
20 | - breadcrumbs.append(crumb) |
21 | - return breadcrumbs |
22 | - |
23 | - def _getBreadcrumbBuilders(self): |
24 | - builders = [] |
25 | for obj in self.objects: |
26 | - builder = queryAdapter(obj, IBreadcrumbBuilder) |
27 | - if builder is not None: |
28 | - builders.append(builder) |
29 | + breadcrumb = queryAdapter(obj, IBreadcrumb) |
30 | + if breadcrumb is not None: |
31 | + breadcrumbs.append(breadcrumb) |
32 | |
33 | host = URI(self.request.getURL()).host |
34 | - if (len(builders) == 0 |
35 | + if (len(breadcrumbs) == 0 |
36 | or host == allvhosts.configs['mainsite'].hostname): |
37 | - return builders |
38 | + return breadcrumbs |
39 | |
40 | # If we got this far it means we have breadcrumbs and we're not on the |
41 | # mainsite, so we'll sneak an extra breadcrumb for the vhost we're on. |
42 | vhost = host.split('.')[0] |
43 | |
44 | - # Iterate over the context of our builders in reverse order and for |
45 | + # Iterate over the context of our breadcrumbs in reverse order and for |
46 | # the first one we find an adapter named after the vhost we're on, |
47 | # generate an extra breadcrumb and insert it in our list. |
48 | - for idx in reversed(xrange(len(builders))): |
49 | - builder = builders[idx] |
50 | - extra_builder = queryAdapter( |
51 | - builder.context, IBreadcrumbBuilder, name=vhost) |
52 | - if extra_builder is not None: |
53 | - builders.insert(idx + 1, extra_builder) |
54 | + for idx in reversed(xrange(len(breadcrumbs))): |
55 | + breadcrumb = breadcrumbs[idx] |
56 | + extra_breadcrumb = queryAdapter( |
57 | + breadcrumb.context, IBreadcrumb, name=vhost) |
58 | + if extra_breadcrumb is not None: |
59 | + breadcrumbs.insert(idx + 1, extra_breadcrumb) |
60 | break |
61 | - return builders |
62 | + return breadcrumbs |
63 | |
64 | def render(self): |
65 | """Render the hierarchy HTML. |
66 | |
67 | === modified file 'lib/canonical/launchpad/browser/personproduct.py' |
68 | --- lib/canonical/launchpad/browser/personproduct.py 2009-06-25 05:30:52 +0000 |
69 | +++ lib/canonical/launchpad/browser/personproduct.py 2009-08-24 17:47:34 +0000 |
70 | @@ -5,7 +5,7 @@ |
71 | |
72 | __metaclass__ = type |
73 | __all__ = [ |
74 | - 'PersonProductBreadcrumbBuilder', |
75 | + 'PersonProductBreadcrumb', |
76 | 'PersonProductFacets', |
77 | 'PersonProductNavigation', |
78 | ] |
79 | @@ -17,7 +17,7 @@ |
80 | from lp.code.interfaces.branchnamespace import ( |
81 | get_branch_namespace) |
82 | from canonical.launchpad.interfaces.personproduct import IPersonProduct |
83 | -from canonical.launchpad.webapp.breadcrumb import BreadcrumbBuilder |
84 | +from canonical.launchpad.webapp.breadcrumb import Breadcrumb |
85 | from canonical.launchpad.webapp import ( |
86 | Link, Navigation, StandardLaunchpadFacets) |
87 | from canonical.launchpad.webapp.interfaces import NotFoundError |
88 | @@ -38,8 +38,8 @@ |
89 | return branch |
90 | |
91 | |
92 | -class PersonProductBreadcrumbBuilder(BreadcrumbBuilder): |
93 | - """Builds a breadcrumb for an `IPersonProduct`.""" |
94 | +class PersonProductBreadcrumb(Breadcrumb): |
95 | + """Breadcrumb for an `IPersonProduct`.""" |
96 | |
97 | @property |
98 | def text(self): |
99 | |
100 | === modified file 'lib/canonical/launchpad/doc/hierarchical-menu.txt' |
101 | --- lib/canonical/launchpad/doc/hierarchical-menu.txt 2009-08-17 19:22:06 +0000 |
102 | +++ lib/canonical/launchpad/doc/hierarchical-menu.txt 2009-08-24 17:47:34 +0000 |
103 | @@ -60,7 +60,7 @@ |
104 | |
105 | The Hierarchy class builds the breadcrumbs by looking at each object in |
106 | the request.traversed_objects attribute. If a traversed object can be |
107 | -adapted to IBreadcrumbBuilder, then it is added to the breadcrumbs list. |
108 | +adapted to IBreadcrumb, then it is added to the breadcrumbs list. |
109 | |
110 | We'll add the objects to the request's list of traversed objects so |
111 | the hierarchy will discover them. |
112 | @@ -71,38 +71,35 @@ |
113 | ... [root, cookbook, recipe]) |
114 | |
115 | The hierarchy's list of breadcrumbs is empty since none of the objects |
116 | -have an IBreadcrumbBuilder adapter. |
117 | +have an IBreadcrumb adapter. |
118 | |
119 | >>> hierarchy = getMultiAdapter((recipe, request), name='+hierarchy') |
120 | >>> hierarchy.items() |
121 | [] |
122 | |
123 | -The ICookbook and IRecipe breadcrumb objects show up in the hierarchy |
124 | -after IBreadcrumbBuilder adapters are registered for them. The |
125 | -hierarchy builds a list of breadcrumbs starting with the breadcrumb |
126 | -closest to the hierarchy root. |
127 | +The ICookbook and IRecipe breadcrumb objects show up in the hierarchy after |
128 | +IBreadcrumb adapters are registered for them. The hierarchy builds a list of |
129 | +breadcrumbs starting with the breadcrumb closest to the hierarchy root. |
130 | |
131 | - >>> from canonical.launchpad.webapp.breadcrumb import BreadcrumbBuilder |
132 | + >>> from canonical.launchpad.webapp.breadcrumb import Breadcrumb |
133 | |
134 | # Note that the Hierarchy assigns the breadcrumb's URL, but we need to |
135 | # give it a valid .text attribute. |
136 | - >>> class TextualBreadcrumbBuilder(BreadcrumbBuilder): |
137 | + >>> class TextualBreadcrumb(Breadcrumb): |
138 | ... @property |
139 | ... def text(self): |
140 | ... return self.context.name.capitalize().replace('-', ' ') |
141 | |
142 | - >>> from canonical.launchpad.webapp.interfaces import IBreadcrumbBuilder |
143 | + >>> from canonical.launchpad.webapp.interfaces import IBreadcrumb |
144 | |
145 | - >>> provideAdapter( |
146 | - ... TextualBreadcrumbBuilder, [ICookbook], IBreadcrumbBuilder) |
147 | - >>> provideAdapter( |
148 | - ... TextualBreadcrumbBuilder, [IRecipe], IBreadcrumbBuilder) |
149 | + >>> provideAdapter(TextualBreadcrumb, [ICookbook], IBreadcrumb) |
150 | + >>> provideAdapter(TextualBreadcrumb, [IRecipe], IBreadcrumb) |
151 | |
152 | >>> hierarchy.items() |
153 | - [<Breadcrumb |
154 | + [<TextualBreadcrumb |
155 | url='http://launchpad.dev/joy-of-cooking' |
156 | text='Joy of cooking'>, |
157 | - <Breadcrumb |
158 | + <TextualBreadcrumb |
159 | url='http://launchpad.dev/joy-of-cooking/spam' |
160 | text='Spam'>] |
161 | |
162 | @@ -114,59 +111,45 @@ |
163 | ... 'http://launchpad.dev/+cooker/jamie', |
164 | ... [root, cooker]) |
165 | |
166 | - >>> provideAdapter( |
167 | - ... TextualBreadcrumbBuilder, [ICooker], IBreadcrumbBuilder) |
168 | + >>> provideAdapter(TextualBreadcrumb, [ICooker], IBreadcrumb) |
169 | |
170 | >>> cooker_hierarchy = getMultiAdapter( |
171 | ... (cooker, cooker_request), name='+hierarchy') |
172 | >>> cooker_hierarchy.items() |
173 | - [<Breadcrumb url='http://launchpad.dev/+cooker/jamie' text='Jamie'>] |
174 | + [<TextualBreadcrumb url='http://launchpad.dev/+cooker/jamie' text='Jamie'>] |
175 | |
176 | |
177 | == Building IBreadcrumb objects == |
178 | |
179 | -The construction of breadcrumb objects is handled by an |
180 | -IBreadcrumbBuilder factory. The factory adapts a context object and |
181 | -produces an IBreadcrumb object for that context. |
182 | - |
183 | -The builder holds a temporary copy of a breadcrumb until the breadcrumb is in |
184 | -a usable state. We can ask the builder for a finished breadcrumb after both |
185 | -the text and the URL have been specified. |
186 | +The construction of breadcrumb objects is handled by an IBreadcrumb adapter, |
187 | +which adapts a context object and produces an IBreadcrumb object for that |
188 | +context. The default adapter provides the url attribute, but the breadcrumb's |
189 | +text must be overriden in subclasses. |
190 | |
191 | >>> from canonical.launchpad.webapp.interfaces import IBreadcrumb |
192 | >>> from zope.interface.verify import verifyObject |
193 | - |
194 | - >>> builder = BreadcrumbBuilder(cookbook) |
195 | - >>> verifyObject(IBreadcrumbBuilder, builder) |
196 | - True |
197 | - |
198 | - >>> builder.text = 'Joy of cooking' |
199 | - |
200 | - >>> breadcrumb = builder.make_breadcrumb() |
201 | + >>> breadcrumb = Breadcrumb(cookbook) |
202 | >>> verifyObject(IBreadcrumb, breadcrumb) |
203 | True |
204 | + >>> print breadcrumb.text |
205 | + None |
206 | >>> breadcrumb |
207 | <Breadcrumb |
208 | url='http://launchpad.dev/joy-of-cooking' |
209 | - text='Joy of cooking'> |
210 | + text='None'> |
211 | |
212 | -The breadcrumb's attributes can be overridden with subclassing and |
213 | -Python properties. |
214 | +As said above, the breadcrumb's attributes can be overridden with subclassing |
215 | +and Python properties. |
216 | |
217 | >>> from canonical.launchpad.webapp.publisher import canonical_url |
218 | - |
219 | - >>> class DynamicBreadcrumbBuilder(BreadcrumbBuilder): |
220 | + >>> class DynamicBreadcrumb(Breadcrumb): |
221 | ... @property |
222 | ... def text(self): |
223 | ... return self.context.name.capitalize().replace('-', ' ') |
224 | - ... |
225 | - ... @property |
226 | - ... def url(self): |
227 | - ... return canonical_url(self.context) |
228 | |
229 | - >>> builder = DynamicBreadcrumbBuilder(cookbook) |
230 | - >>> builder.make_breadcrumb() |
231 | - <Breadcrumb |
232 | + >>> breadcrumb = DynamicBreadcrumb(cookbook) |
233 | + >>> breadcrumb |
234 | + <DynamicBreadcrumb |
235 | url='http://launchpad.dev/joy-of-cooking' |
236 | text='Joy of cooking'> |
237 | |
238 | @@ -186,9 +169,9 @@ |
239 | >>> provideAdapter( |
240 | ... RecipeImageDisplayAPI, [IRecipe], IPathAdapter, 'image') |
241 | |
242 | - >>> builder = DynamicBreadcrumbBuilder(recipe) |
243 | - >>> builder.make_breadcrumb() |
244 | - <Breadcrumb |
245 | + >>> breadcrumb = DynamicBreadcrumb(recipe) |
246 | + >>> breadcrumb |
247 | + <DynamicBreadcrumb |
248 | url='http://launchpad.dev/joy-of-cooking/spam' |
249 | text='Spam' |
250 | icon='<img src="/@@/recipe"/>'> |
251 | @@ -204,9 +187,9 @@ |
252 | >>> print queryAdapter(cookbook, IPathAdapter, name='image').icon() |
253 | None |
254 | |
255 | - >>> builder = DynamicBreadcrumbBuilder(cookbook) |
256 | - >>> builder.make_breadcrumb() |
257 | - <Breadcrumb |
258 | + >>> breadcrumb = DynamicBreadcrumb(cookbook) |
259 | + >>> breadcrumb |
260 | + <DynamicBreadcrumb |
261 | url='http://launchpad.dev/joy-of-cooking' |
262 | text='Joy of cooking'> |
263 | |
264 | @@ -221,7 +204,6 @@ |
265 | consistency across the site. |
266 | |
267 | >>> from canonical.launchpad.browser.launchpad import Hierarchy |
268 | - |
269 | >>> class CustomHierarchy(Hierarchy): |
270 | ... @property |
271 | ... def objects(self): |
272 | @@ -229,7 +211,7 @@ |
273 | |
274 | >>> spammy_hierarchy = CustomHierarchy(root, request) |
275 | >>> spammy_hierarchy.items() |
276 | - [<Breadcrumb |
277 | + [<TextualBreadcrumb |
278 | url='http://launchpad.dev/joy-of-cooking/spam' |
279 | text='Spam' |
280 | icon='<img src="/@@/recipe"/>'>] |
281 | @@ -276,7 +258,7 @@ |
282 | >>> breadcrumb_no_icon, breadcrumb_with_icon = hierarchy.items() |
283 | |
284 | >>> breadcrumb_no_icon |
285 | - <Breadcrumb |
286 | + <TextualBreadcrumb |
287 | url='http://launchpad.dev/joy-of-cooking' |
288 | text='Joy of cooking'> |
289 | |
290 | @@ -284,7 +266,7 @@ |
291 | False |
292 | |
293 | >>> breadcrumb_with_icon |
294 | - <Breadcrumb |
295 | + <TextualBreadcrumb |
296 | url='http://launchpad.dev/joy-of-cooking/spam' |
297 | text='Spam' |
298 | icon='<img src="/@@/recipe"/>'> |
299 | |
300 | === modified file 'lib/canonical/launchpad/webapp/breadcrumb.py' |
301 | --- lib/canonical/launchpad/webapp/breadcrumb.py 2009-08-17 20:20:17 +0000 |
302 | +++ lib/canonical/launchpad/webapp/breadcrumb.py 2009-08-24 17:47:34 +0000 |
303 | @@ -7,7 +7,6 @@ |
304 | |
305 | __all__ = [ |
306 | 'Breadcrumb', |
307 | - 'BreadcrumbBuilder', |
308 | ] |
309 | |
310 | |
311 | @@ -16,38 +15,15 @@ |
312 | from zope.interface import implements |
313 | |
314 | from canonical.launchpad.webapp import canonical_url |
315 | -from canonical.launchpad.webapp.interfaces import ( |
316 | - IBreadcrumb, IBreadcrumbBuilder) |
317 | +from canonical.launchpad.webapp.interfaces import IBreadcrumb |
318 | |
319 | |
320 | class Breadcrumb: |
321 | - """See `IBreadcrumb`.""" |
322 | - implements(IBreadcrumb) |
323 | - |
324 | - def __init__(self, url, text, icon=None): |
325 | - self.url = url |
326 | - self.text = text |
327 | - self.icon = icon |
328 | - |
329 | - def __repr__(self): |
330 | - if self.icon is not None: |
331 | - icon_repr = " icon='%s'" % self.icon |
332 | - else: |
333 | - icon_repr = "" |
334 | - |
335 | - return "<%s url='%s' text='%s'%s>" % ( |
336 | - self.__class__.__name__, self.url, self.text, icon_repr) |
337 | - |
338 | - |
339 | -# XXX: salgado, 2009-08-17: Since this adapter now provides a default |
340 | -# value for the 'url' attribute, we could easily convert it into an |
341 | -# adapter for IBreadcrumb, just changing the Hierarchy view. |
342 | -class BreadcrumbBuilder: |
343 | - """See `IBreadcrumbBuilder`. |
344 | + """See `IBreadcrumb`. |
345 | |
346 | This class is intended for use as an adapter. |
347 | """ |
348 | - implements(IBreadcrumbBuilder) |
349 | + implements(IBreadcrumb) |
350 | |
351 | rootsite = 'mainsite' |
352 | text = None |
353 | @@ -66,14 +42,11 @@ |
354 | return queryAdapter( |
355 | self.context, IPathAdapter, name='image').icon() |
356 | |
357 | - def make_breadcrumb(self): |
358 | - """See `IBreadcrumbBuilder.`""" |
359 | - if self.text is None: |
360 | - raise AssertionError( |
361 | - "The builder has not been given valid text for the " |
362 | - "breadcrumb.") |
363 | - if self.url is None: |
364 | - raise AssertionError( |
365 | - "The builder has not been given a valid breadcrumb URL.") |
366 | + def __repr__(self): |
367 | + if self.icon is not None: |
368 | + icon_repr = " icon='%s'" % self.icon |
369 | + else: |
370 | + icon_repr = "" |
371 | |
372 | - return Breadcrumb(self.url, self.text, icon=self.icon) |
373 | + return "<%s url='%s' text='%s'%s>" % ( |
374 | + self.__class__.__name__, self.url, self.text, icon_repr) |
375 | |
376 | === modified file 'lib/canonical/launchpad/webapp/interfaces.py' |
377 | --- lib/canonical/launchpad/webapp/interfaces.py 2009-08-18 14:36:16 +0000 |
378 | +++ lib/canonical/launchpad/webapp/interfaces.py 2009-08-24 17:47:34 +0000 |
379 | @@ -259,17 +259,6 @@ |
380 | icon = Attribute("An <img> tag showing this breadcrumb's 14x14 icon.") |
381 | |
382 | |
383 | -class IBreadcrumbBuilder(IBreadcrumb): |
384 | - """An object that builds `IBreadcrumb` objects.""" |
385 | - # We subclass IBreadcrumb to minimize interface drift. |
386 | - |
387 | - def make_breadcrumb(): |
388 | - """Return an object implementing the `IBreadcrumb` interface. |
389 | - |
390 | - If for any reason no IBreadcrumb object can be created, return None. |
391 | - """ |
392 | - |
393 | - |
394 | # |
395 | # Canonical URLs |
396 | # |
397 | |
398 | === modified file 'lib/canonical/launchpad/zcml/personproduct.zcml' |
399 | --- lib/canonical/launchpad/zcml/personproduct.zcml 2009-07-17 00:26:05 +0000 |
400 | +++ lib/canonical/launchpad/zcml/personproduct.zcml 2009-08-24 17:47:34 +0000 |
401 | @@ -28,9 +28,9 @@ |
402 | /> |
403 | |
404 | <adapter |
405 | - provides="canonical.launchpad.webapp.interfaces.IBreadcrumbBuilder" |
406 | + provides="canonical.launchpad.webapp.interfaces.IBreadcrumb" |
407 | for="canonical.launchpad.interfaces.personproduct.IPersonProduct" |
408 | - factory="canonical.launchpad.browser.personproduct.PersonProductBreadcrumbBuilder" |
409 | + factory="canonical.launchpad.browser.personproduct.PersonProductBreadcrumb" |
410 | permission="zope.Public" |
411 | /> |
412 | |
413 |
= Summary =
When I redesigned our breadcrumb infrastructure I made the
BreadcbrumbBuilder abstraction unnecessary, but my branch was already
too big so I left it to be removed later.
== Proposed fix ==
Remove BreadcrumbBuilder by directly adapting objects into IBreadcrumbs,
instead of adapting them into IBreadcrumbBuilder and calling
make_breadcrumb() on it.
In order to actually complete this I had to change all existing adapters
so that they inherit from Breadcrumb rather than BreadcrumbBuilder, but
that brought the diff to 1.4KLOC, so I'll ask for a review of that
later.
== Pre-implementation notes ==
== Implementation details ==
== Tests ==
./bin/test -vvt hierarchical- menu.txt
== 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: /launchpad/ webapp/ interfaces. py /launchpad/ browser/ launchpad. py /launchpad/ webapp/ breadcrumb. py /launchpad/ zcml/personprod uct.zcml /launchpad/ browser/ personproduct. py /launchpad/ doc/hierarchica l-menu. txt
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/canonical
== Pyflakes Doctest notices ==
lib/canonical/ launchpad/ doc/hierarchica l-menu. txt
144: 'canonical_url' imported but unused
This has been removed already.
== Pylint notices ==
lib/canonical/ launchpad/ browser/ launchpad. py
103: [F0401] Unable to import 'lazr.uri' (No module named uri)