← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/watermark-from-breadcrumbs into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/watermark-from-breadcrumbs into lp:launchpad.

Requested reviews:
  William Grant (wgrant): code

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/watermark-from-breadcrumbs/+merge/241997
-- 
https://code.launchpad.net/~wgrant/launchpad/watermark-from-breadcrumbs/+merge/241997
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'lib/lp/app/browser/configure.zcml'
--- lib/lp/app/browser/configure.zcml	2014-05-14 11:05:22 +0000
+++ lib/lp/app/browser/configure.zcml	2014-11-17 18:03:10 +0000
@@ -72,7 +72,7 @@
         />
 
     <browser:page
-        for="Exception"
+        for="zope.browser.interfaces.ISystemErrorView"
         name="+hierarchy"
         class="lp.app.browser.launchpad.ExceptionHierarchy"
         template="../templates/launchpad-hierarchy.pt"
@@ -905,14 +905,6 @@
       name="required"
       />
 
-  <!-- TALES watermark: namespace -->
-  <adapter
-      for="*"
-      provides="zope.traversing.interfaces.IPathAdapter"
-      factory="lp.app.browser.watermark.WatermarkTalesAdapter"
-      name="watermark"
-      />
-
   <!-- TALES fmt: namespace for strings -->
   <adapter
       for="basestring"

=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py	2014-11-16 09:33:01 +0000
+++ lib/lp/app/browser/launchpad.py	2014-11-17 18:03:10 +0000
@@ -54,7 +54,11 @@
     TextLine,
     )
 from zope.security.interfaces import Unauthorized
-from zope.traversing.interfaces import ITraversable
+from zope.security.proxy import removeSecurityProxy
+from zope.traversing.interfaces import (
+    IPathAdapter,
+    ITraversable,
+    )
 
 from lp import _
 from lp.answers.interfaces.questioncollection import IQuestionSet
@@ -75,7 +79,11 @@
     NotFoundError,
     POSTToNonCanonicalURL,
     )
-from lp.app.interfaces.headings import IMajorHeadingView
+from lp.app.interfaces.headings import (
+    IEditableContextTitle,
+    IMajorHeadingView,
+    IRootContext,
+    )
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.interfaces.services import IServiceFactory
 from lp.app.widgets.project import ProjectScopeWidget
@@ -131,6 +139,7 @@
     )
 from lp.services.webapp.authorization import check_permission
 from lp.services.webapp.breadcrumb import Breadcrumb
+from lp.services.webapp.escaping import structured
 from lp.services.webapp.interfaces import (
     IBreadcrumb,
     ICanonicalUrlData,
@@ -309,7 +318,9 @@
     @property
     def _naked_context_view(self):
         """Return the unproxied view for the context of the hierarchy."""
-        from zope.security.proxy import removeSecurityProxy
+        # XXX wgrant 2014-11-16: Should just be able to use self.context
+        # here, but some views end up delegating to things that don't
+        # have __name__ or __launchpad_facetname__.
         if len(self.request.traversed_objects) > 0:
             return removeSecurityProxy(self.request.traversed_objects[-1])
         else:
@@ -363,6 +374,55 @@
             self._naked_context_view)
         return len(self.items) > 1 and not has_major_heading
 
+    @property
+    def heading_breadcrumb(self):
+        try:
+            return (
+                crumb for crumb in self.items
+                if IRootContext.providedBy(crumb.context)).next()
+        except StopIteration:
+            return None
+
+    def heading(self):
+        """Return the heading text for the page.
+
+        If the view provides `IEditableContextTitle` then the top heading is
+        rendered from the view's `title_edit_widget` and is generally
+        editable.
+
+        Otherwise, if the context provides `IHeadingContext` then we return an
+        H1, else an H2.
+        """
+        # Check the view; is the title editable?
+        if IEditableContextTitle.providedBy(self.context):
+            return self.context.title_edit_widget()
+        # The title is static, but only the context's index view gets an H1.
+        heading = 'h1' if IMajorHeadingView.providedBy(self.context) else 'h2'
+        # If there is actually no root context, then it's a top-level
+        # context-less page so Launchpad.net is shown as the branding.
+        if self.heading_breadcrumb:
+            title = self.heading_breadcrumb.detail
+        else:
+            title = 'Launchpad.net'
+        # For non-editable titles, generate the static heading.
+        return structured(
+            "<%(heading)s>%(title)s</%(heading)s>",
+            heading=heading, title=title).escapedtext
+
+    def logo(self):
+        """Return the logo image for the top header breadcrumb's context."""
+        logo_context = (
+            self.heading_breadcrumb.context if self.heading_breadcrumb
+            else None)
+        adapter = queryAdapter(logo_context, IPathAdapter, 'image')
+        if logo_context != self.context.context and logo_context is not None:
+            return structured(
+                '<a href="%s">%s</a>',
+                canonical_url(logo_context, rootsite='mainsite'),
+                structured(adapter.logo())).escapedtext
+        else:
+            return adapter.logo()
+
 
 class ExceptionHierarchy(Hierarchy):
 

