~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
= Facets and menus =

An object has a facetmenu if it can be adapted to IFacetMenu.


=== Construct an example url hierarchy ===

    >>> from lp.services.webapp import canonical_url
    >>> from lp.services.webapp.interfaces import (
    ...     IApplicationMenu, ICanonicalUrlData, IContextMenu, IFacetLink,
    ...     IFacetMenu, ILink, ILinkData)
    >>> from zope.interface import implements, directlyProvides, Interface
    >>> from zope.component import queryAdapter

    >>> class ObjectThatHasUrl:
    ...     implements(ICanonicalUrlData)
    ...     def __init__(self, name, parent):
    ...         self.name = name  # Used in examples later.
    ...         self.path = name  # See ICanonicalUrlData.
    ...         self.inside = parent  # See ICanonicalUrlData.
    ...         self.rootsite = None  # See ICanonicalUrlData.

Here's a useful class that we can use to effectively register adapters
for an object, without actually using adapter registries.  This comes
from PEP-246.

    >>> class Conformable:
    ...     adapt_to = None
    ...     def setAdapter(self, interface, object):
    ...         self.adapt_to = (interface, object)
    ...     def __conform__(self, protocol):
    ...         if self.adapt_to is not None:
    ...             interface, object = self.adapt_to
    ...             if interface.extends(protocol, strict=False):
    ...                 return object(self)
    ...         return None

    >>> class ExampleContentObject(ObjectThatHasUrl, Conformable):
    ...     pass

Let's make three objects that together make a url hierarchy:

  (the root)/sesamestreet/number73

    >>> root = ExampleContentObject('', None)
    >>> street = ExampleContentObject('sesamestreet', root)
    >>> house = ExampleContentObject('number73', street)
    >>> canonical_url(house)
    u'http://launchpad.dev/sesamestreet/number73'

    >>> canonical_url(street)
    u'http://launchpad.dev/sesamestreet'


== The Link class ==

When you define a menu using class based on FacetMenu, you return Link
objects from its methods.  The Link class is very simple.  You provide
it with a target and some text for the link.  Other optional arguments
are some summary text, the name of an icon, and a boolean saying whether
the link is enabled.  Defaults are to have no summary, no icon, and to
be enabled.

Exactly how a disabled link appears on a page is up to the page template
code that renders links.  Typically, disabled links will be hidden, or
may be shown "greyed out" and not clickable.

These are the attributes that Link instances provide for setting by the
software developer who defines menus.  When you define a Link, you are
defining an object that provides ILinkData.

There are other attributes that are set by the menu infrastructure, and
are used when a Link is rendered in a page.  These attributes are
defined in ILink and IFacetLink.

    >>> from lp.services.webapp import Link
    >>> no_summary_link = Link('target', 'text -->')
    >>> ILinkData.providedBy(no_summary_link)
    True

    >>> no_summary_link.target
    'target'

    >>> no_summary_link.text
    'text -->'

    >>> print no_summary_link.summary
    None

    >>> print no_summary_link.icon
    None

    >>> no_summary_link.enabled
    True

If we want to include text with markup in the text of a link, we need to
mark is as being "structured".

    >>> from lp.services.webapp import structured
    >>> text = structured('some <b>%s</b> text', ' --> ')

    >>> full_link = Link('target', text, 'summary', icon='icon', enabled=False)
    >>> ILinkData.providedBy(full_link)
    True

    >>> full_link.target
    'target'

    >>> full_link.text
    <structured-string 'some <b>%s</b> text'>

    >>> full_link.summary
    'summary'

    >>> full_link.icon
    'icon'

    >>> full_link.enabled
    False

The menu infrastructure adapts a Link instance to ILink (for context and
application menus) and IFacetLink (for facet menus).  This (morally
speaking) allows the menu infrastructure to set the 'name', 'url',
'linked' and (in the case of IFacetLink) 'selected' attributes of the
link.

By using separate interfaces, and by adapting, we keep the different
responsibilities of links clearly separated, and leave room for making
clean changes and optimisations later.

Let's make a couple of Link instances, adapt them to ILink and
IFacetLink, and show that getting and setting the appropriate attributes
works.

