~launchpad-pqm/launchpad/devel

11134.7.20 by Curtis Hovey
Hushed lint.
1
Hierarchical menus
2
==================
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
3
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
4
The location bar aids users in navigating the depths of Launchpad.  It
5
is built from a list of Breadcrumb objects collected during Zope's
6
object-traversal step.
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
7
11134.7.20 by Curtis Hovey
Hushed lint.
8
A simple object hierarchy
9
-------------------------
6767.5.13 by Maris Fogels
Wrote out the specification for the BreadcrumbBuilder class.
10
11
First, we need a hierarchy of objects to build upon:
6767.5.4 by Maris Fogels
Reorganized the new test structure a bit.
12
11134.7.20 by Curtis Hovey
Hushed lint.
13
    >>> from zope.component import getMultiAdapter, provideAdapter
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
14
    >>> from zope.interface import Interface, implements
15
16
    >>> class ICookbook(Interface):
17
    ...     """A cookbook for holding recipes."""
18
19
    >>> class IRecipe(Interface):
20
    ...     """A recipe in a cookbook."""
21
8003.1.1 by Celso Providelo
Ignoring path segments starting with '+' in the Hierarchical navigation model.
22
    >>> class ICooker(Interface):
23
    ...     """A cooker."""
24
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
25
    >>> from lp.services.webapp.interfaces import ICanonicalUrlData
26
    >>> from lp.services.webapp.url import urlappend
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
27
28
    >>> class BaseContent:
29
    ...     implements(ICanonicalUrlData)
30
    ...
8003.1.1 by Celso Providelo
Ignoring path segments starting with '+' in the Hierarchical navigation model.
31
    ...     def __init__(self, name, parent, path_prefix=None):
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
32
    ...         self.name = name
8003.1.1 by Celso Providelo
Ignoring path segments starting with '+' in the Hierarchical navigation model.
33
    ...         if path_prefix is not None:
8003.1.4 by Celso Providelo
applying review comments, r=intellectronica.
34
    ...             self.path = urlappend(path_prefix, name)
8003.1.1 by Celso Providelo
Ignoring path segments starting with '+' in the Hierarchical navigation model.
35
    ...         else:
36
    ...             self.path = name
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
37
    ...         self.inside = parent
38
    ...         self.rootsite = None
39
40
    >>> class Root(BaseContent):
41
    ...     """ The site root."""
42
43
    >>> class Cookbook(BaseContent):
44
    ...     implements(ICookbook)
45
46
    >>> class Recipe(BaseContent):
47
    ...     implements(IRecipe)
48
8003.1.1 by Celso Providelo
Ignoring path segments starting with '+' in the Hierarchical navigation model.
49
    >>> class Cooker(BaseContent):
50
    ...     implements(ICooker)
51
6767.5.13 by Maris Fogels
Wrote out the specification for the BreadcrumbBuilder class.
52
Today we'll be cooking with Spam!
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
53
54
    >>> root = Root('', None)
8003.1.1 by Celso Providelo
Ignoring path segments starting with '+' in the Hierarchical navigation model.
55
    >>> cooker = Cooker('jamie', root, '+cooker')
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
56
    >>> cookbook = Cookbook('joy-of-cooking', root)
57
    >>> recipe = Recipe('spam', cookbook)
6767.5.4 by Maris Fogels
Reorganized the new test structure a bit.
58
6767.5.13 by Maris Fogels
Wrote out the specification for the BreadcrumbBuilder class.
59
11134.7.20 by Curtis Hovey
Hushed lint.
60
Discovering breadcrumbs
61
-----------------------
6767.5.13 by Maris Fogels
Wrote out the specification for the BreadcrumbBuilder class.
62
63
The Hierarchy class builds the breadcrumbs by looking at each object in
64
the request.traversed_objects attribute.  If a traversed object can be
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
65
adapted to IBreadcrumb, then it is added to the breadcrumbs list.
6767.5.13 by Maris Fogels
Wrote out the specification for the BreadcrumbBuilder class.
66
6767.5.27 by Maris Fogels
Lint and comments.
67
We'll add the objects to the request's list of traversed objects so
6767.5.13 by Maris Fogels
Wrote out the specification for the BreadcrumbBuilder class.
68
the hierarchy will discover them.
69
14606.4.9 by William Grant
Merge canonical.lazr's menu testing stuff to lp.testing.menu.
70
    >>> from lp.testing.menu import make_fake_request
6767.5.5 by Maris Fogels
Updated the doctest with a sensible final form.
71
    >>> request = make_fake_request(
72
    ...     'http://launchpad.dev/joy-of-cooking/spam',
73
    ...     [root, cookbook, recipe])
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
74
6767.5.39 by Maris Fogels
Typo.
75
The hierarchy's list of breadcrumbs is empty since none of the objects
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
76
have an IBreadcrumb adapter.
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
77
6767.5.4 by Maris Fogels
Reorganized the new test structure a bit.
78
    >>> hierarchy = getMultiAdapter((recipe, request), name='+hierarchy')
