← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/json-serialization into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/json-serialization into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~abentley/launchpad/json-serialization/+merge/69519

= Summary =
Make the IJSONRequestCache serialization reusable.

== Proposed fix ==
Use the view, rather than the template, to serialize the IJSONRequestCache

== Pre-implementation notes ==
Discussed with Ian Booth, J.C. Sackett and Brad Crittenden.

== Implementation details ==
The main change is the getCacheJSON method, which implements the serialization
logic.  There are two changes:
1. The text format of the cache generation has changed to generate the cache in
   a single assignment.  Previously, each value was assigned in a separate
   <script> tag.

2. Email obfuscation is now performed at a low level in the marshaller, rather
   than explicitly in the template.

Aside from that, the new cache is equivalent to the old one.

Many tests failed because the template now requires the view to provide
getCacheJSON.  In most cases, this could be fixed by making the view a subclass
of LaunchpadView.  That change made some constructors redundant, so they were
removed.  In some cases, no specific view class had been specified, and so
specifying LaunchpadView was enough.

Some tests relied on the specific text formatting, so they were adjusted to use
the new formatting, or to make assertions about the serialized values.

Some tests broke because email obfuscation wasn't being triggered.  It had
relied on the fact that when a user is logged it, the request's principal does
not provide IUnauthenticatedPrincipal.  However, in tests, the request
principal is typically None, which also doesn't provide
IUnauthenticatedPrincipal.  We usually use ILaunchbag.user to determine the
current user, so changing the marshaller to use this fixed the tests.

I also fixed some lint.

== Tests ==
bin/test -t test_marshallers -t test_publisher

== Demo and Q/A ==
Create a source package with an email address in a text field.
As a logged-in user, visit its +sharing-details page and look at the page
source.  You should see LP.cache having multiple values assigned at once.
After logging out, only the context should be assigned, and the email address
should be obfuscated.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/browser/distroseriessourcepackagerelease.py
  lib/lp/registry/browser/sourcepackage.py
  lib/lp/registry/browser/peoplemerge.py
  lib/canonical/launchpad/webapp/publisher.py
  lib/lp/code/browser/bazaar.py
  lib/lp/registry/browser/codeofconduct.py
  lib/lp/soyuz/browser/sourcepackage.py
  lib/lp/registry/browser/distributionsourcepackage.py
  lib/canonical/launchpad/webapp/tests/test_publisher.py
  lib/lp/soyuz/browser/distroseriesbinarypackage.py
  lib/lp/app/templates/base-layout-macros.pt
  lib/lp/testing/__init__.py
  lib/lp/bugs/browser/configure.zcml
  lib/lp/bugs/browser/cve.py
  lib/lp/soyuz/browser/distroarchseriesbinarypackagerelease.py
  lib/lp/soyuz/browser/distroarchseriesbinarypackage.py
  lib/lp/translations/browser/tests/test_sharing_details.py
  lib/lp/translations/browser/translationgroup.py
  lib/lp/app/webservice/tests/test_marshallers.py
  lib/lp/blueprints/browser/configure.zcml
  lib/lp/registry/browser/teammembership.py
  lib/lp/app/webservice/marshallers.py
  lib/lp/translations/browser/translations.py
  lib/lp/translations/browser/sourcepackage.py
  lib/lp/registry/browser/tests/test_subscription_links.py

./lib/lp/translations/browser/tests/test_sharing_details.py
     218: E251 no spaces around keyword / parameter equals
     226: E251 no spaces around keyword / parameter equals
     279: E251 no spaces around keyword / parameter equals
     311: E251 no spaces around keyword / parameter equals