We use two separate links because setting attributes on the adapted link
will change the data in the underlying link instance.  We may change
this later, to allow some links to be precomputed or shared, but right
now, link instances are meant to be created freshly on each request, and
not shared or reused.

    >>> link1 = Link('target', 'text', 'summary', icon='icon', enabled=False)
    >>> link2 = Link('target', 'text', 'summary', icon='icon', enabled=False)

    >>> for menu_link in ILink(link1), IFacetLink(link2):
    ...     print menu_link.name, menu_link.url, menu_link.linked
    ...     menu_link.name = 'name'
    ...     menu_link.url = 'url'
    ...     menu_link.linked = False
    ...     print menu_link.name, menu_link.url, menu_link.linked
    None None True
    name url False
    None None True
    name url False

    >>> facet_menu_link = IFacetLink(link2)
    >>> facet_menu_link.selected
    False

    >>> facet_menu_link.selected = True
    >>> facet_menu_link.selected
    True


== The FacetMenu class ==

    >>> from lp.services.webapp import FacetMenu

FacetMenu is meant to be used as a base-class for writing your own
IFacetMenu classes.  Here's what happens when you use it on its own.

    >>> bad_idea_menu = FacetMenu(object())
    >>> for link in bad_idea_menu.iterlinks():
    ...     pass
    Traceback (most recent call last):
    ...
    AssertionError: Subclasses of FacetMenu must provide self.links

So, we must test FacetMenu by making our own menu subclass.  We'll just
call our menu 'Facets'.  One thing missing from this class is the
'usedfor' declaration.  We need to use this in practice because we need
to know how to register our menu as an adapter.  For this part of the
test, we won't worry about that.

The FacetMenu class also allows you to specify the 'defaultlink'
attribute, which is None by default (for no default link), but can be
the name of the default link for this menu.

    >>> class Facets(FacetMenu):
    ...     links = ['foo', 'bar']
    ...
    ...     def foo(self):
    ...         target = '+foo'
    ...         text = 'Foo'
    ...         return Link(target, text)
    ...
    ...     def bar(self):
    ...         target = '+bar'
    ...         text = 'Bar'
    ...         summary = (
    ...             'More explanation about Bar of %s' % self.context.name)
    ...         return Link(target, text, summary)

Now, we can make an instance of this Facets class, with a contextobject
to show that its methods can access `self.context`.

    >>> facetmenu = Facets(street)

We can go through each attribute of each of the links, checking that
they are as we expect.

    >>> for link in facetmenu.iterlinks():
    ...     print '--- link %s ---' % link.name
    ...     for attrname in sorted(IFacetLink.names(all=True)):
    ...         print '%s: %s' % (attrname, getattr(link, attrname))
    --- link foo ---
    enabled: True
    escapedtext: Foo
    hidden: False
    icon: None
    icon_url: None
    linked: True
    menu: None
    name: foo
    path: /sesamestreet/+foo
    render: <bound method FacetLink.render ...>
    selected: False
    site: None
    sort_key: 0
    summary: None
    target: +foo
    text: Foo
    url: http://launchpad.dev/sesamestreet/+foo
    --- link bar ---
    enabled: True
    escapedtext: Bar
    hidden: False
    icon: None
    icon_url: None
    linked: True
    menu: None
    name: bar
    path: /sesamestreet/+bar
    render: <bound method FacetLink.render ...>
    selected: False
    site: None
    sort_key: 1
    summary: More explanation about Bar of sesamestreet
    target: +bar
    text: Bar
    url: http://launchpad.dev/sesamestreet/+bar


== The MenuLink and FacetLink adapter classes ==

The menus system needs to be able to adapt ILinkData objects to objects
that provide ILink or IFacetLink.  The menus system needs to be able to
set the 'enabled', 'name', 'url', 'linked, and for IFacetLink,
'selected' attributes, but without altering the underlying ILinkData
object.  ILink objects also provide an HTML-escaped version of the link
text, and a render() method for returning the link as HTML.

    >>> somelink = Link('target', 'text', 'summary', icon='icon')
    >>> ILinkData.providedBy(somelink)
    True

    >>> ILink.providedBy(somelink)
    False

    >>> adaptedtolink = ILink(somelink)
    >>> ILink.providedBy(adaptedtolink)
    True