=== modified file 'lib/lp/app/browser/tales.py'
--- lib/lp/app/browser/tales.py	2014-08-29 01:34:04 +0000
+++ lib/lp/app/browser/tales.py	2014-11-17 18:03:10 +0000
@@ -692,8 +692,7 @@
         ROOT_TITLE = 'Launchpad'
         view = self._context
         request = get_current_browser_request()
-        hierarchy_view = getMultiAdapter(
-            (view.context, request), name='+hierarchy')
+        hierarchy_view = getMultiAdapter((view, request), name='+hierarchy')
         if (isinstance(view, SystemErrorView) or
             hierarchy_view is None or
             not hierarchy_view.display_breadcrumbs):

=== modified file 'lib/lp/app/browser/tests/watermark.txt'
--- lib/lp/app/browser/tests/watermark.txt	2012-12-11 05:41:50 +0000
+++ lib/lp/app/browser/tests/watermark.txt	2014-11-17 18:03:10 +0000
@@ -1,57 +1,65 @@
-===============================
-The "watermark" TALES formatter
-===============================
+=======================
+The "watermark" heading
+=======================
 
-The watermark formatter is used to get the image and heading used on all the
-main Launchpad pages.  The image and heading are determined by the
+The watermark is the image and heading used on all the main Launchpad
+pages. The image and heading are determined by Hierarchy from the
 IRootContext for the context object.
 
 
 Watermark headings
 ==================
 
-The `watermark:heading` is used when you want a heading for the nearest object
-that implements IRootContext.
+Hierarchy.heading() is used when you want a heading for the nearest
+object that implements IRootContext.
 
-    >>> from lp.testing import test_tales
-    >>> class view:
+    >>> from lp.app.browser.launchpad import Hierarchy
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> class TrivialView:
+    ...     __name__ = '+trivial'
     ...     def __init__(self, context):
     ...         self.context = context
+    >>> def get_hierarchy(obj, viewcls=TrivialView):
+    ...     req = LaunchpadTestRequest()
+    ...     view = viewcls(obj)
+    ...     req.traversed_objects.append(view.context)
+    ...     req.traversed_objects.append(view)
+    ...     return Hierarchy(view, req)
 
 Products directly implement IRootContext.
 
     >>> widget = factory.makeProduct(title='Widget')
-    >>> print test_tales('view/watermark:heading', view=view(widget))
+    >>> print get_hierarchy(widget).heading()
     <h...>Widget</h...>
 
 A series of the product still show the product watermark.
 
     >>> dev_focus = widget.development_focus
-    >>> print test_tales('view/watermark:heading', view=view(dev_focus))
+    >>> print get_hierarchy(dev_focus).heading()
     <h...>Widget</h...>
 
 ProjectGroups also directly implement IRootContext ...
 
     >>> kde = factory.makeProject(title='KDE')
-    >>> print test_tales('view/watermark:heading', view=view(kde))
+    >>> print get_hierarchy(kde).heading()
     <h...>KDE</h...>
 
 ... as do distributions ...
 
     >>> mint = factory.makeDistribution(title='Mint Linux')
-    >>> print test_tales('view/watermark:heading', view=view(mint))
+    >>> print get_hierarchy(mint).heading()
     <h...>Mint Linux</h...>
 
 ... and people ...
 
     >>> eric = factory.makePerson(displayname="Eric the Viking")
-    >>> print test_tales('view/watermark:heading', view=view(eric))
+    >>> print get_hierarchy(eric).heading()
     <h...>Eric the Viking</h...>
 
 ... and sprints.
 
     >>> sprint = factory.makeSprint(title="Launchpad Epic")
-    >>> print test_tales('view/watermark:heading', view=view(sprint))
+    >>> print get_hierarchy(sprint).heading()
     <h...>Launchpad Epic</h...>
 
 If there is no root context defined for the object, then the heading is
@@ -59,14 +67,14 @@
 Launchpad.net).
 
     >>> machine = factory.makeCodeImportMachine()