-- 
https://code.launchpad.net/~abentley/launchpad/json-serialization/+merge/69519
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/json-serialization into lp:launchpad.
=== 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 18:06:47 +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."""

=== 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 18:06:47 +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

=== 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 18:06:47 +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 18:06:47 +0000
@@ -13,9 +13,10 @@
 from lazr.restful.marshallers import (
     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 +27,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/app/webservice/tests/test_marshallers.py'
--- lib/lp/app/webservice/tests/test_marshallers.py	2011-07-12 10:02:51 +0000
+++ lib/lp/app/webservice/tests/test_marshallers.py	2011-07-27 18:06:47 +0000
@@ -6,17 +6,19 @@
 __metaclass__ = type
 
 import transaction
-from zope.component import getUtility
 
 from canonical.launchpad.testing.pages import (
     LaunchpadWebServiceCaller,
     webservice_for_person,
     )
-from canonical.launchpad.webapp.interfaces import IPlacelessAuthUtility
 from canonical.launchpad.webapp.servers import WebServiceTestRequest
 from canonical.testing.layers import DatabaseFunctionalLayer
 from lp.app.webservice.marshallers import TextFieldMarshaller
-from lp.testing import logout, TestCaseWithFactory
+from lp.testing import (
+    logout,
+    person_logged_in,
+    TestCaseWithFactory,
+    )
 
 
 def ws_url(bug):
@@ -28,28 +30,17 @@
 
     layer = DatabaseFunctionalLayer
 
-    def _makeRequest(self, is_anonymous):
-        """Create either an anonymous or authenticated request."""
-        request = WebServiceTestRequest()
-        if is_anonymous:
-            request.setPrincipal(
-                getUtility(IPlacelessAuthUtility).unauthenticatedPrincipal())
-        else:
-            request.setPrincipal(self.factory.makePerson())
-        return request
-
     def test_unmarshall_obfuscated(self):
-        # Data is obfuccated if the request is anonynous.
-        request = self._makeRequest(is_anonymous=True)
-        marshaller = TextFieldMarshaller(None, request)
+        # Data is obfuscated if the user is anonynous.
+        marshaller = TextFieldMarshaller(None, WebServiceTestRequest())
         result = marshaller.unmarshall(None, u"foo@xxxxxxxxxxx")
         self.assertEqual(u"<email address hidden>", result)
 
     def test_unmarshall_not_obfuscated(self):
-        # Data is not obfuccated if the request is authenticated.
-        request = self._makeRequest(is_anonymous=False)
-        marshaller = TextFieldMarshaller(None, request)
-        result = marshaller.unmarshall(None, u"foo@xxxxxxxxxxx")
+        # Data is not obfuccated if the user is authenticated.
+        marshaller = TextFieldMarshaller(None, WebServiceTestRequest())
+        with person_logged_in(self.factory.makePerson()):
+            result = marshaller.unmarshall(None, u"foo@xxxxxxxxxxx")
         self.assertEqual(u"foo@xxxxxxxxxxx", result)
 
 

=== 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 18:06:47 +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 18:06:47 +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 18:06:47 +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 18:06:47 +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 18:06:47 +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 18:06:47 +0000
@@ -130,7 +130,8 @@
 
     def overview(self):
         text = 'Overview'
-        summary = 'General information about {0}'.format(self.context.displayname)
+        summary = 'General information about {0}'.format(
+            self.context.displayname)
         return Link('', text, summary)
 
     def bugs(self):
@@ -277,7 +278,7 @@
         return Link('+changelog', text, icon="info")
 
 
-class DistributionSourcePackageBaseView:
+class DistributionSourcePackageBaseView(LaunchpadView):
     """Common features to all `DistributionSourcePackage` views."""
 
     def releases(self):
@@ -633,7 +634,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 18:06:47 +0000
@@ -139,7 +139,7 @@
                 naked_email.personID = self.target_person.id
                 naked_email.accountID = self.target_person.accountID
                 naked_email.status = EmailAddressStatus.NEW
-        job = getUtility(IPersonSet).mergeAsync(
+        getUtility(IPersonSet).mergeAsync(
             self.dupe_person, self.target_person, reviewer=self.user)
         self.request.response.addInfoNotification(self.merge_message)
         self.next_url = self.success_url
@@ -214,7 +214,6 @@
 
         super(AdminTeamMergeView, self).validate(data)
         dupe_team = data['dupe_person']
-        target_team = data['target_person']
         # We cannot merge the teams if there is a mailing list on the
         # duplicate person, unless that mailing list is purged.
         if self.hasMailingList(dupe_team):
@@ -342,19 +341,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 18:06:47 +0000
@@ -212,7 +212,8 @@
 
     def overview(self):
         text = 'Overview'
-        summary = 'General information about {0}'.format(self.context.displayname)
+        summary = 'General information about {0}'.format(
+            self.context.displayname)
         return Link('', text, summary)
 
     def bugs(self):
@@ -227,7 +228,8 @@
 
     def translations(self):
         text = 'Translations'
-        summary = 'Translations of {0} in Launchpad'.format(self.context.displayname)
+        summary = 'Translations of {0} in Launchpad'.format(
+            self.context.displayname)
         return Link('', text, summary)
 
 
@@ -444,7 +446,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 18:06:47 +0000
@@ -25,7 +25,6 @@
     LaunchpadView,
     )
 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
-from canonical.launchpad.webapp.interfaces import ILaunchBag
 from lp.app.errors import UnexpectedFormData
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.widgets.date import DateWidget
@@ -40,12 +39,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
@@ -256,7 +253,7 @@
 
         assert self.context.status == TeamMembershipStatus.PROPOSED
 
-        action = self.request.form.get('editproposed')
+        self.request.form.get('editproposed')
         if self.request.form.get('decline'):
             status = TeamMembershipStatus.DECLINED
         elif self.request.form.get('approve'):

=== 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 18:06:47 +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 18:06:47 +0000
@@ -11,8 +11,8 @@
 from canonical.launchpad.webapp import (
     ApplicationMenu,
     GetitemNavigation,
+    LaunchpadView,
     )
-from canonical.launchpad.webapp.breadcrumb import Breadcrumb
 from canonical.lazr.utils import smartquote
 from lp.soyuz.interfaces.distroarchseriesbinarypackage import (
     IDistroArchSeriesBinaryPackage,
@@ -31,11 +31,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 18:06:47 +0000
@@ -11,6 +11,7 @@
 
 from canonical.launchpad.webapp import (
     ApplicationMenu,
+    LaunchpadView,
     Navigation,
     )
 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
@@ -38,9 +39,8 @@
     usedfor = IDistroArchSeriesBinaryPackageRelease
 
 
-class DistroArchSeriesBinaryPackageReleaseView:
+class DistroArchSeriesBinaryPackageReleaseView(LaunchpadView):
 
     def __init__(self, context, request):
         self.context = context
         self.request = request
-

=== 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 18:06:47 +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 18:06:47 +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 18:06:47 +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 18:06:47 +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 18:06:47 +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 18:06:47 +0000
@@ -26,6 +26,7 @@
 from lp.testing import (
     BrowserTestCase,
     EventRecorder,
+    extract_lp_cache,
     person_logged_in,
     TestCaseWithFactory,
     )
@@ -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 18:06:47 +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 18:06:47 +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