It is important we don't get a facetlink here, so that we can do views
on links if we want to, and have distinct views on facet and other
links.

    >>> IFacetLink.providedBy(adaptedtolink)
    False

    >>> adaptedtolink.target
    'target'

    >>> adaptedtolink.text
    'text'

    >>> adaptedtolink.summary
    'summary'

    >>> adaptedtolink.icon
    'icon'

    >>> adaptedtolink.enabled
    True

    >>> adaptedtolink.enabled = False
    >>> adaptedtolink.enabled
    False

    >>> somelink.enabled
    True

Checking out the escapedtext attribute.

    >>> link = Link('+target', 'text -->')

    >>> ILink(link).escapedtext
    'text --&gt;'

    >>> IFacetLink(link).escapedtext
    'text --&gt;'

    >>> text = structured('some <b> %s </b> text', '-->')
    >>> link = Link('+target', text)

    >>> ILink(link).escapedtext
    u'some <b> --&gt; </b> text'

    >>> IFacetLink(link).escapedtext
    u'some <b> --&gt; </b> text'

We can test the 'structured' helper directly.

    >>> structured('foo').escapedtext
    u'foo'

    >>> structured('%s', 'a < b > c & d " e').escapedtext
    u'a &lt; b &gt; c &amp; d " e'

    >>> structured('a < b > c & d " e').escapedtext
    u'a < b > c & d " e'

    >>> structured(
    ...     'replacement %(foo)s, %(bar)s', foo='<foo>', bar='bar').escapedtext
    u'replacement &lt;foo&gt;, bar'

    >>> structured('whatever', 'foo', 'bar', baz='baz')
    Traceback (most recent call last):
    ...
    TypeError:...

Next, we return the link as HTML.

    # We need to use a real launchpad test request so the view adapter
    # lookups will work.  That request also needs to implement
    # IParticipation so that the login machinery will work.
    >>> from zope.security.interfaces import IParticipation
    >>> from lp.services.webapp.servers import LaunchpadTestRequest
    >>> class InteractiveTestRequest(LaunchpadTestRequest):
    ...     implements(IParticipation)
    ...     principal = None
    ...     interaction = None
    >>> request = InteractiveTestRequest()
    >>> login(ANONYMOUS, request)

    >>> link = Link('+target', 'text-->', 'summary', icon='icon')
    >>> print ILink(link).render() #doctest: +NORMALIZE_WHITESPACE
    <a class="menu-link-None sprite icon" title="summary">text--&gt;</a>

    # Clean up our special login.
    >>> login(ANONYMOUS)

A menu item can be marked as hidden even though it is enabled.

    >>> link = Link('z', 'text', 'summary', icon='icon', hidden=True)
    >>> print ILink(link).render() #doctest: +NORMALIZE_WHITESPACE
    <a class="menu-link-None sprite icon invisible-link"
        title="summary">text</a>


== How do we tell which link from a facetmenu is the selected one? ==

A link will be selected if its name is passed to the facet menu's
iterlinks method, or otherwise, if its name is the defaultlink.

    >>> for link in facetmenu.iterlinks(selectedfacetname='bar'):
    ...     print '--- link %s ---' % link.name
    ...     print 'selected:', link.selected
    --- link foo ---
    selected: False
    --- link bar ---
    selected: True

When a link name is passed in, but no link of that name is in the menu,
it is not an error.  No link is selected.

    >>> for link in facetmenu.iterlinks(selectedfacetname='nosuchname'):
    ...     print '--- link %s ---' % link.name
    ...     print 'selected:', link.selected
    --- link foo ---
    selected: False
    --- link bar ---
    selected: False

No selected link is given, but the default is 'foo', so 'foo' will be
selected.

    >>> facetmenu.defaultlink = 'foo'
    >>> for link in facetmenu.iterlinks():
    ...     print '--- link %s ---' % link.name
    ...     print 'selected:', link.selected
    --- link foo ---
    selected: True
    --- link bar ---
    selected: False

Now, 'foo' is still the default, but 'bar' has been selected.  So only
'bar' will be selected.

    >>> for link in facetmenu.iterlinks(selectedfacetname='bar'):
    ...     print '--- link %s ---' % link.name
    ...     print 'selected:', link.selected
    --- link foo ---
    selected: False
    --- link bar ---
    selected: True

