← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Brad Crittenden has proposed merging lp:~bac/launchpad/getnewcache into lp:launchpad.

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/69468

= 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/69468
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:02:34 +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:02:34 +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

=== modified file 'lib/canonical/launchpad/webapp/publisher.py'
--- lib/canonical/launchpad/webapp/publisher.py	2011-07-21 22:45:05 +0000
+++ lib/canonical/launchpad/webapp/publisher.py	2011-07-27 14:02:34 +0000
@@ -26,6 +26,7 @@
     ]
 
 import httplib
+import simplejson
 
 from zope.app import zapi
 from zope.app.publisher.interfaces.xmlrpc import IXMLRPCView
@@ -51,7 +52,14 @@
     )
 from zope.traversing.browser.interfaces import IAbsoluteURL
 
+from lazr.restful import (
+    EntryResource,
+    ResourceJSONEncoder,
+    )
 from lazr.restful.declarations import error_status
+from lazr.restful.interfaces import IJSONRequestCache
+
+from lazr.restful.tales import WebLayerAPI
 
 from canonical.launchpad.layers import (
     LaunchpadLayer,
@@ -340,6 +348,17 @@
 
     info_message = property(_getInfoMessage, _setInfoMessage)
 
+    def getCacheJSON(self):
+        if self.user is not None:
+            cache = dict(IJSONRequestCache(self.request).objects)
+        else:
+            cache = dict()
+        if WebLayerAPI(self.context).is_entry:
+            cache['context'] = self.context
+        return simplejson.dumps(
+            cache, cls=ResourceJSONEncoder,
+            media_type=EntryResource.JSON_TYPE)
+
 
 class LaunchpadXMLRPCView(UserAttributeCache):
     """Base class for writing XMLRPC view code."""

=== 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:02:34 +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>

=== modified file 'lib/canonical/launchpad/webapp/tests/test_publisher.py'
--- lib/canonical/launchpad/webapp/tests/test_publisher.py	2010-10-12 01:11:41 +0000
+++ lib/canonical/launchpad/webapp/tests/test_publisher.py	2011-07-27 14:02:34 +0000
@@ -5,10 +5,96 @@
     DocTestSuite,
     ELLIPSIS,
     )
+from unittest import TestLoader, TestSuite
+
+from lazr.restful.interfaces import IJSONRequestCache
+import simplejson
+from zope.component import getUtility
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from canonical.launchpad.webapp.publisher import LaunchpadView
+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+from lp.services.worlddata.interfaces.country import ICountrySet
+from lp.testing import (
+    logout,
+    person_logged_in,
+    TestCaseWithFactory,
+    )
 
 from canonical.launchpad.webapp import publisher
 
 