9225.1.3 by Michael Nelson
New styled breadcrumbs.
79
    >>> hierarchy.items
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
80
    []
81
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
82
The ICookbook and IRecipe breadcrumb objects show up in the hierarchy after
83
IBreadcrumb adapters are registered for them.  The hierarchy builds a list of
84
breadcrumbs starting with the breadcrumb closest to the hierarchy root.
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
85
13130.1.1 by Curtis Hovey
Moved launchpad.py to lp.app.browser
86
    >>> from lp.app.browser.launchpad import Hierarchy
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
87
    >>> from lp.services.webapp.breadcrumb import Breadcrumb
9322.10.22 by Guilherme Salgado
Fix a bunch of tests and change makeBreadcrumbForRequestedPage() to use view.label before trying to look up a title in pagetitles.py
88
9322.10.18 by Guilherme Salgado
Fix a few tests
89
    # Monkey patch Hierarchy.makeBreadcrumbForRequestedPage so that we don't
90
    # have to create fake views and other stuff to test breadcrumbs here. The
91
    # functionality provided by that method is tested in
92
    # webapp/tests/test_breadcrumbs.py.
11889.4.16 by Tim Penhey
Fix the bad monkey patch.
93
    >>> make_breadcrumb_func = Hierarchy.makeBreadcrumbForRequestedPage
9322.10.18 by Guilherme Salgado
Fix a few tests
94
    >>> Hierarchy.makeBreadcrumbForRequestedPage = lambda self: None
6767.5.9 by Maris Fogels
Explicitly state and test the IBreadcrumb adapter interface contract.
95
6767.5.19 by Maris Fogels
Fixed the doctest and added some comments to the code.
96
    # Note that the Hierarchy assigns the breadcrumb's URL, but we need to
97
    # give it a valid .text attribute.
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
98
    >>> class TextualBreadcrumb(Breadcrumb):
6767.5.19 by Maris Fogels
Fixed the doctest and added some comments to the code.
99
    ...     @property
100
    ...     def text(self):
101
    ...         return self.context.name.capitalize().replace('-', ' ')
102
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
103
    >>> from lp.services.webapp.interfaces import IBreadcrumb
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
104
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
105
    >>> provideAdapter(TextualBreadcrumb, [ICookbook], IBreadcrumb)
106
    >>> provideAdapter(TextualBreadcrumb, [IRecipe], IBreadcrumb)
6767.5.16 by Maris Fogels
Changed the system a bit to use an IBreadcrumbBuilder interface.
107
9225.1.3 by Michael Nelson
New styled breadcrumbs.
108
    >>> hierarchy = getMultiAdapter((recipe, request), name='+hierarchy')
109
    >>> hierarchy.items
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
110
    [<TextualBreadcrumb
6767.5.5 by Maris Fogels
Updated the doctest with a sensible final form.
111
        url='http://launchpad.dev/joy-of-cooking'
6767.5.7 by Maris Fogels
Tweaked the test a bit.
112
        text='Joy of cooking'>,
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
113
     <TextualBreadcrumb
6767.5.5 by Maris Fogels
Updated the doctest with a sensible final form.
114
        url='http://launchpad.dev/joy-of-cooking/spam'
6767.5.7 by Maris Fogels
Tweaked the test a bit.
115
        text='Spam'>]
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
116
8003.1.1 by Celso Providelo
Ignoring path segments starting with '+' in the Hierarchical navigation model.
117
The ICooker object contains a path prefix, a segment of the path that
118
does not correspond to any object, it's only used to split traversal
119
domains. The `Hierarchy` model copes fine with objects like that.
120
121
    >>> cooker_request = make_fake_request(
122
    ...     'http://launchpad.dev/+cooker/jamie',
123
    ...     [root, cooker])
124
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
125
    >>> provideAdapter(TextualBreadcrumb, [ICooker], IBreadcrumb)
8003.1.1 by Celso Providelo
Ignoring path segments starting with '+' in the Hierarchical navigation model.
126
127
    >>> cooker_hierarchy = getMultiAdapter(
128
    ...     (cooker, cooker_request), name='+hierarchy')
9225.1.3 by Michael Nelson
New styled breadcrumbs.
129
    >>> cooker_hierarchy.items
11134.7.20 by Curtis Hovey
Hushed lint.
130
    [<TextualBreadcrumb url='.../+cooker/jamie' text='Jamie'>]