We still have 'foo' as the default.  This time, 'nosuchlink' has been
selected. As there is no such link, nothing will be selected.

    >>> for link in facetmenu.iterlinks(selectedfacetname='nosuchlink'):
    ...     print '--- link %s ---' % link.name
    ...     print 'selected:', link.selected
    --- link foo ---
    selected: False
    --- link bar ---
    selected: False


== Application Menus ==

Application menus are defined for a context object for a particular
named Facet menu item.  The name of the facet menu item used is
whichever facet is selected from the nearest context object that has an
IFacetMenu adapter.

Defining an ApplicationMenu works like defining a FacetMenu, except we
also need to say what facet menu item it is for.

    >>> from lp.services.webapp import ApplicationMenu

ApplicationMenu is meant to be used as a base-class for writing your own
IApplicationMenu classes.  Here's what happens when you use it on its
own.

    >>> bad_idea_menu = ApplicationMenu(object())
    >>> for link in bad_idea_menu.iterlinks():
    ...     pass
    Traceback (most recent call last):
    ...
    AssertionError: Subclasses of ApplicationMenu must provide self.links

So, we must test ApplicationMenu by making our own menu subclass.  We'll
just call our menu 'FooApplicationMenu' as we intend it to be used when
the 'foo' facet is selected.  Two things missing from this class are the
'usedfor' declaration and the 'facet' declaration.  We need to use these
in practice because we need to know how to register our menu as an
adapter.  For this part of the test, we won't worry about that.

    >>> class FooApplicationMenu(ApplicationMenu):
    ...
    ...     links = ['first']
    ...     facet = 'foo'
    ...
    ...     def first(self):
    ...         target = '+first'
    ...         text = 'First menu'
    ...         return Link(target, text)

Now, we can make an instance of this FooApplicationMenu class.  We
should really be using some link text that shows that its methods can
access `self.context`. That's a minor TODO item.

    >>> housefooappmenu = FooApplicationMenu(house)

We can go through each attribute of each of the links, checking that
they are as we expect.

    >>> for link in housefooappmenu.iterlinks():
    ...     print '--- link %s ---' % link.name
    ...     for attrname in sorted(ILink.names(all=True)):
    ...         print '%s: %s' % (attrname, getattr(link, attrname))
    --- link first ---
    enabled: True
    escapedtext: First menu
    hidden: False
    icon: None
    icon_url: None
    linked: True
    menu: None
    name: first
    path: /sesamestreet/number73/+first
    render: <bound method MenuLink.render ...>
    site: None
    sort_key: 0
    summary: None
    target: +first
    text: First menu
    url: http://launchpad.dev/sesamestreet/number73/+first


== Context Menus ==

Context menus are defined for a context object.  Each context object has
just one context menu, and it is available at all times.

Defining a ContextMenu works like defining a FacetMenu.

    >>> from lp.services.webapp import ContextMenu

ContextMenu is meant to be used as a base-class for writing your own
IContextMenu classes.  Here's what happens when you use it on its own.

    >>> bad_idea_menu = ContextMenu(object())
    >>> for link in bad_idea_menu.iterlinks():
    ...     pass
    Traceback (most recent call last):
    ...
    AssertionError: Subclasses of ContextMenu must provide self.links

So, we must test ContextMenu by making our own menu subclass.  We'll
just call our menu 'MyContextMenu'.

One thing missing from this class is the 'usedfor' declaration, which
tells the registration machinery how to render this menu as an adapter.
For this part of the test, we won't worry about that.

    >>> class MyContextMenu(ContextMenu):
    ...
    ...     links = ['first']
    ...
    ...     def first(self):
    ...         target = '+firstcontext'
    ...         text = 'First context menu item'
    ...         return Link(target, text)

Now, we can make an instance of this MyContextMenu class.  We should
really be using some link text that shows that its methods can access
`self.context`. That's a minor TODO item.

    >>> housefoocontextmenu = MyContextMenu(house)

We can go through each attribute of each of the links, checking that
they are as we expect.

    >>> for link in housefoocontextmenu.iterlinks():
    ...     print '--- link %s ---' % link.name
    ...     for attrname in sorted(ILink.names(all=True)):
    ...         print '%s: %s' % (attrname, getattr(link, attrname))
    --- link first ---
    enabled: True
    escapedtext: First context menu item
    hidden: False
    icon: None
    icon_url: None
    linked: True
    menu: None
    name: first
    path: /sesamestreet/number73/+firstcontext
    render: <bound method MenuLink.render ...>
    site: None
    sort_key: 0
    summary: None
    target: +firstcontext
    text: First context menu item
    url: http://launchpad.dev/sesamestreet/number73/+firstcontext


