launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #04398
[Merge] lp:~bac/launchpad/getnewcache into lp:launchpad
Brad Crittenden has proposed merging lp:~bac/launchpad/getnewcache into lp:launchpad with lp:~abentley/launchpad/json-serialization as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #810128 in Launchpad itself: "Expose a LaunchpadView's JSON for refreshing"
https://bugs.launchpad.net/launchpad/+bug/810128
For more details, see:
https://code.launchpad.net/~bac/launchpad/getnewcache/+merge/69476
= 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++
--
https://code.launchpad.net/~bac/launchpad/getnewcache/+merge/69476
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/launchpad/getnewcache into lp:launchpad.
=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
--- lib/canonical/launchpad/webapp/configure.zcml 2011-06-08 18:39:38 +0000
+++ lib/canonical/launchpad/webapp/configure.zcml 2011-07-27 14:33:35 +0000
@@ -440,6 +440,25 @@
factory="canonical.launchpad.webapp.namespace.FormNamespaceView"
/>
+ <!-- Expose LaunchpadView methods. -->
+ <class class="canonical.launchpad.webapp.publisher.LaunchpadView">
+ <allow attributes="getCacheJson initialize" />
+ </class>
+
+ <!-- Create a namespace to render the model of any LaunchpadView-->
+ <view
+ name="model" type="*"
+ provides="zope.traversing.interfaces.ITraversable" for="*"
+ factory="canonical.launchpad.webapp.namespace.JsonModelNamespaceView"
+ permission="zope.Public"
+ />
+
+ <class class="canonical.launchpad.webapp.namespace.JsonModelNamespaceView">
+ <allow
+ attributes="__call__"
+ interface="zope.publisher.interfaces.browser.IBrowserPublisher" />
+ </class>
+
<!-- Registrations to support +haproxy status url. -->
<browser:page
for="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"
=== modified file 'lib/canonical/launchpad/webapp/namespace.py'
--- lib/canonical/launchpad/webapp/namespace.py 2011-05-27 21:03:22 +0000
+++ lib/canonical/launchpad/webapp/namespace.py 2011-07-27 14:33:35 +0000
@@ -1,11 +1,23 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
+__metaclass__ = type
+
+__all__ = [
+ 'FormNamespaceView',
+ 'JsonModelNamespaceView',
+ ]
+
+
from z3c.ptcompat import ViewPageTemplateFile
from zope.app.pagetemplate.viewpagetemplatefile import BoundPageTemplate
+from zope.app.publisher.browser import getDefaultViewName
+from zope.component import getMultiAdapter
from zope.security.proxy import removeSecurityProxy
from zope.traversing.interfaces import TraversalError
from zope.traversing.namespace import view
+from zope.interface import implements
+from zope.publisher.interfaces.browser import IBrowserPublisher
from lp.app.browser.launchpadform import LaunchpadFormView
@@ -28,7 +40,7 @@
context = removeSecurityProxy(self.context)
if isinstance(context, LaunchpadFormView):
- # Note: without explicitely creating the BoundPageTemplate here
+ # Note: without explicitly creating the BoundPageTemplate here
# the view fails to render.
context.index = BoundPageTemplate(FormNamespaceView.template,
context)
@@ -36,3 +48,37 @@
raise TraversalError("The URL does not correspond to a form.")
return self.context
+
+
+class JsonModelNamespaceView(view):
+ """A namespace view to handle traversals with ++model++."""
+
+ implements(IBrowserPublisher)
+
+ def traverse(self, name, ignored):
+ """Model traversal adapter.
+
+ This adapter allows any LaunchpadView to render its JSON cache.
+ """
+ return self
+
+ def browserDefault(self, request):
+ # Tell traversal to stop, already.
+ return self, None
+
+ def __call__(self):
+ """Return the JSON cache."""
+ if IBrowserPublisher.providedBy(self.context):
+ view = self.context
+ else:
+ defaultviewname = getDefaultViewName(
+ self.context, self.request)
+ view = getMultiAdapter(
+ (self.context, self.request), name=defaultviewname)
+ if view is None:
+ return
+ naked_view = removeSecurityProxy(view)
+ naked_view.initialize()
+ cache = naked_view.getCacheJSON()
+ self.request.response.setHeader('content-type', 'application/json')
+ return cache
=== added file 'lib/canonical/launchpad/webapp/templates/launchpad-model.pt'
--- lib/canonical/launchpad/webapp/templates/launchpad-model.pt 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/webapp/templates/launchpad-model.pt 2011-07-27 14:33:35 +0000
@@ -0,0 +1,14 @@
+<div
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ xml:lang="en"
+ lang="en"
+ dir="ltr"
+ i18n:domain="launchpad">
+
+ <h1>MODEL, DAMMIT</h1>
+ <tal:json replace="structure context/getCacheJSON" />
+
+</div>
=== added file 'lib/canonical/launchpad/webapp/tests/test_view_model.py'
--- lib/canonical/launchpad/webapp/tests/test_view_model.py 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/webapp/tests/test_view_model.py 2011-07-27 14:33:35 +0000
@@ -0,0 +1,167 @@
+# Copyright 2011 Canonical Ltd. All rights reserved.
+
+"""Tests for the user requested oops using ++oops++ traversal."""
+
+__metaclass__ = type
+
+
+from lazr.restful.interfaces import IJSONRequestCache
+from lazr.restful.utils import get_current_browser_request
+from simplejson import loads
+from testtools.matchers import KeysEqual
+from zope.configuration import xmlconfig
+
+from canonical.launchpad.webapp import LaunchpadView
+from canonical.launchpad.webapp.publisher import canonical_url
+from canonical.launchpad.webapp.namespace import JsonModelNamespaceView
+import canonical.launchpad.webapp.tests
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.app.browser.launchpadform import LaunchpadFormView
+from lp.testing import (
+ ANONYMOUS,
+ BrowserTestCase,
+ login,
+ logout,
+ TestCaseWithFactory,
+ )
+
+
+class FakeView:
+ """A view object that just has a fake context and request."""
+ def __init__(self):
+ self.context = object()
+ self.request = object()
+
+
+class TestJsonModelNamespace(TestCaseWithFactory):
+ """Test that traversal to ++model++ returns a namespace."""
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ TestCaseWithFactory.setUp(self)
+ login(ANONYMOUS)
+
+ def tearDown(self):
+ logout()
+ TestCaseWithFactory.tearDown(self)
+
+ def test_JsonModelNamespace_traverse_non_LPview(self):
+ # Test traversal for JSON model namespace,
+ # ++model++ for a non-LaunchpadView context.
+ request = get_current_browser_request()
+ context = object()
+ view = FakeView()
+ namespace = JsonModelNamespaceView(context, request)
+ result = namespace.traverse(view, None)
+ self.assertEqual(result, namespace)
+
+ def test_JsonModelNamespace_traverse_LPView(self):
+ # Test traversal for JSON model namespace,
+ # ++model++ for a non-LaunchpadView context.
+ request = get_current_browser_request()
+ context = object()
+ view = LaunchpadView(context, request)
+ namespace = JsonModelNamespaceView(view, request)
+ result = namespace.traverse(view, None)
+ self.assertEqual(result, namespace)
+
+ def test_JsonModelNamespace_traverse_LPFormView(self):
+ # Test traversal for JSON model namespace,
+ # ++model++ for a non-LaunchpadView context.
+ request = get_current_browser_request()
+ context = object()
+ view = LaunchpadFormView(context, request)
+ namespace = JsonModelNamespaceView(view, request)
+ result = namespace.traverse(view, None)
+ self.assertEqual(result, namespace)
+
+
+class BaseProductModelTestView(LaunchpadView):
+ def initialize(self):
+ # Ensure initialize does not put anything in the cache.
+ pass
+
+
+class TestJsonModelView(BrowserTestCase):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ TestCaseWithFactory.setUp(self)
+ login(ANONYMOUS)
+ self.product = self.factory.makeProduct(name="test-product")
+ self.url = canonical_url(self.product) + '/+modeltest/++model++'
+
+ def tearDown(self):
+ logout()
+ TestCaseWithFactory.tearDown(self)
+
+ def configZCML(self):
+ # Register the ZCML for our test view. Note the view class must be
+ # registered first.
+ xmlconfig.string("""
+ <configure
+ xmlns:browser="http://namespaces.zope.org/browser">
+ <include package="canonical.launchpad.webapp"
+ file="meta.zcml" />
+ <include package="zope.app.zcmlfiles" file="meta.zcml" />
+ <browser:page
+ name="+modeltest"
+ for="lp.registry.interfaces.product.IProduct"
+ class="canonical.launchpad.webapp.tests.ProductModelTestView"
+ permission="zope.Public"
+ />
+ </configure>""")
+
+ def test_JsonModel_default_cache(self):
+ # If nothing is added to the class by the view, the cache will only
+ # have the context.
+ class ProductModelTestView(BaseProductModelTestView):
+ pass
+ canonical.launchpad.webapp.tests.ProductModelTestView = \
+ ProductModelTestView
+ self.configZCML()
+ browser = self.getUserBrowser(self.url)
+ cache = loads(browser.contents)
+ self.assertEqual(['context'], cache.keys())
+
+ def test_JsonModel_custom_cache(self):
+ # Adding an item to the cache in the initialize method results in it
+ # being in the cache.
+ class ProductModelTestView(BaseProductModelTestView):
+ def initialize(self):
+ request = get_current_browser_request()
+ target_info = {}
+ target_info['title'] = "The Title"
+ cache = IJSONRequestCache(request).objects
+ cache['target_info'] = target_info
+ canonical.launchpad.webapp.tests.ProductModelTestView = \
+ ProductModelTestView
+ self.configZCML()
+ browser = self.getUserBrowser(self.url)
+ cache = loads(browser.contents)
+ self.assertThat(cache, KeysEqual('context', 'target_info'))
+
+ def test_JsonModel_custom_cache_wrong_method(self):
+ # Adding an item to the cache in some other method is not recognized,
+ # even if it called as part of normal rendering.
+ class ProductModelTestView(BaseProductModelTestView):
+ def initialize(self):
+ request = get_current_browser_request()
+ target_info = {}
+ target_info['title'] = "The Title"
+ cache = IJSONRequestCache(request).objects
+ cache['target_info'] = target_info
+
+ def render(self):
+ request = get_current_browser_request()
+ other_info = {}
+ other_info['spaz'] = "Stuff"
+ IJSONRequestCache(request).objects['other_info'] = other_info
+
+ canonical.launchpad.webapp.tests.ProductModelTestView = \
+ ProductModelTestView
+ self.configZCML()
+ browser = self.getUserBrowser(self.url)
+ cache = loads(browser.contents)
+ self.assertThat(cache, KeysEqual('context', 'target_info'))
=== modified file 'lib/lp/translations/browser/tests/test_sharing_details.py'
--- lib/lp/translations/browser/tests/test_sharing_details.py 2011-07-27 14:33:34 +0000
+++ lib/lp/translations/browser/tests/test_sharing_details.py 2011-07-27 14:33:35 +0000
@@ -251,7 +251,7 @@
# If the packaging link is set and if an upstream series
# uses Launchpad translations but if the other conditions
# are not fulfilled, is_configuration_complete is False.
- self.configureSharing(translations_usage=ServiceUsage.LAUNCHPAD)
+ self.configureSharing(translations_usage = ServiceUsage.LAUNCHPAD)
self.assertFalse(self.view.is_configuration_complete)
def test_is_configuration_complete__no_auto_sync(self):
@@ -262,8 +262,8 @@
# but if the upstream series does not synchronize translations
# then is_configuration_complete is False.
self.configureSharing(
- set_upstream_branch=True,
- translations_usage=ServiceUsage.LAUNCHPAD)
+ set_upstream_branch = True,
+ translations_usage = ServiceUsage.LAUNCHPAD)
self.assertFalse(self.view.is_configuration_complete)
def test_is_configuration_complete__all_conditions_fulfilled(self):
@@ -274,9 +274,9 @@
# - the upstream series synchronizes translations
# then is_configuration_complete is True.
self.configureSharing(
- set_upstream_branch=True,
- translations_usage=ServiceUsage.LAUNCHPAD,
- translation_import_mode=
+ set_upstream_branch = True,
+ translations_usage = ServiceUsage.LAUNCHPAD,
+ translation_import_mode =
TranslationsBranchImportMode.IMPORT_TRANSLATIONS)
self.assertTrue(self.view.is_configuration_complete)