131
132
133
Displaying breadcrumbs
134
----------------------
9225.1.7 by Michael Nelson
Added doc for HierarchyView.display_breadcrumbs.
135
136
Breadcrumbs are only displayed if there is more than one breadcrumb, as
137
otherwise the breadcrumb will simply replicate the context.title heading
138
above it.
139
140
    >>> len(hierarchy.items)
141
    2
142
    >>> hierarchy.display_breadcrumbs
143
    True
144
145
    >>> len(cooker_hierarchy.items)
146
    1
147
    >>> cooker_hierarchy.display_breadcrumbs
148
    False
149
9511.2.7 by Michael Nelson
Fixed bug 433852
150
Additionally, if the view implements IMajorHeadingView then the breadcrumbs
151
will not be displayed.
152
153
    >>> ham_recipe = Recipe('ham', cookbook)
154
    >>> ham_request = make_fake_request(
155
    ...     'http://launchpad.dev/joy-of-cooking/ham',
156
    ...     [root, cookbook, ham_recipe])
157
158
    >>> ham_hierarchy = getMultiAdapter(
159
    ...     (ham_recipe, ham_request), name='+hierarchy')
160
    >>> hierarchy.display_breadcrumbs
161
    True
162
163
    >>> from zope.interface import alsoProvides
164
    >>> from lp.app.interfaces.headings import IMajorHeadingView
165
    >>> alsoProvides(ham_recipe, IMajorHeadingView)
166
    >>> ham_hierarchy.display_breadcrumbs
167
    False
168
9225.1.7 by Michael Nelson
Added doc for HierarchyView.display_breadcrumbs.
169
11134.7.20 by Curtis Hovey
Hushed lint.
170
Building IBreadcrumb objects
171
----------------------------
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
172
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
173
The construction of breadcrumb objects is handled by an IBreadcrumb adapter,
174
which adapts a context object and produces an IBreadcrumb object for that
175
context.  The default adapter provides the url attribute, but the breadcrumb's
176
text must be overriden in subclasses.
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
177
178
    >>> from zope.interface.verify import verifyObject
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
179
    >>> from lp.services.webapp.interfaces import IBreadcrumb
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
180
    >>> breadcrumb = Breadcrumb(cookbook)
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
181
    >>> verifyObject(IBreadcrumb, breadcrumb)
182
    True
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
183
    >>> print breadcrumb.text
184
    None
9678.12.2 by Guilherme Salgado
Revert previous change and fix the test instead
185
    >>> print breadcrumb.url
186
    http://launchpad.dev/joy-of-cooking
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
187
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
188
As said above, the breadcrumb's attributes can be overridden with subclassing
189
and Python properties.
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
190
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
191
    >>> class DynamicBreadcrumb(Breadcrumb):
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
192
    ...     @property
193
    ...     def text(self):
194
    ...         return self.context.name.capitalize().replace('-', ' ')
195
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
196
    >>> breadcrumb = DynamicBreadcrumb(cookbook)
197
    >>> breadcrumb
198
    <DynamicBreadcrumb
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
199
        url='http://launchpad.dev/joy-of-cooking'
200
        text='Joy of cooking'>
201
202
11134.7.20 by Curtis Hovey
Hushed lint.
203
Customizing the hierarchy
204
-------------------------
6767.5.21 by Maris Fogels
Changed the hierarchy interface a bit, so that breadcrumb construction is handed off to the class. This ensures consistency in presentation.
205
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
206
We can customize the hierarchy itself by changing the list of objects
207
and URLs that it uses to construct the breadcrumbs list.
6767.5.21 by Maris Fogels
Changed the hierarchy interface a bit, so that breadcrumb construction is handed off to the class. This ensures consistency in presentation.
208
6767.5.38 by Maris Fogels
Wording.
209
The Hierarchy object should *not* construct the Breadcrumb objects
210
itself.  It should let the IBreadcrumbBuilder handle it: this ensures
211
consistency across the site.
6767.5.21 by Maris Fogels
Changed the hierarchy interface a bit, so that breadcrumb construction is handed off to the class. This ensures consistency in presentation.
212
213
    >>> class CustomHierarchy(Hierarchy):
9087.4.4 by Guilherme Salgado
Fix one test
214
    ...     @property
215
    ...     def objects(self):
216
    ...         return [recipe]
6767.5.21 by Maris Fogels
Changed the hierarchy interface a bit, so that breadcrumb construction is handed off to the class. This ensures consistency in presentation.
217
218
    >>> spammy_hierarchy = CustomHierarchy(root, request)
9225.1.3 by Michael Nelson
New styled breadcrumbs.
219
    >>> spammy_hierarchy.items