== Registering menus in ZCML ==

First, we define a couple of interfaces, and put them in the
lp.testing module.

    >>> class IThingHavingFacets(Interface):
    ...     pass
    >>> import lp.testing
    >>> getattr(lp.testing, 'IThingHavingFacets', None) is None
    True

    >>> lp.testing.IThingHavingFacets = IThingHavingFacets
    >>> IThingHavingFacets.__module__ = 'lp.testing'

    >>> class IThingHavingMenus(Interface):
    ...     pass
    >>> import lp.testing
    >>> getattr(lp.testing, 'IThingHavingMenus', None) is None
    True

    >>> lp.testing.IThingHavingMenus = IThingHavingMenus
    >>> IThingHavingMenus.__module__ = 'lp.testing'

Next, we define a FacetMenu subclass to be used for IThingHavingFacets,
using a usedfor class attribute to say what interface it is to be
registered for, and put it too in the lp.testing module.

    >>> class FacetsForThing(Facets):
    ...     usedfor = IThingHavingFacets
    ...
    ...     links = ['foo', 'bar', 'baz']
    ...
    ...     def baz(self):
    ...         target = ''
    ...         text = 'baz'
    ...         if self.request is None:
    ...             summary = "No request available"
    ...         else:
    ...             summary = self.request.method
    ...         return Link(target, text, summary=summary)

    >>> getattr(lp.testing, 'FacetsForThing', None) is None
    True

    >>> lp.testing.FacetsForThing = FacetsForThing

And likewise for an application menu registered for IThingHavingMenus.

    >>> class FooMenuForThing(FooApplicationMenu):
    ...     usedfor = IThingHavingMenus
    ...     facet = 'foo'

    >>> getattr(lp.testing, 'FooMenuForThing', None) is None
    True

    >>> lp.testing.FooMenuForThing = FooMenuForThing

We do the same for a context menu.

    >>> class ContextMenuForThing(MyContextMenu):
    ...     usedfor = IThingHavingMenus

    >>> print getattr(lp.testing, 'ContextMenuForThing', None)
    None

    >>> lp.testing.ContextMenuForThing = ContextMenuForThing

Now, check that we have no IFacetMenu adapter for an IThingHavingFacets
object.

    >>> class SomeThing:
    ...     implements(IThingHavingFacets)
    >>> something_with_facets = SomeThing()
    >>> IFacetMenu(something_with_facets, None) is None
    True

We also need to check that we have no IApplicationMenu adapter named
'foo' for an IThingHavingMenus object.

    >>> class SomeOtherThing:
    ...     implements(IThingHavingMenus)
    >>> something_with_menus = SomeOtherThing()
    >>> print queryAdapter(something_with_menus, IApplicationMenu, 'foo')
    None

Same for an IContextMenu adapter.

    >>> print queryAdapter(something_with_menus, IContextMenu, 'foo')
    None

    >>> from zope.configuration import xmlconfig
    >>> zcmlcontext = xmlconfig.string("""
    ... <configure xmlns:browser="http://namespaces.zope.org/browser">
    ...   <include file="lib/lp/services/webapp/meta.zcml" />
    ...   <browser:menus
    ...       module="lp.testing"
    ...       classes="FacetsForThing FooMenuForThing ContextMenuForThing"
    ...       />
    ... </configure>
    ... """)

    >>> menu1 = IFacetMenu(something_with_facets)
    >>> menu1.context = something_with_facets
    >>> menu1.__class__ is FacetsForThing
    True

    >>> menu2 = queryAdapter(something_with_menus, IApplicationMenu, 'foo')
    >>> menu2.context = something_with_menus
    >>> menu2.__class__ is FooMenuForThing
    True

    >>> menu3 = IContextMenu(something_with_menus)
    >>> menu3.context = something_with_menus
    >>> menu3.__class__ is ContextMenuForThing
    True

The browser:menus directive also makes security declarations for the
adapters.


== Using menus in page templates ==

We use menus in page templates by using the `thing/menu:typeofmenu`
TALES namespace.

