Merge lp:~bac/launchpad/getnewcache into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Merged at revision: 13548
Proposed branch: lp:~bac/launchpad/getnewcache
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/json-serialization
Diff against target: 336 lines (+258/-8)
5 files modified
lib/canonical/launchpad/webapp/configure.zcml (+19/-0)
lib/canonical/launchpad/webapp/namespace.py (+53/-2)
lib/canonical/launchpad/webapp/templates/launchpad-model.pt (+13/-0)
lib/canonical/launchpad/webapp/tests/test_view_model.py (+167/-0)
lib/lp/translations/browser/tests/test_sharing_details.py (+6/-6)
To merge this branch: bzr merge lp:~bac/launchpad/getnewcache
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+69476@code.launchpad.net

Commit message

Provide namespace ++model++ for retrieving a view's JSON cache.

Description of the change

= Summary =

Add a namespace ++model++ so that using it returns a fresh JSON cache for the object or view referenced.

== Pre-implementation notes ==

Lots of conferring with Aaron and Gary.

== Tests ==

bin/test -vvt '(test_publisher|test_view_model)'

== Demo and Q/A ==

Go to any Launchpad page and append ++model++, e.g.
https://launchpad.net/~bac/++model++
https://launchpad.net/~bac/+edit/++model++

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

This branch looks good. I have a couple of concerns that I'd like you
to take under consideration.

We should do a quick (but not overly taxing) audit to identify existing
views that populate the JSON cache post-initialization and attempt to
fix them.

We should also consider a mechanism to "close" the JSON cache after view
initialization to prevent people from accidentally putting data in it
too late.

In launchpad-model.pt (line 121 of the diff): we probably want more
mellow header text.

review: Approve (code)
Revision history for this message
Brad Crittenden (bac) wrote :

Thanks for the review Benji.

I did a quick look at our use of IJSONRequestCache and see that all uses are within view.initialize() ... except for one. The new ILongPollSubscriber adapter does not work within initialize and seems adamant about not doing it.

Gary and I talked about ways to force compliance but decided against it reasoning that an unenforced contract is sufficient. An explanation of expectations should be added to the namespace definition so that developers will understand the restriction.

Does that sound reasonable to you? If we run into problems in practice we can investigate a more robust approach.

Revision history for this message
Benji York (benji) wrote :

On Wed, Jul 27, 2011 at 11:22 AM, Brad Crittenden <email address hidden> wrote:
> Gary and I talked about ways to force compliance but decided against
> it reasoning that an unenforced contract is sufficient.  An
> explanation of expectations should be added to the namespace
> definition so that developers will understand the restriction.

That would be good.

> Does that sound reasonable to you?  If we run into problems in
> practice we can investigate a more robust approach.