+class TestLaunchpadView(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_getCacheJSON_non_resource_context(self):
+        view = LaunchpadView(object(), LaunchpadTestRequest())
+        self.assertEqual('{}', view.getCacheJSON())
+
+    @staticmethod
+    def getCanada():
+        return getUtility(ICountrySet)['CA']
+
+    def assertIsCanada(self, json_dict):
+        self.assertIs(None, json_dict['description'])
+        self.assertEqual('CA', json_dict['iso3166code2'])
+        self.assertEqual('CAN', json_dict['iso3166code3'])
+        self.assertEqual('Canada', json_dict['name'])
+        self.assertIs(None, json_dict['title'])
+        self.assertContentEqual(
+            ['description', 'http_etag', 'iso3166code2', 'iso3166code3',
+             'name', 'resource_type_link', 'self_link', 'title'],
+            json_dict.keys())
+
+    def test_getCacheJSON_resource_context(self):
+        view = LaunchpadView(self.getCanada(), LaunchpadTestRequest())
+        json_dict = simplejson.loads(view.getCacheJSON())['context']
+        self.assertIsCanada(json_dict)
+
+    def test_getCacheJSON_non_resource_object(self):
+        request = LaunchpadTestRequest()
+        view = LaunchpadView(object(), request)
+        IJSONRequestCache(request).objects['my_bool'] = True
+        with person_logged_in(self.factory.makePerson()):
+            self.assertEqual('{"my_bool": true}', view.getCacheJSON())
+
+    def test_getCacheJSON_resource_object(self):
+        request = LaunchpadTestRequest()
+        view = LaunchpadView(object(), request)
+        IJSONRequestCache(request).objects['country'] = self.getCanada()
+        with person_logged_in(self.factory.makePerson()):
+            json_dict = simplejson.loads(view.getCacheJSON())['country']
+        self.assertIsCanada(json_dict)
+
+    def test_getCacheJSON_context_overrides_objects(self):
+        request = LaunchpadTestRequest()
+        view = LaunchpadView(self.getCanada(), request)
+        IJSONRequestCache(request).objects['context'] = True
+        with person_logged_in(self.factory.makePerson()):
+            json_dict = simplejson.loads(view.getCacheJSON())['context']
+        self.assertIsCanada(json_dict)
+
+    def test_getCache_anonymous(self):
+        request = LaunchpadTestRequest()
+        view = LaunchpadView(self.getCanada(), request)
+        self.assertIs(None, view.user)
+        IJSONRequestCache(request).objects['my_bool'] = True
+        json_dict = simplejson.loads(view.getCacheJSON())
+        self.assertIsCanada(json_dict['context'])
+        self.assertFalse('my_bool' in json_dict)
+
+    def test_getCache_anonymous_obfuscated(self):
+        request = LaunchpadTestRequest()
+        branch = self.factory.makeBranch(name='user@domain')
+        logout()
+        view = LaunchpadView(branch, request)
+        self.assertIs(None, view.user)
+        self.assertNotIn('user@domain', view.getCacheJSON())
+
+
 def test_suite():
-    suite = DocTestSuite(publisher, optionflags=ELLIPSIS)
+    suite = TestSuite()
+    suite.addTest(DocTestSuite(publisher, optionflags=ELLIPSIS))
+    suite.addTest(TestLoader().loadTestsFromName(__name__))
     return suite

=== 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:02:34 +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/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt	2011-07-08 11:46:41 +0000
+++ lib/lp/app/templates/base-layout-macros.pt	2011-07-27 14:02:34 +0000
@@ -168,15 +168,9 @@
       tal:content="string:LP.links['${key}'] =
                    '${links/?key/fmt:api_url}';">
     </script>
-    <script tal:repeat="key objects"
-      tal:content="string:LP.cache['${key}'] =
-                   ${objects/?key/webservice:json};">
-    </script>
   </tal:cache>
 
-  <script tal:condition="context/webservice:is_entry"
-    tal:content="string:LP.cache['context'] =
-                 ${context/webservice:json/fmt:obfuscate-email};">
+  <script tal:content="string:LP.cache = ${view/getCacheJSON};">
   </script>
 </metal:lp-client-cache>
 

=== modified file 'lib/lp/app/webservice/marshallers.py'
--- lib/lp/app/webservice/marshallers.py	2011-07-12 10:02:51 +0000
+++ lib/lp/app/webservice/marshallers.py	2011-07-27 14:02:34 +0000
@@ -14,8 +14,10 @@
     TextFieldMarshaller as LazrTextFieldMarshaller,
     )
 from zope.app.security.interfaces import IUnauthenticatedPrincipal
+from zope.component import getUtility
 
 from lp.services.utils import obfuscate_email
+from canonical.launchpad.webapp.interfaces import ILaunchBag
 
 
 class TextFieldMarshaller(LazrTextFieldMarshaller):