-    >>> print test_tales('view/watermark:heading', view=view(machine))
+    >>> print get_hierarchy(machine).heading()
     <h...>Launchpad.net</h...>
 
 Any HTML in the context title will be escaped to avoid XSS vulnerabilities.
 
     >>> person = factory.makePerson(
     ...     displayname="Fubar<br/><script>alert('XSS')</script>")
-    >>> print test_tales('view/watermark:heading', view=view(person))
+    >>> print get_hierarchy(person).heading()
     <h...>Fubar&lt;br/&gt;&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;</h...>
 
 
@@ -75,15 +83,15 @@
 
 The image for the watermark is determined effectively by context/image:logo.
 
-    >>> print test_tales('view/watermark:logo', view=view(dev_focus))
+    >>> print get_hierarchy(dev_focus).logo()
     <a href="..."><img ... src="/@@/product-logo" /></a>
 
-    >>> print test_tales('view/watermark:logo', view=view(eric))
+    >>> print get_hierarchy(eric).logo()
     <img ... src="/@@/person-logo" />
 
 If there is no root context, the Launchpad logo is shown.
 
-    >>> print test_tales('view/watermark:logo', view=view(machine))
+    >>> print get_hierarchy(machine).logo()
     <img ... src="/@@/launchpad-logo" />
 
 
@@ -100,7 +108,7 @@
 is not the index page of the context.  In this case the heading is rendered in
 H2.
 
-    >>> print test_tales('view/watermark:heading', view=view(widget))
+    >>> print get_hierarchy(widget).heading()
     <h2>Widget</h2>
 
 If the view class implements IMajorHeadingView, then this is the index page
@@ -108,13 +116,9 @@
 
     >>> from zope.interface import implements
     >>> from lp.app.interfaces.headings import IMajorHeadingView
-
-    >>> class view:
+    >>> class HeadingView(TrivialView):
     ...     implements(IMajorHeadingView)
-    ...     def __init__(self, context):
-    ...         self.context = context
-
-    >>> print test_tales('view/watermark:heading', view=view(widget))
+    >>> print get_hierarchy(widget, viewcls=HeadingView).heading()
     <h1>Widget</h1>
 
 
@@ -127,12 +131,11 @@
 appropriate H tag.
 
     >>> from lp.app.interfaces.headings import IEditableContextTitle
-    >>> class view:
+    >>> class EditableView(TrivialView):
     ...     implements(IEditableContextTitle)
     ...     def __init__(self, context):
     ...         self.context = context
     ...     def title_edit_widget(self):
     ...         return '<h0>EDIT ME</h0>'
-
-    >>> print test_tales('view/watermark:heading', view=view(widget))
+    >>> print get_hierarchy(widget, viewcls=EditableView).heading()
     <h0>EDIT ME</h0>