First, let's look at `thing/menu:facet`.  What this does is to look up
nearest_adapter(thing, IFacetMenu), getting an IFacetMenu adapter from
it. Then, it gets the request from either the view or the current
interaction, and calculates a request Url object to pass into
IMenu.iterlinks, so that it can properly decide whether a particular
link should appear linked. The request is also set as the menu's
'request' attribute.

    >>> from zope.publisher.interfaces.browser import IBrowserRequest
    >>> from zope.publisher.interfaces.http import IHTTPApplicationRequest
    >>> from lp.testing import test_tales
    >>> from lp.services.webapp import LaunchpadView
    >>> from lp.services.webapp.vhosts import allvhosts
    >>> class FakeRequest:
    ...     implements(IHTTPApplicationRequest, IBrowserRequest)
    ...
    ...     interaction = None
    ...
    ...     def __init__(self, url, query=None, url1=None):
    ...         self.url = url
    ...         self.query = query
    ...         self.url1 = url1  # returned from getURL(1)
    ...         self.method = 'GET'
    ...         self.annotations = {}
    ...
    ...     def getURL(self, level=0):
    ...         assert 0 <= level <=1, 'level must be 0 or 1'
    ...         if level == 0:
    ...             return self.url
    ...         else:
    ...             assert self.url1 is not None, (
    ...                 'Must set url1 in FakeRequest')
    ...             return self.url1
    ...
    ...     def getRootURL(self, rootsite):
    ...         if rootsite is not None:
    ...             return allvhosts.configs[rootsite].rooturl
    ...         else:
    ...             return self.getApplicationURL() + '/'
    ...
    ...     def getApplicationURL(self):
    ...         # Just the http://place:port part, so stop at the 3rd slash.
    ...         return '/'.join(self.url.split('/', 3)[:3])
    ...
    ...     def get(self, key, default=None):
    ...         assert key == 'QUERY_STRING', 'we handle only QUERY_STRING'
    ...         if self.query is None:
    ...             return default
    ...         else:
    ...             return self.query
    ...
    ...     def setPrincipal(self, principal):
    ...         self.principal = principal
    >>> request = FakeRequest('http://launchpad.dev/sesamestreet/+bar')
    >>> view = LaunchpadView(house, request)
    >>> view.__launchpad_facetname__ = 'bar'

    >>> street.adapt_to = None
    >>> directlyProvides(street, IThingHavingFacets)
    >>> house.adapt_to = None
    >>> directlyProvides(house, IThingHavingMenus)

    >>> links = test_tales('view/menu:facet', view=view)

    >>> for link in links:
    ...     print link.url, link.selected, link.linked, link.summary
    http://launchpad.dev/sesamestreet/+foo False True None
    http://launchpad.dev/sesamestreet/+bar True False More explanation about
                                                      Bar of sesamestreet
    http://launchpad.dev/sesamestreet False True GET

So, the first link '+foo' is linked, but the second '+bar' is not.  This
is because the URL of '+bar' is the same as the request in the view.

Let's try again, this time with a request from the participation.

    >>> participation = FakeRequest(
    ...     'http://launchpad.dev/sesamestreet/+bar')
    >>> login(ANONYMOUS, participation)

    >>> links = test_tales('context/menu:facet', context=house)
    >>> for link in links:
    ...     print link.url, link.selected, link.linked
    http://launchpad.dev/sesamestreet/+foo False True
    http://launchpad.dev/sesamestreet/+bar False False
    http://launchpad.dev/sesamestreet False True

Note that '+bar' is not selected.  This is because we're adapting
'context' and not 'view', so the menus system has no way of knowing what
the selected facet is for the current page.

Sometimes, we need to take into account the default view name for an
object. Let's say that the default view name for an IStreet is '+baz'.
This is the common case where the overview link is the default view
name.

    >>> class IStreet(Interface):
    ...     """A street."""
    >>> directlyProvides(street, IStreet, IThingHavingFacets)
    >>> from zope.app.testing import ztapi
    >>> ztapi.setDefaultViewName(IStreet, '+baz', None)

    >>> request = FakeRequest(
    ...     'http://launchpad.dev/sesamestreet/+baz',
    ...     url1='http://launchpad.dev/sesamestreet/')

    >>> from zope.app.zapi import getDefaultViewName
    >>> getDefaultViewName(street, request)
    '+baz'