@@ -26,7 +28,7 @@
 
         Return the value as is.
         """
-        if (value is not None and
-                IUnauthenticatedPrincipal.providedBy(self.request.principal)):
+
+        if (value is not None and getUtility(ILaunchBag).user is None):
             return obfuscate_email(value)
         return value

=== modified file 'lib/lp/blueprints/browser/configure.zcml'
--- lib/lp/blueprints/browser/configure.zcml	2011-06-19 13:32:15 +0000
+++ lib/lp/blueprints/browser/configure.zcml	2011-07-27 14:02:34 +0000
@@ -135,6 +135,7 @@
         permission="zope.Public"/>
     <browser:pages
         for="lp.blueprints.interfaces.sprint.IHasSprints"
+        class="canonical.launchpad.webapp.LaunchpadView"
         facet="overview"
         permission="zope.Public">
         <browser:page

=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml	2011-07-21 05:13:08 +0000
+++ lib/lp/bugs/browser/configure.zcml	2011-07-27 14:02:34 +0000
@@ -852,6 +852,7 @@
             name="+index"/>
         <browser:pages
             for="lp.bugs.interfaces.bugtracker.IRemoteBug"
+            class="canonical.launchpad.webapp.LaunchpadView"
             permission="zope.Public">
             <browser:page
                 name="+index"

=== modified file 'lib/lp/bugs/browser/cve.py'
--- lib/lp/bugs/browser/cve.py	2011-02-24 15:30:54 +0000
+++ lib/lp/bugs/browser/cve.py	2011-07-27 14:02:34 +0000
@@ -20,6 +20,7 @@
     canonical_url,
     ContextMenu,
     GetitemNavigation,
+    LaunchpadView,
     Link,
     )
 from canonical.launchpad.webapp.batching import BatchNavigator
@@ -122,11 +123,10 @@
     heading = 'Remove links to bug reports'
 
 
-class CveSetView:
+class CveSetView(LaunchpadView):
 
     def __init__(self, context, request):
-        self.context = context
-        self.request = request
+        super(CveSetView, self).__init__(context, request)
         self.notices = []
         self.results = None
         self.text = self.request.form.get('text', None)

=== modified file 'lib/lp/code/browser/bazaar.py'
--- lib/lp/code/browser/bazaar.py	2010-08-31 11:24:54 +0000
+++ lib/lp/code/browser/bazaar.py	2011-07-27 14:02:34 +0000
@@ -87,7 +87,7 @@
     @cachedproperty
     def short_product_tag_cloud(self):
         """Show a preview of the product tag cloud."""
-        return BazaarProductView().products(
+        return BazaarProductView(None, None).products(
             num_products=config.launchpad.code_homepage_product_cloud_size)
 
 
@@ -150,7 +150,7 @@
         self.request.response.redirect(redirect_url, status=301)
 
 
-class BazaarProductView:
+class BazaarProductView(LaunchpadView):
     """Browser class for products gettable with Bazaar."""
 
     def _make_distribution_map(self, values, percentile_map):

=== modified file 'lib/lp/registry/browser/codeofconduct.py'
--- lib/lp/registry/browser/codeofconduct.py	2010-11-23 23:22:27 +0000
+++ lib/lp/registry/browser/codeofconduct.py	2011-07-27 14:02:34 +0000
@@ -220,7 +220,7 @@
         self.request = request
 
 
-class SignedCodeOfConductAdminView:
+class SignedCodeOfConductAdminView(LaunchpadView):
     """Admin Console for SignedCodeOfConduct Entries."""
 
     def __init__(self, context, request):

=== modified file 'lib/lp/registry/browser/distributionsourcepackage.py'
--- lib/lp/registry/browser/distributionsourcepackage.py	2011-06-29 16:23:36 +0000
+++ lib/lp/registry/browser/distributionsourcepackage.py	2011-07-27 14:02:34 +0000
@@ -277,7 +277,7 @@
         return Link('+changelog', text, icon="info")
 
 
-class DistributionSourcePackageBaseView:
+class DistributionSourcePackageBaseView(LaunchpadView):
     """Common features to all `DistributionSourcePackage` views."""
 
     def releases(self):
@@ -633,7 +633,7 @@
     cancel_url = next_url
 
 
-class DistributionSourcePackageHelpView:
+class DistributionSourcePackageHelpView(LaunchpadView):
     """A View to show Answers help."""
 
     page_title = 'Help and support options'

=== modified file 'lib/lp/registry/browser/peoplemerge.py'
--- lib/lp/registry/browser/peoplemerge.py	2011-05-27 21:12:25 +0000
+++ lib/lp/registry/browser/peoplemerge.py	2011-07-27 14:02:34 +0000
@@ -342,19 +342,18 @@
             return ''
 
 
-class RequestPeopleMergeMultipleEmailsView:
+class RequestPeopleMergeMultipleEmailsView(LaunchpadView):
     """Merge request view when dupe account has multiple email addresses."""
 
     label = 'Merge Launchpad accounts'
     page_title = label
 
     def __init__(self, context, request):
-        self.context = context
-        self.request = request
+        super(RequestPeopleMergeMultipleEmailsView, self).__init__(
+            context, request)
         self.form_processed = False
         self.dupe = None
         self.notified_addresses = []
-        self.user = getUtility(ILaunchBag).user
 
     def processForm(self):
         dupe = self.request.form.get('dupe')

=== modified file 'lib/lp/registry/browser/sourcepackage.py'
--- lib/lp/registry/browser/sourcepackage.py	2011-06-29 16:23:36 +0000
+++ lib/lp/registry/browser/sourcepackage.py	2011-07-27 14:02:34 +0000
@@ -444,7 +444,7 @@
                 'The packaging link has already been deleted.')
 
 
-class SourcePackageView:
+class SourcePackageView(LaunchpadView):
     """A view for (distro series) source packages."""
 
     def initialize(self):

=== modified file 'lib/lp/registry/browser/teammembership.py'
--- lib/lp/registry/browser/teammembership.py	2011-05-27 21:12:25 +0000
+++ lib/lp/registry/browser/teammembership.py	2011-07-27 14:02:34 +0000
@@ -40,12 +40,10 @@
         return "%s's membership" % self.context.person.displayname
 
 
-class TeamMembershipEditView:
+class TeamMembershipEditView(LaunchpadView):
 
     def __init__(self, context, request):
-        self.context = context
-        self.request = request
-        self.user = getUtility(ILaunchBag).user
+        super(TeamMembershipEditView, self).__init__(context, request)
         self.errormessage = ""
         self.prefix = 'membership'
         self.max_year = 2050

=== modified file 'lib/lp/registry/browser/tests/test_subscription_links.py'
--- lib/lp/registry/browser/tests/test_subscription_links.py	2011-06-22 14:09:43 +0000
+++ lib/lp/registry/browser/tests/test_subscription_links.py	2011-07-27 14:02:34 +0000
@@ -69,7 +69,7 @@
             None, self.new_edit_link,
             "Expected edit_bug_mail link missing")
         # Ensure the LP.cache has been populated.
-        self.assertTrue("LP.cache['administratedTeams']" in self.contents)
+        self.assertIn('LP.cache = {"administratedTeams": [', self.contents)
         # Ensure the call to setup the subscription is in the HTML.
         # Only check for the presence of setup's configuration step; more
         # detailed checking is needlessly brittle.

=== modified file 'lib/lp/soyuz/browser/distroarchseriesbinarypackage.py'
--- lib/lp/soyuz/browser/distroarchseriesbinarypackage.py	2010-08-20 20:31:18 +0000
+++ lib/lp/soyuz/browser/distroarchseriesbinarypackage.py	2011-07-27 14:02:34 +0000
@@ -11,6 +11,7 @@
 from canonical.launchpad.webapp import (
     ApplicationMenu,
     GetitemNavigation,
+    LaunchpadView,
     )
 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
 from canonical.lazr.utils import smartquote
@@ -31,11 +32,7 @@
     usedfor = IDistroArchSeriesBinaryPackage
 
 
-class DistroArchSeriesBinaryPackageView:
-
-    def __init__(self, context, request):
-        self.context = context
-        self.request = request
+class DistroArchSeriesBinaryPackageView(LaunchpadView):
 
     @property
     def page_title(self):

=== modified file 'lib/lp/soyuz/browser/distroarchseriesbinarypackagerelease.py'
--- lib/lp/soyuz/browser/distroarchseriesbinarypackagerelease.py	2010-08-20 20:31:18 +0000
+++ lib/lp/soyuz/browser/distroarchseriesbinarypackagerelease.py	2011-07-27 14:02:34 +0000
@@ -11,6 +11,7 @@
 
 from canonical.launchpad.webapp import (
     ApplicationMenu,
+    LaunchpadView,
     Navigation,
     )
 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
@@ -38,7 +39,7 @@
     usedfor = IDistroArchSeriesBinaryPackageRelease
 
 
-class DistroArchSeriesBinaryPackageReleaseView:
+class DistroArchSeriesBinaryPackageReleaseView(LaunchpadView):
 
     def __init__(self, context, request):
         self.context = context

=== modified file 'lib/lp/soyuz/browser/distroseriesbinarypackage.py'
--- lib/lp/soyuz/browser/distroseriesbinarypackage.py	2010-08-20 20:31:18 +0000
+++ lib/lp/soyuz/browser/distroseriesbinarypackage.py	2011-07-27 14:02:34 +0000
@@ -12,6 +12,7 @@
 
 from canonical.launchpad.webapp import (
     ApplicationMenu,
+    LaunchpadView,
     Navigation,
     StandardLaunchpadFacets,
     )
@@ -49,7 +50,7 @@
         return self.context.binarypackagename.name
 
 
-class DistroSeriesBinaryPackageView:
+class DistroSeriesBinaryPackageView(LaunchpadView):
 
     def __init__(self, context, request):
         self.context = context

=== modified file 'lib/lp/soyuz/browser/distroseriessourcepackagerelease.py'
--- lib/lp/soyuz/browser/distroseriessourcepackagerelease.py	2010-08-20 20:31:18 +0000
+++ lib/lp/soyuz/browser/distroseriessourcepackagerelease.py	2011-07-27 14:02:34 +0000
@@ -10,6 +10,7 @@
 
 from canonical.launchpad.webapp import (
     ApplicationMenu,
+    LaunchpadView,
     Navigation,
     stepthrough,
     )
@@ -49,7 +50,7 @@
         return None
 
 
-class DistroSeriesSourcePackageReleaseView:
+class DistroSeriesSourcePackageReleaseView(LaunchpadView):
 
     def __init__(self, context, request):
         self.context = context

=== modified file 'lib/lp/soyuz/browser/sourcepackage.py'
--- lib/lp/soyuz/browser/sourcepackage.py	2011-05-12 12:09:17 +0000
+++ lib/lp/soyuz/browser/sourcepackage.py	2011-07-27 14:02:34 +0000
@@ -12,7 +12,10 @@
 
 from zope.component import getUtility
 
-from canonical.launchpad.webapp import Navigation
+from canonical.launchpad.webapp import (
+    LaunchpadView,
+    Navigation,
+    )
 from canonical.lazr.utils import smartquote
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.distroseries import IDistroSeriesSet
@@ -21,7 +24,7 @@
     )
 
 
-class SourcePackageChangelogView:
+class SourcePackageChangelogView(LaunchpadView):
     """View class for source package change logs."""
 
     page_title = "Change log"
@@ -32,7 +35,7 @@
         return smartquote("Change logs for " + self.context.title)
 
 
-class SourcePackageCopyrightView:
+class SourcePackageCopyrightView(LaunchpadView):
     """A view to display a source package's copyright information."""
 
     page_title = "Copyright"

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2011-07-21 18:38:50 +0000
+++ lib/lp/testing/__init__.py	2011-07-27 14:02:34 +0000
@@ -15,6 +15,7 @@
     'build_yui_unittest_suite',
     'celebrity_logged_in',
     'ExpectedException',
