Merge lp:~sinzui/launchpad/pageids into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 16028
Proposed branch: lp:~sinzui/launchpad/pageids
Merge into: lp:launchpad
Diff against target: 209 lines (+81/-44)
5 files modified
lib/lp/services/inlinehelp/README.txt (+13/-7)
lib/lp/services/inlinehelp/zcml.py (+2/-1)
lib/lp/services/webapp/doc/webapp-publication.txt (+0/-33)
lib/lp/services/webapp/publication.py (+12/-3)
lib/lp/services/webapp/tests/test_pageid.py (+54/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/pageids
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+126081@code.launchpad.net

Commit message

Make sensible pageids for help folders and ++model++.

Description of the change

The page performance report cannot handle Page-Ids with random/changing information like revisionids in URLs. Launchpad's
standard views always provide sensible __name__ and __class__.__name__ attrs
to construct them. Two cases where views are dynamically generated for
templates and resources do not provide the expected attributes, or the
code does not know how to find the data.

--------------------------------------------------------------------
RULES

    Pre-implementation: no one
    * HelpFolders are missing the __name__ and that is trivial to register
      with the dynamic view class.
    * The ++model++ example is a case were the context is also a view, and
      it is dynamically generated from a page template. Either the
      registration can modified, or constructPageID() must learn how to
      handle the recursive issue.
      * Handling at least one-level of recursion will prevent future breaks
        for future dynamic view classes from happening.

QA

    * Visit https://blueprints.qastaging.launchpad.net/+help-blueprints/workitems-help.html/++profile++show
    * Verify the Page-Id is just "RootObject:+help-blueprints", not dir
      path
    * Visit https://bugs.qastaging.launchpad.net/ubuntu/+bugs/++model++
    * Expect a timeout, view the oops.
    * Verify the Page-id is Distribution:+bugs:JsonModelNamespaceView,
      no path to the template.

LINT

    lib/lp/services/inlinehelp/README.txt
    lib/lp/services/inlinehelp/zcml.py
    lib/lp/services/webapp/publication.py
    lib/lp/services/webapp/doc/webapp-publication.txt
    lib/lp/services/webapp/tests/test_pageid.py

LoC:
    This add about 40 lines of code, but I have a 3000+ line credit.

TEST

    ./bin/test -vvc -t README lp.services.inlinehelp
    ./bin/test -vvc -t PageID lp.services.webapp.tests.test_pageid
    ./bin/test -vvc -t webapp-publication.txt lp.services.webapp.tests.test_doc

IMPLEMENTATION

Use the name from the zcml as the help folder's __name__ attr.
    lib/lp/services/inlinehelp/README.txt
    lib/lp/services/inlinehelp/zcml.py

Recuse through constructPageID() when the context is a dynamic view
created from a page template. I converted from doctests to unittests.
    lib/lp/services/webapp/publication.py
    lib/lp/services/webapp/doc/webapp-publication.txt
    lib/lp/services/webapp/tests/test_pageid.py

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

Looks good. A couple of minor comments:

As a reader of the code, I would appreciate a comment above line 122 of the
diff describing the significance of having a space in the context name.

Line 122-124 of the diff contains the comment

# This is a view of a generated view class,
# such as ++model++ view of Product:+bugs. Recuse!

Is "Recuse" a typo? It seems to me that it should be "recurse".

I suppose the function is recusing itself from working further, but that's a
stretch. :)

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/services/inlinehelp/README.txt'
2--- lib/lp/services/inlinehelp/README.txt 2011-12-30 09:00:06 +0000
3+++ lib/lp/services/inlinehelp/README.txt 2012-09-25 13:57:35 +0000
4@@ -45,13 +45,19 @@
5
6 >>> from zope.component import queryMultiAdapter
7 >>> from lp.services.webapp.publisher import rootObject
8- >>> help = queryMultiAdapter((rootObject, request), name="+help")
9-
10- >>> help.folder == help_folder
11- True
12-
13- >>> isinstance(help, HelpFolder)
14- True
15+ >>> help_view = queryMultiAdapter((rootObject, request), name="+help")
16+
17+ >>> help_view.folder == help_folder
18+ True
19+
20+ >>> isinstance(help_view, HelpFolder)
21+ True
22+
23+ >>> print help_view.__name__
24+ +help
25+
26+ >>> print help_view.__class__.__name__
27+ +help for /tmp/help...
28
29
30 Cleanup
31
32=== modified file 'lib/lp/services/inlinehelp/zcml.py'
33--- lib/lp/services/inlinehelp/zcml.py 2012-01-01 02:58:52 +0000
34+++ lib/lp/services/inlinehelp/zcml.py 2012-09-25 13:57:35 +0000
35@@ -37,7 +37,8 @@
36 """Create a help folder subclass and register it with the ZCA."""
37
38 help_folder = type(
39- str('%s for %s' % (name, folder)), (HelpFolder, ), {'folder': folder})
40+ str('%s for %s' % (name, folder)), (HelpFolder, ),
41+ {'folder': folder, '__name__': name})
42
43 defineChecker(
44 help_folder,
45
46=== modified file 'lib/lp/services/webapp/doc/webapp-publication.txt'
47--- lib/lp/services/webapp/doc/webapp-publication.txt 2012-08-14 23:37:21 +0000
48+++ lib/lp/services/webapp/doc/webapp-publication.txt 2012-09-25 13:57:35 +0000
49@@ -623,39 +623,6 @@
50 >>> print request._orig_env['launchpad.pageid']
51 TestContext:TestView
52
53-When a view is registered using ZCML, the Zope machinery will store
54-the name under which the view was registered in the view's '__name__'
55-attribute. For these views, that's the name that will be used in
56-the pageid.
57-
58- >>> view.__name__ = '+test'
59- >>> publication.callObject(request, view)
60- u'Result'
61- >>> print request._orig_env['launchpad.pageid']
62- TestContext:+test
63-
64-Views registered through ZCML using the attribute property, are really a
65-bounded method. These views have the same pageid as their class.
66-
67- >>> del request._orig_env['launchpad.pageid']
68- >>> publication.callObject(request, view.__call__)
69- u'Result'
70- >>> print request._orig_env['launchpad.pageid']
71- TestContext:+test
72-
73-If the published object doesn't have a context attribute (unlike most
74-views), the pageid will be empty.
75-
76- >>> request, publication = get_request_and_publication()
77- >>> request.setPrincipal(auth_utility.unauthenticatedPrincipal())
78-
79- >>> def callable_publication():
80- ... return u'Result'
81- >>> publication.callObject(request, callable_publication)
82- u'Result'
83- >>> request._orig_env['launchpad.pageid']
84- ''
85-
86
87 Tick counts
88 -----------
89
90=== modified file 'lib/lp/services/webapp/publication.py'
91--- lib/lp/services/webapp/publication.py 2012-08-09 03:36:26 +0000
92+++ lib/lp/services/webapp/publication.py 2012-09-25 13:57:35 +0000
93@@ -14,6 +14,7 @@
94 import traceback
95 import urllib
96
97+from lazr.restful.utils import safe_hasattr
98 from lazr.uri import (
99 InvalidURIError,
100 URI,
101@@ -380,7 +381,7 @@
102 uri = uri.replace(query=query_string)
103 return str(uri)
104
105- def constructPageID(self, view, context):
106+ def constructPageID(self, view, context, view_names=()):
107 """Given a view, figure out what its page ID should be.
108
109 This provides a hook point for subclasses to override.
110@@ -392,11 +393,19 @@
111 # is accessible in the instance __name__ attribute. We use
112 # that if it's available, otherwise fall back to the class
113 # name.
114- if getattr(view, '__name__', None) is not None:
115+ if safe_hasattr(view, '__name__'):
116 view_name = view.__name__
117 else:
118 view_name = view.__class__.__name__
119- pageid = '%s:%s' % (context.__class__.__name__, view_name)
120+ names = [
121+ n for n in [view_name] + list(view_names) if n is not None]
122+ context_name = context.__class__.__name__
123+ # Is this a view of a generated view class,
124+ # such as ++model++ view of Product:+bugs. Recurse!
125+ if ' ' in context_name and safe_hasattr(context, 'context'):
126+ return self.constructPageID(context, context.context, names)
127+ view_names = ':'.join(names)
128+ pageid = '%s:%s' % (context_name, view_names)
129 # The view name used in the pageid usually comes from ZCML and so
130 # it will be a unicode string although it shouldn't. To avoid
131 # problems we encode it into ASCII.
132
133=== modified file 'lib/lp/services/webapp/tests/test_pageid.py'
134--- lib/lp/services/webapp/tests/test_pageid.py 2011-12-24 17:49:30 +0000
135+++ lib/lp/services/webapp/tests/test_pageid.py 2012-09-25 13:57:35 +0000
136@@ -8,6 +8,7 @@
137 from lazr.restful.interfaces import ICollectionResource
138 from zope.interface import implements
139
140+from lp.services.webapp.publication import LaunchpadBrowserPublication
141 from lp.services.webapp.servers import WebServicePublication
142 from lp.testing import TestCase
143
144@@ -34,6 +35,9 @@
145 self.context = FakeContext()
146 self.request = FakeRequest()
147
148+ def __call__(self):
149+ return 'result'
150+
151
152 class FakeCollectionResourceView(FakeView):
153 """A view object that provides ICollectionResource."""
154@@ -45,6 +49,56 @@
155 u'https://launchpad.dev/api/devel/#milestone-page-resource')
156
157
158+class LaunchpadBrowserPublicationPageIDTestCase(TestCase):
159+ """Ensure that the web service enhances the page ID correctly."""
160+
161+ def setUp(self):
162+ super(LaunchpadBrowserPublicationPageIDTestCase, self).setUp()
163+ self.publication = LaunchpadBrowserPublication(db=None)
164+ self.view = FakeView()
165+ self.context = FakeContext()
166+
167+ def test_pageid_without_context(self):
168+ # The pageid is an empty string if there is no context.
169+ self.assertEqual('', self.publication.constructPageID(self.view, None))
170+
171+ def test_pageid_view_without_name(self):
172+ # The view. __class__.__name__ is used if the view does not have a
173+ # __name__ attribute.
174+ self.assertEqual(
175+ 'FakeContext:FakeView',
176+ self.publication.constructPageID(self.view, self.context))
177+
178+ def test_pageid_view_with_name(self):
179+ # The view.__name__ is used when it exists.
180+ self.view.__name__ = '+snarf'
181+ self.assertEqual(
182+ 'FakeContext:+snarf',
183+ self.publication.constructPageID(self.view, self.context))
184+
185+ def test_pageid_context_is_view_from_template(self):
186+ # When the context is a dynamic view class of a page template,
187+ # such as adapting a form view to ++model++, the method recurses
188+ # the views to locate the true context.
189+ class FakeView2(FakeView):
190+ pass
191+
192+ class FakeViewView(FakeView):
193+ __name__ = '++model++'
194+
195+ def __init__(self):
196+ self.request = FakeRequest()
197+ self.context = FakeView2()
198+
199+ self.view = FakeViewView()
200+ self.context = self.view.context
201+ self.context.__name__ = '+bugs'
202+ self.context.__class__.__name__ = 'SimpleViewClass from template.pt'
203+ self.assertEqual(
204+ 'FakeContext:+bugs:++model++',
205+ self.publication.constructPageID(self.view, self.context))
206+
207+
208 class TestWebServicePageIDs(TestCase):
209 """Ensure that the web service enhances the page ID correctly."""
210