So, in this example, the last link should not be 'linked' because it is
equivalent to the default view name for a street.  The TALES
infrastructure actually calculates a shortened URL for this case.

    >>> view = LaunchpadView(street, request)
    >>> view.__launchpad_facetname__ = 'bar'
    >>> links = test_tales('view/menu:facet', view=view)
    >>> for link in links:
    ...     print link.url, link.linked
    http://launchpad.dev/sesamestreet/+foo True
    http://launchpad.dev/sesamestreet/+bar True
    http://launchpad.dev/sesamestreet False

You can traverse to an individual menu item from the facet menu:

    >>> view = LaunchpadView(house, request)
    >>> view.__launchpad_facetname__ = 'bar'
    >>> link = test_tales('view/menu:foo/first', view=view, request=request)
    >>> print link.url
    http://launchpad.dev/sesamestreet/number73/+first

But if a non-existing entry is requested, a KeyError is raised:

    >>> test_tales('view/menu:foo/broken', view=view)
    Traceback (most recent call last):
    ...
    KeyError: 'broken'

We also report when the selected facet does not exist with a
LocationError exception:

    >>> test_tales('view/menu:broken/bar', view=view)
    Traceback (most recent call last):
    ...
    LocationError: ..., 'broken')

We can also get a context menu as menu:context.  It makes no difference
whether the TALES code is view/menu:context or context/menu:context,
because the menus system doesn't need to know anything about the view
object.

    >>> links = test_tales('view/menu:context', view=view)
    >>> for link in links.values():
    ...     print link.url
    http://launchpad.dev/sesamestreet/number73/+firstcontext

The link is also reachable by name:

    >>> link = test_tales('context/menu:context/first', context=house)
    >>> print link.url
    http://launchpad.dev/sesamestreet/number73/+firstcontext

When there is no menu for a thing, we get an empty iterator.

    >>> view = LaunchpadView(root, request)
    >>> view.__launchpad_facetname__ = 'bar'
    >>> menu = test_tales('view/menu:facet', view=view)
    >>> list(menu)
    []

    >>> menu = test_tales('view/menu:context', view=view)
    >>> list(menu)
    []

And thus, we don't have a facet to navigate to:

    >>> test_tales('view/menu:foo/+first', view=view)
    Traceback (most recent call last):
    ...
    LocationError: ..., 'foo')

    >>> view = LaunchpadView(house, request)
    >>> view.__launchpad_facetname__ = 'bar'


== Shortcuts for rendering menu items ==

A thing's menu may be rendered directly as HTML using the menu link's
render() method.

    >>> request = InteractiveTestRequest()
    >>> login(ANONYMOUS, request)

    >>> html = test_tales('context/menu:foo/first/render',
    ...                   context=house, view=view, request=request)
    >>> print html #doctest: +NORMALIZE_WHITESPACE
    <a...href="http://127.0.0.1/sesamestreet/number73/+first"
    ...class="menu-link-first">First menu</a>

    # Clean up our special login.

    >>> login(ANONYMOUS)


== Cleaning up ==

We're done testing the zcml, so we can clean up the
lp.testing module.

    >>> del lp.testing.FacetsForThing
    >>> del lp.testing.FooMenuForThing
    >>> del lp.testing.ContextMenuForThing
    >>> del lp.testing.IThingHavingFacets
    >>> del lp.testing.IThingHavingMenus


== The enabled_with_permission function decorator ==

If you have a menu item that should be enabled only when the current
logged-in user has a particular permission, then you can write the link
as usual, and use the enabled_with_permission function decorator.

It works like this:

    >>> from lp.services.webapp import enabled_with_permission

    >>> class SomeMenu(ContextMenu):
    ...
    ...     @enabled_with_permission('launchpad.Admin')
    ...     def foo(self):
    ...         return Link('+admin', 'Admin the foo')

    >>> somemenu = SomeMenu(street)

If we're logged in as an anonymous user, then the link will be disabled.

    >>> login(ANONYMOUS)

    >>> foolink = somemenu.foo()
    >>> foolink.text
    'Admin the foo'

    >>> foolink.enabled
    False

Now, we log in as foo.bar@canonical.com, an admin.

    >>> login('foo.bar@canonical.com')
    >>> foolink = somemenu.foo()
    >>> foolink.enabled
    True