+    'extract_lp_cache',
     'FakeTime',
     'get_lsb_information',
     'is_logged_in',
@@ -1306,3 +1307,8 @@
         self.caught_exc = exc_value
         return super(ExpectedException, self).__exit__(
             exc_type, exc_value, traceback)
+
+
+def extract_lp_cache(text):
+    match = re.search(r'<script>LP.cache = (\{.*\});</script>', text)
+    return simplejson.loads(match.group(1))

=== modified file 'lib/lp/translations/browser/sourcepackage.py'
--- lib/lp/translations/browser/sourcepackage.py	2011-04-21 19:56:36 +0000
+++ lib/lp/translations/browser/sourcepackage.py	2011-07-27 14:02:34 +0000
@@ -43,7 +43,7 @@
 from lp.translations.model.translationpackagingjob import TranslationMergeJob
 
 
-class SourcePackageTranslationsView(TranslationsMixin,
+class SourcePackageTranslationsView(LaunchpadView, TranslationsMixin,
                                     TranslationSharingDetailsMixin):
 
     @property

=== modified file 'lib/lp/translations/browser/tests/test_sharing_details.py'
--- lib/lp/translations/browser/tests/test_sharing_details.py	2011-06-30 16:49:37 +0000
+++ lib/lp/translations/browser/tests/test_sharing_details.py	2011-07-27 14:02:34 +0000
@@ -26,6 +26,7 @@
 from lp.testing import (
     BrowserTestCase,
     EventRecorder,
+    extract_lp_cache,
     person_logged_in,
     TestCaseWithFactory,
     )
@@ -250,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):
@@ -261,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):
@@ -273,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)
 