=== removed file 'lib/lp/app/browser/watermark.py'
--- lib/lp/app/browser/watermark.py	2012-12-11 00:32:46 +0000
+++ lib/lp/app/browser/watermark.py	1970-01-01 00:00:00 +0000
@@ -1,90 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""The watermark TALES path adapter."""
-
-__metaclass__ = type
-__all__ = [
-    'WatermarkTalesAdapter',
-    ]
-
-
-from zope.component import queryAdapter
-from zope.interface import implements
-from zope.traversing.interfaces import (
-    IPathAdapter,
-    ITraversable,
-    TraversalError,
-    )
-
-from lp.app.interfaces.headings import (
-    IEditableContextTitle,
-    IMajorHeadingView,
-    IRootContext,
-    )
-from lp.services.webapp.canonicalurl import nearest_provides_or_adapted
-from lp.services.webapp.escaping import structured
-from lp.services.webapp.publisher import canonical_url
-
-
-class WatermarkTalesAdapter:
-    """Adapter for any object to get the watermark heading and image."""
-
-    implements(ITraversable)
-
-    def __init__(self, view):
-        self._view = view
-        self._context = view.context
-
-    @property
-    def root_context(self):
-        return nearest_provides_or_adapted(self._context, IRootContext)
-
-    def heading(self):
-        """Return the heading text for the page.
-
-        If the view provides `IEditableContextTitle` then the top heading is
-        rendered from the view's `title_edit_widget` and is generally
-        editable.
-
-        Otherwise, if the context provides `IRootContext` then we return an
-        H1, else an H2.
-        """
-        # Check the view; is the title editable?
-        if IEditableContextTitle.providedBy(self._view):
-            return self._view.title_edit_widget()
-        # The title is static, but only the context's index view gets an H1.
-        if IMajorHeadingView.providedBy(self._view):
-            heading = structured('h1')
-        else:
-            heading = structured('h2')
-        # If there is actually no root context, then it's a top-level
-        # context-less page so Launchpad.net is shown as the branding.
-        if self.root_context is None:
-            title = 'Launchpad.net'
-        else:
-            title = self.root_context.title
-        # For non-editable titles, generate the static heading.
-        return structured(
-            "<%(heading)s>%(title)s</%(heading)s>",
-            heading=heading,
-            title=title).escapedtext
-
-    def logo(self):
-        """Return the logo image for the root context."""
-        adapter = queryAdapter(self.root_context, IPathAdapter, 'image')
-        if (self.root_context != self._context
-            and self.root_context is not None):
-            return '<a href="%s">%s</a>' % (
-                canonical_url(self.root_context, rootsite='mainsite'),
-                adapter.logo())
-        else:
-            return adapter.logo()
-
-    def traverse(self, name, furtherPath):
-        if name == "heading":
-            return self.heading()
-        elif name == "logo":
-            return self.logo()
-        else:
-            raise TraversalError(name)

=== modified file 'lib/lp/app/templates/base-layout.pt'
--- lib/lp/app/templates/base-layout.pt	2012-11-12 15:49:29 +0000
+++ lib/lp/app/templates/base-layout.pt	2014-11-17 18:03:10 +0000
@@ -93,10 +93,10 @@
       <div id="watermark" class="watermark-apps-portlet"
         tal:condition="view/macro:has-watermark">
         <div>
-          <span tal:replace="structure view/watermark:logo"></span>
+          <span tal:replace="structure view/@@+hierarchy/logo"></span>
         </div>
         <div class="wide">
-          <h2 tal:replace="structure view/watermark:heading">
+          <h2 tal:replace="structure view/@@+hierarchy/heading">
             Celso Providelo
           </h2>
           <metal:heading_nav
@@ -119,7 +119,7 @@
                 metal:define-slot="heading"
                 >Page Label
               </h1>
-              <tal:breadcrumbs replace="structure context/@@+hierarchy">
+              <tal:breadcrumbs replace="structure view/@@+hierarchy">
                 ProjectName > Branches > Merge Proposals > fix-for-navigation
               </tal:breadcrumbs>
               <div id="registration" class="registering"

=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py	2014-11-16 09:17:22 +0000
+++ lib/lp/code/browser/branch.py	2014-11-17 18:03:10 +0000
@@ -189,11 +189,6 @@
         return self.context.target.components[-1]
 
 
-def branch_root_context(branch):
-    """Return the IRootContext for the branch."""
-    return branch.target.components[0]
-
-
 class BranchNavigation(Navigation):
 
     usedfor = IBranch

=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml	2014-11-16 09:49:59 +0000
+++ lib/lp/code/browser/configure.zcml	2014-11-17 18:03:10 +0000
@@ -19,11 +19,6 @@
                  PersonRevisionFeed ProductRevisionFeed ProjectRevisionFeed"
         />
 
-  <adapter
-      provides="lp.app.interfaces.headings.IRootContext"
-      for="lp.code.interfaces.branch.IBranch"
-      factory="lp.code.browser.branch.branch_root_context"/>
-
   <facet facet="branches">
 
   <browser:defaultView

=== modified file 'lib/lp/code/browser/tests/test_branch.py'
--- lib/lp/code/browser/tests/test_branch.py	2013-03-09 09:15:47 +0000
+++ lib/lp/code/browser/tests/test_branch.py	2014-11-17 18:03:10 +0000
@@ -17,7 +17,6 @@
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
-from lp.app.interfaces.headings import IRootContext
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.interfaces.services import IService
 from lp.bugs.interfaces.bugtask import (
@@ -893,30 +892,6 @@
         self.assertEqual([], view.dependent_branches)
 
 
-class TestBranchRootContext(TestCaseWithFactory):
-    """Test the adaptation of IBranch to IRootContext."""
-
-    layer = DatabaseFunctionalLayer
-
-    def test_personal_branch(self):
-        # The root context of a personal branch is the person.
-        branch = self.factory.makePersonalBranch()
-        root_context = IRootContext(branch)
-        self.assertEqual(branch.owner, root_context)
-
-    def test_package_branch(self):
-        # The root context of a package branch is the distribution.
-        branch = self.factory.makePackageBranch()
-        root_context = IRootContext(branch)
-        self.assertEqual(branch.distroseries.distribution, root_context)
-
-    def test_product_branch(self):
-        # The root context of a product branch is the product.
-        branch = self.factory.makeProductBranch()
-        root_context = IRootContext(branch)
-        self.assertEqual(branch.product, root_context)
-
-
 class TestBranchEditView(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer

=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py	2014-11-16 09:34:58 +0000
+++ lib/lp/registry/browser/pillar.py	2014-11-17 18:03:10 +0000
@@ -62,7 +62,10 @@
     BatchNavigator,
     StormRangeFactory,
     )
-from lp.services.webapp.breadcrumb import Breadcrumb
+from lp.services.webapp.breadcrumb import (
+    Breadcrumb,
+    DisplaynameBreadcrumb,
+    )
 from lp.services.webapp.menu import (
     ApplicationMenu,
     enabled_with_permission,
@@ -77,6 +80,14 @@
     )
 
 
+class PillarBreadcrumb(DisplaynameBreadcrumb):
+    """Breadcrumb that uses the displayname or title as appropriate."""
+
+    @property
+    def detail(self):
+        return self.context.title
+
+
 class PillarPersonBreadcrumb(Breadcrumb):
     """Builds a breadcrumb for an `IPillarPerson`."""
 

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2014-07-31 00:23:58 +0000
+++ lib/lp/registry/configure.zcml	2014-11-17 18:03:10 +0000
@@ -457,7 +457,7 @@
     <adapter
         provides="lp.services.webapp.interfaces.IBreadcrumb"
         for="lp.registry.interfaces.projectgroup.IProjectGroup"
-        factory="lp.services.webapp.breadcrumb.DisplaynameBreadcrumb"
+        factory="lp.registry.browser.pillar.PillarBreadcrumb"
         permission="zope.Public"/>
     <adapter
         provides="lp.services.webapp.interfaces.IBreadcrumb"
@@ -1499,7 +1499,7 @@
     <adapter
         provides="lp.services.webapp.interfaces.IBreadcrumb"
         for="lp.registry.interfaces.product.IProduct"
-        factory="lp.services.webapp.breadcrumb.DisplaynameBreadcrumb"
+        factory="lp.registry.browser.pillar.PillarBreadcrumb"
         permission="zope.Public"/>
     <adapter
         provides="lp.services.webapp.interfaces.IBreadcrumb"
@@ -1808,7 +1808,7 @@
     <adapter
         provides="lp.services.webapp.interfaces.IBreadcrumb"
         for="lp.registry.interfaces.distribution.IDistribution"
-        factory="lp.services.webapp.breadcrumb.DisplaynameBreadcrumb"
+        factory="lp.registry.browser.pillar.PillarBreadcrumb"
         permission="zope.Public"/>
     <adapter
         provides="lp.services.webapp.interfaces.IBreadcrumb"

=== modified file 'lib/lp/services/webapp/tests/test_breadcrumbs.py'
--- lib/lp/services/webapp/tests/test_breadcrumbs.py	2014-02-25 08:51:21 +0000
+++ lib/lp/services/webapp/tests/test_breadcrumbs.py	2014-11-17 18:03:10 +0000
@@ -118,10 +118,11 @@
             page_title = Message(
                 '${name} test', mapping={'name': 'breadcrumb'})
             __name__ = 'test-page'
+            context = self.product
         test_view = TestView()
         request = LaunchpadTestRequest()
         request.traversed_objects = [self.product, test_view]
-        hierarchy_view = Hierarchy(self.product, request)
+        hierarchy_view = Hierarchy(test_view, request)
         breadcrumb = hierarchy_view.makeBreadcrumbForRequestedPage()
         self.assertEquals(breadcrumb.text, 'breadcrumb test')
 

=== modified file 'lib/lp/testing/breadcrumbs.py'
--- lib/lp/testing/breadcrumbs.py	2012-01-01 02:58:52 +0000
+++ lib/lp/testing/breadcrumbs.py	2014-11-17 18:03:10 +0000
@@ -3,6 +3,8 @@
 
 __metaclass__ = type
 
+from zope.security.proxy import removeSecurityProxy
+
 from lp.services.webapp.publisher import canonical_url
 from lp.testing import TestCaseWithFactory
 from lp.testing.layers import DatabaseFunctionalLayer
@@ -48,5 +50,8 @@
 
     def getBreadcrumbsForUrl(self, url):
         obj, view, request = test_traverse(url)
-        view = create_initialized_view(obj, '+hierarchy', request=request)
-        return view.items
+        # Sometimes test_traverse returns the __call__, while the template
+        # always has access to the instance.
+        view = getattr(removeSecurityProxy(view), 'im_self', view)
+        hier = create_initialized_view(view, '+hierarchy', request=request)
+        return hier.items


References