It is reasonable. I am a little worried that with only a hand full of
users of the JSON cache we already have one that's incompatible with the
new view, but it's entirely possible that it will never rise up and hurt
us. Tie goes to the runner.
--
Benji York

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
2--- lib/canonical/launchpad/webapp/configure.zcml 2011-06-08 18:39:38 +0000
3+++ lib/canonical/launchpad/webapp/configure.zcml 2011-07-27 15:26:31 +0000
4@@ -440,6 +440,25 @@
5 factory="canonical.launchpad.webapp.namespace.FormNamespaceView"
6 />
7
8+ <!-- Expose LaunchpadView methods. -->
9+ <class class="canonical.launchpad.webapp.publisher.LaunchpadView">
10+ <allow attributes="getCacheJson initialize" />
11+ </class>
12+
13+ <!-- Create a namespace to render the model of any LaunchpadView-->
14+ <view
15+ name="model" type="*"
16+ provides="zope.traversing.interfaces.ITraversable" for="*"
17+ factory="canonical.launchpad.webapp.namespace.JsonModelNamespaceView"
18+ permission="zope.Public"
19+ />
20+
21+ <class class="canonical.launchpad.webapp.namespace.JsonModelNamespaceView">
22+ <allow
23+ attributes="__call__"
24+ interface="zope.publisher.interfaces.browser.IBrowserPublisher" />
25+ </class>
26+
27 <!-- Registrations to support +haproxy status url. -->
28 <browser:page
29 for="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"
30
31=== modified file 'lib/canonical/launchpad/webapp/namespace.py'
32--- lib/canonical/launchpad/webapp/namespace.py 2011-05-27 21:03:22 +0000
33+++ lib/canonical/launchpad/webapp/namespace.py 2011-07-27 15:26:31 +0000
34@@ -1,11 +1,23 @@
35-# Copyright 2009 Canonical Ltd. This software is licensed under the
36+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
37 # GNU Affero General Public License version 3 (see the file LICENSE).
38
39+__metaclass__ = type
40+
41+__all__ = [
42+ 'FormNamespaceView',
43+ 'JsonModelNamespaceView',
44+ ]
45+
46+
47 from z3c.ptcompat import ViewPageTemplateFile
48 from zope.app.pagetemplate.viewpagetemplatefile import BoundPageTemplate
49+from zope.app.publisher.browser import getDefaultViewName
50+from zope.component import getMultiAdapter
51 from zope.security.proxy import removeSecurityProxy
52 from zope.traversing.interfaces import TraversalError
53 from zope.traversing.namespace import view
54+from zope.interface import implements
55+from zope.publisher.interfaces.browser import IBrowserPublisher
56
57 from lp.app.browser.launchpadform import LaunchpadFormView
58
59@@ -28,7 +40,7 @@
60 context = removeSecurityProxy(self.context)
61
62 if isinstance(context, LaunchpadFormView):
63- # Note: without explicitely creating the BoundPageTemplate here
64+ # Note: without explicitly creating the BoundPageTemplate here
65 # the view fails to render.
66 context.index = BoundPageTemplate(FormNamespaceView.template,
67 context)
68@@ -36,3 +48,42 @@
69 raise TraversalError("The URL does not correspond to a form.")
70
71 return self.context
72+
73+
74+class JsonModelNamespaceView(view):
75+ """A namespace view to handle traversals with ++model++.
76+
77+ Use of this namespace is only guaranteed to work if it is fully populated
78+ by a view's `initialize` method. Any objects added after the call to
79+ initialize will not be presented by the namespace.
80+ """
81+
82+ implements(IBrowserPublisher)
83+
84+ def traverse(self, name, ignored):
85+ """Model traversal adapter.
86+
87+ This adapter allows any LaunchpadView to render its JSON cache.
88+ """
89+ return self
90+
91+ def browserDefault(self, request):
92+ # Tell traversal to stop, already.
93+ return self, None
94+
95+ def __call__(self):
96+ """Return the JSON cache."""
97+ if IBrowserPublisher.providedBy(self.context):
98+ view = self.context
99+ else:
100+ defaultviewname = getDefaultViewName(
101+ self.context, self.request)
102+ view = getMultiAdapter(
103+ (self.context, self.request), name=defaultviewname)
104+ if view is None:
105+ return
106+ naked_view = removeSecurityProxy(view)
107+ naked_view.initialize()
108+ cache = naked_view.getCacheJSON()
109+ self.request.response.setHeader('content-type', 'application/json')
110+ return cache
111
112=== added file 'lib/canonical/launchpad/webapp/templates/launchpad-model.pt'
113--- lib/canonical/launchpad/webapp/templates/launchpad-model.pt 1970-01-01 00:00:00 +0000
114+++ lib/canonical/launchpad/webapp/templates/launchpad-model.pt 2011-07-27 15:26:31 +0000
115@@ -0,0 +1,13 @@
116+<div
117+ xmlns="http://www.w3.org/1999/xhtml"
118+ xmlns:tal="http://xml.zope.org/namespaces/tal"
119+ xmlns:metal="http://xml.zope.org/namespaces/metal"
120+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
121+ xml:lang="en"
122+ lang="en"
123+ dir="ltr"
124+ i18n:domain="launchpad">
125+
126+ <tal:json replace="structure context/getCacheJSON" />
127+
128+</div>
129
130=== added file 'lib/canonical/launchpad/webapp/tests/test_view_model.py'
131--- lib/canonical/launchpad/webapp/tests/test_view_model.py 1970-01-01 00:00:00 +0000
132+++ lib/canonical/launchpad/webapp/tests/test_view_model.py 2011-07-27 15:26:31 +0000
133@@ -0,0 +1,167 @@
134+# Copyright 2011 Canonical Ltd. All rights reserved.
135+
136+"""Tests for the user requested oops using ++oops++ traversal."""
137+
138+__metaclass__ = type
139+
140+
141+from lazr.restful.interfaces import IJSONRequestCache
142+from lazr.restful.utils import get_current_browser_request
143+from simplejson import loads
144+from testtools.matchers import KeysEqual
145+from zope.configuration import xmlconfig
146+
147+from canonical.launchpad.webapp import LaunchpadView
148+from canonical.launchpad.webapp.publisher import canonical_url
149+from canonical.launchpad.webapp.namespace import JsonModelNamespaceView
150+import canonical.launchpad.webapp.tests
151+from canonical.testing.layers import DatabaseFunctionalLayer
152+from lp.app.browser.launchpadform import LaunchpadFormView
153+from lp.testing import (
154+ ANONYMOUS,
155+ BrowserTestCase,
156+ login,
157+ logout,
158+ TestCaseWithFactory,
159+ )
160+
161+
162+class FakeView:
163+ """A view object that just has a fake context and request."""
164+ def __init__(self):
165+ self.context = object()
166+ self.request = object()
167+
168+
169+class TestJsonModelNamespace(TestCaseWithFactory):
170+ """Test that traversal to ++model++ returns a namespace."""
171+ layer = DatabaseFunctionalLayer
172+
173+ def setUp(self):
174+ TestCaseWithFactory.setUp(self)
175+ login(ANONYMOUS)
176+
177+ def tearDown(self):
178+ logout()
179+ TestCaseWithFactory.tearDown(self)
180+
181+ def test_JsonModelNamespace_traverse_non_LPview(self):
182+ # Test traversal for JSON model namespace,
183+ # ++model++ for a non-LaunchpadView context.
184+ request = get_current_browser_request()
185+ context = object()
186+ view = FakeView()
187+ namespace = JsonModelNamespaceView(context, request)
188+ result = namespace.traverse(view, None)
189+ self.assertEqual(result, namespace)
190+
191+ def test_JsonModelNamespace_traverse_LPView(self):
192+ # Test traversal for JSON model namespace,
193+ # ++model++ for a non-LaunchpadView context.
194+ request = get_current_browser_request()
195+ context = object()
196+ view = LaunchpadView(context, request)
197+ namespace = JsonModelNamespaceView(view, request)
198+ result = namespace.traverse(view, None)
199+ self.assertEqual(result, namespace)
200+
201+ def test_JsonModelNamespace_traverse_LPFormView(self):
202+ # Test traversal for JSON model namespace,
203+ # ++model++ for a non-LaunchpadView context.
204+ request = get_current_browser_request()
205+ context = object()
206+ view = LaunchpadFormView(context, request)
207+ namespace = JsonModelNamespaceView(view, request)
208+ result = namespace.traverse(view, None)
209+ self.assertEqual(result, namespace)
210+
211+
212+class BaseProductModelTestView(LaunchpadView):
213+ def initialize(self):
214+ # Ensure initialize does not put anything in the cache.
215+ pass
216+
217+
218+class TestJsonModelView(BrowserTestCase):
219+
220+ layer = DatabaseFunctionalLayer
221+
222+ def setUp(self):
223+ TestCaseWithFactory.setUp(self)
224+ login(ANONYMOUS)
225+ self.product = self.factory.makeProduct(name="test-product")
226+ self.url = canonical_url(self.product) + '/+modeltest/++model++'
227+
228+ def tearDown(self):
229+ logout()
230+ TestCaseWithFactory.tearDown(self)
231+
232+ def configZCML(self):
233+ # Register the ZCML for our test view. Note the view class must be
234+ # registered first.
235+ xmlconfig.string("""
236+ <configure
237+ xmlns:browser="http://namespaces.zope.org/browser">
238+ <include package="canonical.launchpad.webapp"
239+ file="meta.zcml" />
240+ <include package="zope.app.zcmlfiles" file="meta.zcml" />
241+ <browser:page
242+ name="+modeltest"
243+ for="lp.registry.interfaces.product.IProduct"
244+ class="canonical.launchpad.webapp.tests.ProductModelTestView"
245+ permission="zope.Public"
246+ />
247+ </configure>""")
248+
249+ def test_JsonModel_default_cache(self):
250+ # If nothing is added to the class by the view, the cache will only
251+ # have the context.
252+ class ProductModelTestView(BaseProductModelTestView):
253+ pass
254+ canonical.launchpad.webapp.tests.ProductModelTestView = \
255+ ProductModelTestView
256+ self.configZCML()
257+ browser = self.getUserBrowser(self.url)
258+ cache = loads(browser.contents)
259+ self.assertEqual(['context'], cache.keys())
260+
261+ def test_JsonModel_custom_cache(self):
262+ # Adding an item to the cache in the initialize method results in it
263+ # being in the cache.
264+ class ProductModelTestView(BaseProductModelTestView):
265+ def initialize(self):
266+ request = get_current_browser_request()
267+ target_info = {}
268+ target_info['title'] = "The Title"
269+ cache = IJSONRequestCache(request).objects
270+ cache['target_info'] = target_info
271+ canonical.launchpad.webapp.tests.ProductModelTestView = \
272+ ProductModelTestView
273+ self.configZCML()
274+ browser = self.getUserBrowser(self.url)
275+ cache = loads(browser.contents)
276+ self.assertThat(cache, KeysEqual('context', 'target_info'))
277+
278+ def test_JsonModel_custom_cache_wrong_method(self):
279+ # Adding an item to the cache in some other method is not recognized,
280+ # even if it called as part of normal rendering.
281+ class ProductModelTestView(BaseProductModelTestView):
282+ def initialize(self):
283+ request = get_current_browser_request()
284+ target_info = {}
285+ target_info['title'] = "The Title"
286+ cache = IJSONRequestCache(request).objects
287+ cache['target_info'] = target_info
288+
289+ def render(self):
290+ request = get_current_browser_request()
291+ other_info = {}
292+ other_info['spaz'] = "Stuff"
293+ IJSONRequestCache(request).objects['other_info'] = other_info
294+
295+ canonical.launchpad.webapp.tests.ProductModelTestView = \
296+ ProductModelTestView
297+ self.configZCML()
298+ browser = self.getUserBrowser(self.url)
299+ cache = loads(browser.contents)
300+ self.assertThat(cache, KeysEqual('context', 'target_info'))
301
302=== modified file 'lib/lp/translations/browser/tests/test_sharing_details.py'
303--- lib/lp/translations/browser/tests/test_sharing_details.py 2011-07-27 15:26:29 +0000
304+++ lib/lp/translations/browser/tests/test_sharing_details.py 2011-07-27 15:26:31 +0000
305@@ -251,7 +251,7 @@
306 # If the packaging link is set and if an upstream series
307 # uses Launchpad translations but if the other conditions
308 # are not fulfilled, is_configuration_complete is False.
309- self.configureSharing(translations_usage=ServiceUsage.LAUNCHPAD)
310+ self.configureSharing(translations_usage = ServiceUsage.LAUNCHPAD)
311 self.assertFalse(self.view.is_configuration_complete)
312
313 def test_is_configuration_complete__no_auto_sync(self):
314@@ -262,8 +262,8 @@
315 # but if the upstream series does not synchronize translations
316 # then is_configuration_complete is False.
317 self.configureSharing(
318- set_upstream_branch=True,
319- translations_usage=ServiceUsage.LAUNCHPAD)
320+ set_upstream_branch = True,
321+ translations_usage = ServiceUsage.LAUNCHPAD)
322 self.assertFalse(self.view.is_configuration_complete)
323
324 def test_is_configuration_complete__all_conditions_fulfilled(self):
325@@ -274,9 +274,9 @@
326 # - the upstream series synchronizes translations
327 # then is_configuration_complete is True.
328 self.configureSharing(
329- set_upstream_branch=True,
330- translations_usage=ServiceUsage.LAUNCHPAD,
331- translation_import_mode=
332+ set_upstream_branch = True,
333+ translations_usage = ServiceUsage.LAUNCHPAD,
334+ translation_import_mode =
335 TranslationsBranchImportMode.IMPORT_TRANSLATIONS)
336 self.assertTrue(self.view.is_configuration_complete)
337