@@ -938,10 +939,12 @@
         sourcepackage = self.makeFullyConfiguredSharing()[0]
         anon_browser = self._getSharingDetailsViewBrowser(sourcepackage)
         # Anonymous users don't get cached objects due to bug #740208
-        self.assertNotIn("LP.cache['productseries'] =", anon_browser.contents)
+        self.assertNotIn(
+            'productseries', extract_lp_cache(anon_browser.contents))
         browser = self._getSharingDetailsViewBrowser(
             sourcepackage, user=self.user)
-        self.assertIn("LP.cache['productseries'] =", browser.contents)
+        self.assertIn(
+            'productseries', extract_lp_cache(browser.contents))
 
     def test_potlist_only_ubuntu(self):
         # Without a packaging link, only Ubuntu templates are listed.

=== modified file 'lib/lp/translations/browser/translationgroup.py'
--- lib/lp/translations/browser/translationgroup.py	2010-11-23 23:22:27 +0000
+++ lib/lp/translations/browser/translationgroup.py	2011-07-27 14:02:34 +0000
@@ -57,7 +57,7 @@
     text = u"Translation groups"
 
 
-class TranslationGroupSetView:
+class TranslationGroupSetView(LaunchpadView):
     """Translation groups overview."""
     page_title = "Translation groups"
     label = page_title

=== modified file 'lib/lp/translations/browser/translations.py'
--- lib/lp/translations/browser/translations.py	2011-05-27 19:53:20 +0000
+++ lib/lp/translations/browser/translations.py	2011-07-27 14:02:34 +0000
@@ -56,13 +56,9 @@
             rootsite='answers')
 
 
-class RosettaApplicationView(TranslationsMixin):
+class RosettaApplicationView(LaunchpadView, TranslationsMixin):
     """View for various top-level Translations pages."""
 
-    def __init__(self, context, request):
-        self.context = context
-        self.request = request
-
     @property
     def ubuntu_translationseries(self):
         ubuntu = getUtility(ILaunchpadCelebrities).ubuntu