9209.4.1 by Guilherme Salgado
Get rid of BreadcrumbBuilder as it's not necessary anymore
220
    [<TextualBreadcrumb
9087.4.4 by Guilherme Salgado
Fix one test
221
        url='http://launchpad.dev/joy-of-cooking/spam'
9494.1.1 by Guilherme Salgado
Remove icons from breadcrumbs
222
        text='Spam'>]
6767.5.21 by Maris Fogels
Changed the hierarchy interface a bit, so that breadcrumb construction is handed off to the class. This ensures consistency in presentation.
223
224
11134.7.20 by Curtis Hovey
Hushed lint.
225
Rendering the list
226
------------------
6767.5.2 by Maris Fogels
Roughed-in a hierarchial menus doctest.
227
6767.5.34 by Maris Fogels
Rework based on reviewer feedback.
228
The Hierarchy object is responsible for rendering the HTML for the
229
location bar.
6767.5.12 by Maris Fogels
Added a test to show regular and Launchpad homepage breadcrumb rendering.
230
231
    >>> from BeautifulSoup import BeautifulSoup
14578.4.1 by William Grant
Move the remains of canonical.launchpad.testing to lp.testing.
232
    >>> from lp.testing.pages import extract_text
6767.5.12 by Maris Fogels
Added a test to show regular and Launchpad homepage breadcrumb rendering.
233
14578.4.2 by William Grant
Fix a few stragglers.
234
    # Borrowed from lp.testing.pages.print_location()
6767.5.12 by Maris Fogels
Added a test to show regular and Launchpad homepage breadcrumb rendering.
235
    >>> def print_hierarchy(html):
236
    ...     soup = BeautifulSoup(html)
9225.1.3 by Michael Nelson
New styled breadcrumbs.
237
    ...     hierarchy = soup.find(attrs={'class': 'breadcrumbs'}).findAll(
6767.5.12 by Maris Fogels
Added a test to show regular and Launchpad homepage breadcrumb rendering.
238
    ...         recursive=False)
239
    ...     segments = [extract_text(step).encode('us-ascii', 'replace')
9225.1.6 by Michael Nelson
Updated the print_location test helper.
240
    ...                 for step in hierarchy]
9225.1.3 by Michael Nelson
New styled breadcrumbs.
241
    ...     print 'Location:', ' > '.join(segments)
6767.5.12 by Maris Fogels
Added a test to show regular and Launchpad homepage breadcrumb rendering.
242
10197.4.8 by Curtis Hovey
Fixed the breadcrumb markup.
243
    >>> markup = hierarchy.render()
244
    >>> print_hierarchy(markup)
6767.5.12 by Maris Fogels
Added a test to show regular and Launchpad homepage breadcrumb rendering.
245
    Location: Joy of cooking > Spam
246
10197.4.8 by Curtis Hovey
Fixed the breadcrumb markup.
247
The items in the breadcrumbs are linked, except for the last one which
248
represents the current location.
249
250
    >>> print markup
14325.2.8 by mbp at canonical
Update breadcrumb tests to expect schema.org metadata
251
    <ol itemprop="breadcrumb" class="breadcrumbs">
10197.4.8 by Curtis Hovey
Fixed the breadcrumb markup.
252
      <li>
10197.4.11 by Curtis Hovey
Removed leading white space from the breadcrumb links. Fixed the __all__ declarations
253
        <a href="http://launchpad.dev/joy-of-cooking">Joy of cooking</a>
10197.4.8 by Curtis Hovey
Fixed the breadcrumb markup.
254
      </li>
255
      <li>
256
          Spam
257
      </li>
258
    </ol>
259
9087.4.4 by Guilherme Salgado
Fix one test
260
The Launchpad Homepage displays no items in its location bar.  We are
6767.5.12 by Maris Fogels
Added a test to show regular and Launchpad homepage breadcrumb rendering.
261
considered to be on the home page if there are no breadcrumbs.
262
263
    # Simulate a visit to the site root
264
    >>> request = make_fake_request('http://launchpad.dev/', [root])
265
    >>> homepage_hierarchy = getMultiAdapter(
266
    ...     (root, request), name='+hierarchy')
267
9225.1.3 by Michael Nelson
New styled breadcrumbs.
268
    >>> homepage_hierarchy.items
6767.5.12 by Maris Fogels
Added a test to show regular and Launchpad homepage breadcrumb rendering.
269
    []
270
11134.7.19 by Curtis Hovey
Removed dubious tests...templates do not control the content of launchpad header;
271
    >>> homepage_hierarchy.render().strip()
11235.1.1 by Curtis Hovey
Updated test to verify the expected template content.
272
    u''
11889.4.16 by Tim Penhey
Fix the bad monkey patch.
273
274
275
Put the monkey patched method back.
276
277
    >>> Hierarchy.makeBreadcrumbForRequestedPage = make_breadcrumb_func