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

Proposed by Guilherme Salgado
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
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+10609@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Guilherme Salgado (salgado) wrote :

= 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:
  lib/canonical/launchpad/webapp/interfaces.py
  lib/canonical/launchpad/browser/launchpad.py
  lib/canonical/launchpad/webapp/breadcrumb.py
  lib/canonical/launchpad/zcml/personproduct.zcml
  lib/canonical/launchpad/browser/personproduct.py
  lib/canonical/launchpad/doc/hierarchical-menu.txt

== Pyflakes Doctest notices ==

lib/canonical/launchpad/doc/hierarchical-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)

Revision history for this message
Aaron Bentley (abentley) wrote :
Download full text (17.8 KiB)

-----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-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:
> lib/canonical/launchpad/webapp/interfaces.py
> lib/canonical/launchpad/browser/launchpad.py
> lib/canonical/launchpad/webapp/breadcrumb.py
> lib/canonical/launchpad/zcml/personproduct.zcml
> lib/canonical/launchpad/browser/personproduct.py
> lib/canonical/launchpad/doc/hierarchical-menu.txt
>
>
> == Pyflakes Doctest notices ==
>
> lib/canonical/launchpad/doc/hierarchical-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)
>
>
> === modified file 'lib/canonical/launchpad/browser/launchpad.py'
> --- lib/canonical/launchpad/browser/launchpad.py 2009-08-20 05:06:34 +0000
> +++ lib/canonical/launchpad/browser/launchpad.py 2009-08-24 17:47:34 +0000
> @@ -96,7 +96,7 @@
> StandardLaunchpadFacets, canonical_name, canonical_url, custom_widget,
> stepto)
> from canonical.launchpad.webapp.interfaces import (
> - IBreadcrumbBuilder, ILaunchBag, ILaunchpadRoot, INavigationMenu,
> + IBreadcrumb, ILaunchBag, ILaunchpadRoot, INavigationMenu,
> NotFoundError, POSTToNonCanonicalURL)
> from canonical.launchpad.webapp.publisher import RedirectionView
> from canonical.launchpad.webapp.authorization import check_permission
> @@ -226,39 +226,31 @@
> The list starts with the breadcrumb closest to the hierarchy root.
> """
> breadcrumbs = []
> - for builder in self._getBreadcrumbBuilders():
> - crumb = builder.make_breadcrumb()
> - if crumb is not None:
> - breadcrumbs.append(crumb)
> - return breadcrumbs
> -
> - def _getBreadcrumbBuilders(self):
> - builders = []
> for obj in self.objects:
> - builder = queryAdapter(obj, IBreadcrumbBuilder)
> -...

review: Approve
Revision history for this message
Guilherme Salgado (salgado) wrote :

Hi Aaron,

I did the change you suggested, but I need to cast the result of enumerate(breadcrumbs) into a list so that I can feed it to reversed().

Also, https://pastebin.canonical.com/21444/ has the remaining changes. It is huge but has just mechanical changes I did using sed. To test, './bin/test -vvt test_breadcrumbs'.

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