← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/breadcrumb-override-hierarchy into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/breadcrumb-override-hierarchy into lp:launchpad.

Commit message:
Use CanonicalUrlData.inside to generate breadcrumbs, rather than relying on traversed_objects to represent the graph as we desire. Objects can use Breadcrumb.inside to override the URL defaults.

Requested reviews:
  William Grant (wgrant): code

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/breadcrumb-override-hierarchy/+merge/241905

Use CanonicalUrlData.inside to generate breadcrumbs, rather than relying on traversed_objects to represent the graph as we desire. Objects can use Breadcrumb.inside to override the URL defaults.
-- 
https://code.launchpad.net/~wgrant/launchpad/breadcrumb-override-hierarchy/+merge/241905
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py	2014-08-06 16:10:18 +0000
+++ lib/lp/app/browser/launchpad.py	2014-11-16 20:58:35 +0000
@@ -133,6 +133,7 @@
 from lp.services.webapp.breadcrumb import Breadcrumb
 from lp.services.webapp.interfaces import (
     IBreadcrumb,
+    ICanonicalUrlData,
     ILaunchBag,
     ILaunchpadRoot,
     INavigationMenu,
@@ -248,7 +249,29 @@
     @property
     def objects(self):
         """The objects for which we want breadcrumbs."""
-        return self.request.traversed_objects
+        # Start the chain with the deepest object that has a breadcrumb.
+        try:
+            objects = [(
+                obj for obj in reversed(self.request.traversed_objects)
+                if IBreadcrumb(obj, None)).next()]
+        except StopIteration:
+            return []
+        # Now iterate. If an object has a breadcrumb, it can override
+        # its parent. Otherwise we just follow the normal URL hierarchy
+        # until we reach the end.
+        while True:
+            next_obj = None
+            breadcrumb = IBreadcrumb(objects[0], None)
+            if breadcrumb is not None:
+                next_obj = breadcrumb.inside
+            if next_obj is None:
+                urldata = ICanonicalUrlData(objects[0], None)
+                if urldata is not None:
+                    next_obj = urldata.inside
+            if next_obj is None:
+                break
+            objects.insert(0, next_obj)
+        return objects
 
     @cachedproperty
     def items(self):
@@ -324,9 +347,7 @@
                 title = getattr(view, 'label', None)
             if isinstance(title, Message):
                 title = i18n.translate(title, context=self.request)
-            breadcrumb = Breadcrumb(None)
-            breadcrumb._url = url
-            breadcrumb.text = title
+            breadcrumb = Breadcrumb(None, url=url, text=title)
             return breadcrumb
         else:
             return None

=== modified file 'lib/lp/app/doc/hierarchical-menu.txt'
--- lib/lp/app/doc/hierarchical-menu.txt	2012-01-06 11:08:30 +0000
+++ lib/lp/app/doc/hierarchical-menu.txt	2014-11-16 20:58:35 +0000
@@ -2,8 +2,8 @@
 ==================
 
 The location bar aids users in navigating the depths of Launchpad.  It
-is built from a list of Breadcrumb objects collected during Zope's
-object-traversal step.
+is built from a chain of Breadcrumb objects built up from the URL
+generation rules with some manual override.s
 
 A simple object hierarchy
 -------------------------
@@ -60,9 +60,11 @@
 Discovering breadcrumbs
 -----------------------
 
-The Hierarchy class builds the breadcrumbs by looking at each object in
-the request.traversed_objects attribute.  If a traversed object can be
-adapted to IBreadcrumb, then it is added to the breadcrumbs list.
+The Hierarchy class builds the breadcrumbs by finding the deepest object
+in request.traversed_objects that can be adapted to IBreadcrumb, then
+building the chain by following IBreadcrumb.inside or
+ICanonicalUrlData.inside. Any object in the chain that can be adapted
+to IBreadcrumb is added to the breadcrumbs list.
 
 We'll add the objects to the request's list of traversed objects so
 the hierarchy will discover them.
@@ -129,6 +131,19 @@
     >>> cooker_hierarchy.items
     [<TextualBreadcrumb url='.../+cooker/jamie' text='Jamie'>]
 
+An IBreadcrumb can override ICanonicalUrlData.inside with its inside
+attribute.
+
+    >>> class ParentedTextualBreadcrumb(TextualBreadcrumb):
+    ...     inside = cookbook
+    >>> provideAdapter(ParentedTextualBreadcrumb, [ICooker], IBreadcrumb)
+    >>> cooker_hierarchy = getMultiAdapter(
+    ...     (cooker, cooker_request), name='+hierarchy')
+    >>> cooker_hierarchy.items
+    [<TextualBreadcrumb url='.../joy-of-cooking' text='Joy of cooking'>,
+     <ParentedTextualBreadcrumb url='.../+cooker/jamie' text='Jamie'>]
+
+    >>> provideAdapter(TextualBreadcrumb, [ICooker], IBreadcrumb)
 
 Displaying breadcrumbs
 ----------------------
@@ -142,6 +157,8 @@
     >>> hierarchy.display_breadcrumbs
     True
 
+    >>> cooker_hierarchy = getMultiAdapter(
+    ...     (cooker, cooker_request), name='+hierarchy')
     >>> len(cooker_hierarchy.items)
     1
     >>> cooker_hierarchy.display_breadcrumbs

=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml	2014-11-14 22:31:05 +0000
+++ lib/lp/bugs/browser/configure.zcml	2014-11-16 20:58:35 +0000
@@ -790,12 +790,6 @@
                 name="+index"
                 template="../templates/remotebug-index.pt"/>
         </browser:pages>
-    <browser:page
-        for="lp.bugs.interfaces.bugbranch.IBugBranch"
-        name="+hierarchy"
-        class="lp.code.browser.branch.BranchHierarchy"
-        template="../../app/templates/launchpad-hierarchy.pt"
-        permission="zope.Public"/>
     <browser:url
         for="lp.bugs.interfaces.bugbranch.IBugBranch"
         path_expression="string:+bug/${bug/id}"

=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py	2014-05-06 09:11:14 +0000
+++ lib/lp/code/browser/branch.py	2014-11-16 20:58:35 +0000
@@ -6,6 +6,7 @@
 __metaclass__ = type
 
 __all__ = [
+    'BranchBreadcrumb',
     'BranchContextMenu',
     'BranchDeletionView',
     'BranchEditStatusView',
@@ -74,7 +75,6 @@
 
 from lp import _
 from lp.app.browser.informationtype import InformationTypePortletMixin
-from lp.app.browser.launchpad import Hierarchy
 from lp.app.browser.launchpadform import (
     action,
     custom_widget,
@@ -158,6 +158,7 @@
     check_permission,
     precache_permission_for_objects,
     )
+from lp.services.webapp.breadcrumb import NameBreadcrumb
 from lp.services.webapp.escaping import structured
 from lp.services.webapp.interfaces import ICanonicalUrlData
 from lp.translations.interfaces.translationtemplatesbuild import (
@@ -181,34 +182,18 @@
         return self.branch.unique_name
 
 
+class BranchBreadcrumb(NameBreadcrumb):
+
+    @property
+    def inside(self):
+        return self.context.target.components[-1]
+
+
 def branch_root_context(branch):
     """Return the IRootContext for the branch."""
     return branch.target.components[0]
 
 
-class BranchHierarchy(Hierarchy):
-    """The hierarchy for a branch should be the product if there is one."""
-
-    @property
-    def objects(self):
-        """See `Hierarchy`."""
-        traversed = list(self.request.traversed_objects)
-        # Pass back the root object.
-        yield traversed.pop(0)
-        # Now pop until we find the branch.
-        branch = traversed.pop(0)
-        while not IBranch.providedBy(branch):
-            branch = traversed.pop(0)
-        # Now pass back the branch components.
-        for component in branch.target.components:
-            yield component
-        # Now the branch.
-        yield branch
-        # Now whatever is left.
-        for obj in traversed:
-            yield obj
-
-
 class BranchNavigation(Navigation):
 
     usedfor = IBranch

=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml	2014-05-07 06:12:40 +0000
+++ lib/lp/code/browser/configure.zcml	2014-11-16 20:58:35 +0000
@@ -129,12 +129,6 @@
         class="lp.code.browser.bazaar.BazaarApplicationView"
         name="+index"
         template="../templates/bazaar-index.pt" />
-    <browser:page
-        for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal"
-        name="+hierarchy"
-        class="lp.code.browser.branch.BranchHierarchy"
-        template="../../app/templates/launchpad-hierarchy.pt"
-        permission="zope.Public"/>
     <browser:navigation
         module="lp.code.browser.branchmergeproposal"
         classes="BranchMergeProposalNavigation"/>
@@ -302,12 +296,6 @@
             BranchMergeProposalEditMenu
             BranchMergeProposalActionNavigationMenu"
         module="lp.code.browser.branchmergeproposal"/>
-    <browser:page
-        for="lp.code.interfaces.branchsubscription.IBranchSubscription"
-        name="+hierarchy"
-        class="lp.code.browser.branch.BranchHierarchy"
-        template="../../app/templates/launchpad-hierarchy.pt"
-        permission="zope.Public"/>
     <browser:defaultView
         for="lp.code.interfaces.branchsubscription.IBranchSubscription"
         name="+index"/>
@@ -322,12 +310,6 @@
         path_expression="string:+subscription/${person/name}"
         attribute_to_parent="branch"
         rootsite="code"/>
-    <browser:page
-        for="lp.code.interfaces.branch.IBranch"
-        name="+hierarchy"
-        class="lp.code.browser.branch.BranchHierarchy"
-        template="../../app/templates/launchpad-hierarchy.pt"
-        permission="zope.Public"/>
     <browser:defaultView
         for="lp.code.interfaces.branch.IBranch"
         name="+index"/>
@@ -537,12 +519,6 @@
             BranchContextMenu
             BranchEditMenu"
         module="lp.code.browser.branch"/>
-    <browser:page
-        for="lp.code.interfaces.codereviewcomment.ICodeReviewComment"
-        name="+hierarchy"
-        class="lp.code.browser.branch.BranchHierarchy"
-        template="../../app/templates/launchpad-hierarchy.pt"
-        permission="zope.Public"/>
     <browser:url
         for="lp.code.interfaces.codereviewcomment.ICodeReviewComment"
         path_expression="string:comments/${id}"
@@ -919,7 +895,7 @@
     <adapter
         provides="lp.services.webapp.interfaces.IBreadcrumb"
         for="lp.code.interfaces.branch.IBranch"
-        factory="lp.services.webapp.breadcrumb.NameBreadcrumb"
+        factory="lp.code.browser.branch.BranchBreadcrumb"
         permission="zope.Public"/>
 
     <adapter
@@ -1067,17 +1043,10 @@
             name="+recipes"
             template="../templates/sourcepackagerecipe-listing.pt"/>
 
-        <browser:page
-            for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"
-            name="+hierarchy"
-            class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeHierarchy"
-            template="../../app/templates/launchpad-hierarchy.pt"
-            permission="zope.Public"/>
-
         <adapter
             provides="lp.services.webapp.interfaces.IBreadcrumb"
             for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"
-            factory="lp.services.webapp.breadcrumb.NameBreadcrumb"
+            factory="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeBreadcrumb"
             permission="zope.Public"/>
 
         <browser:page

=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py	2014-08-01 08:47:00 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py	2014-11-16 20:58:35 +0000
@@ -62,7 +62,6 @@
 from zope.security.proxy import isinstance as zope_isinstance
 
 from lp import _
-from lp.app.browser.launchpad import Hierarchy
 from lp.app.browser.launchpadform import (
     action,
     custom_widget,
@@ -114,53 +113,23 @@
     structured,
     )
 from lp.services.webapp.authorization import check_permission
-from lp.services.webapp.breadcrumb import Breadcrumb
+from lp.services.webapp.breadcrumb import (
+    Breadcrumb,
+    NameBreadcrumb,
+    )
 from lp.soyuz.interfaces.archive import ArchiveDisabled
 from lp.soyuz.model.archive import validate_ppa
 
 
-class IRecipesForPerson(Interface):
-    """A marker interface for source package recipe sets."""
-
-
-class RecipesForPersonBreadcrumb(Breadcrumb):
-    """A Breadcrumb to handle the "Recipes" link for recipe breadcrumbs."""
-
-    rootsite = 'code'
-    text = 'Recipes'
-
-    implements(IRecipesForPerson)
-
-    @property
-    def url(self):
-        return canonical_url(
-            self.context, view_name="+recipes", rootsite='code')
-
-
-class SourcePackageRecipeHierarchy(Hierarchy):
-    """Hierarchy for Source Package Recipe."""
-
-    vhost_breadcrumb = False
-
-    @property
-    def objects(self):
-        """See `Hierarchy`."""
-        traversed = list(self.request.traversed_objects)
-
-        # Pop the root object
-        yield traversed.pop(0)
-
-        recipe = traversed.pop(0)
-        while not ISourcePackageRecipe.providedBy(recipe):
-            yield recipe
-            recipe = traversed.pop(0)
-
-        # Pop in the "Recipes" link to recipe listings.
-        yield RecipesForPersonBreadcrumb(recipe.owner)
-        yield recipe
-
-        for item in traversed:
-            yield item
+class SourcePackageRecipeBreadcrumb(NameBreadcrumb):
+
+    @property
+    def inside(self):
+        return Breadcrumb(
+            self.context.owner,
+            url=canonical_url(
+                self.context.owner, view_name="+recipes", rootsite="code"),
+            text="Recipes", inside=self.context.owner)
 
 
 class SourcePackageRecipeNavigationMenu(NavigationMenu):

=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py	2013-02-07 06:10:38 +0000
+++ lib/lp/registry/browser/pillar.py	2014-11-16 20:58:35 +0000
@@ -84,6 +84,13 @@
     def text(self):
         return "Sharing details for %s" % self.context.person.displayname
 
+    @property
+    def inside(self):
+        return Breadcrumb(
+            self.context.pillar,
+            url=canonical_url(self.context.pillar, view_name="+sharing"),
+            text="Sharing", inside=self.context.pillar)
+
 
 class PillarNavigationMixin:
 

=== modified file 'lib/lp/services/webapp/breadcrumb.py'
--- lib/lp/services/webapp/breadcrumb.py	2012-04-20 18:50:03 +0000
+++ lib/lp/services/webapp/breadcrumb.py	2014-11-16 20:58:35 +0000
@@ -32,13 +32,16 @@
     text = None
     _detail = None
     _url = None
+    inside = None
 
-    def __init__(self, context, url=None, text=None):
+    def __init__(self, context, url=None, text=None, inside=None):
         self.context = context
         if url is not None:
             self._url = url
         if text is not None:
             self.text = text
+        if inside is not None:
+            self.inside = inside
 
     @property
     def rootsite(self):

=== modified file 'lib/lp/services/webapp/interfaces.py'
--- lib/lp/services/webapp/interfaces.py	2013-04-10 09:12:32 +0000
+++ lib/lp/services/webapp/interfaces.py	2014-11-16 20:58:35 +0000
@@ -230,6 +230,9 @@
 
     detail = Attribute('Detailed text of this breadcrumb.')
 
+    inside = Attribute(
+        'Next object up the chain. If not specified, the URL parent is used.')
+
 
 #
 # Canonical URLs

=== modified file 'lib/lp/services/webapp/publisher.py'
--- lib/lp/services/webapp/publisher.py	2014-05-29 06:26:57 +0000
+++ lib/lp/services/webapp/publisher.py	2014-11-16 20:58:35 +0000
@@ -39,14 +39,12 @@
 from lazr.restful.tales import WebLayerAPI
 from lazr.restful.utils import get_current_browser_request
 import simplejson
-from zope import i18n
 from zope.app.publisher.xmlrpc import IMethodPublisher
 from zope.component import (
     getUtility,
     queryMultiAdapter,
     )
 from zope.component.interfaces import ComponentLookupError
-from zope.i18nmessageid import Message
 from zope.interface import (
     directlyProvides,
     implements,
@@ -1010,34 +1008,6 @@
                             nextobj = handler(self, nextstep)
                         except NotFoundError:
                             nextobj = None
-                        else:
-                            # Circular import; breaks make.
-                            from lp.services.webapp.breadcrumb import (
-                                Breadcrumb,
-                            )
-                            stepthrough_page = queryMultiAdapter(
-                                    (self.context, self.request), name=name)
-                            if stepthrough_page:
-                                # Not all stepthroughs have a page; if they
-                                # don't, there's no need for a breadcrumb.
-                                page_title = getattr(
-                                    stepthrough_page, 'page_title', None)
-                                label = getattr(
-                                    stepthrough_page, 'label', None)
-                                stepthrough_text = page_title or label
-                                if isinstance(stepthrough_text, Message):
-                                    stepthrough_text = i18n.translate(
-                                        stepthrough_text,
-                                        context=self.request)
-                                stepthrough_url = canonical_url(
-                                    self.context, view_name=name)
-                                stepthrough_breadcrumb = Breadcrumb(
-                                    context=self.context,
-                                    url=stepthrough_url,
-                                    text=stepthrough_text)
-                                self.request.traversed_objects.append(
-                                    stepthrough_breadcrumb)
-
                         return self._handle_next_object(nextobj, request,
                             nextstep)
 

=== modified file 'lib/lp/soyuz/browser/configure.zcml'
--- lib/lp/soyuz/browser/configure.zcml	2014-11-09 23:25:28 +0000
+++ lib/lp/soyuz/browser/configure.zcml	2014-11-16 20:58:35 +0000
@@ -733,17 +733,10 @@
         permission="launchpad.Edit"
         name="+edit"
         template="../../app/templates/generic-edit.pt"/>
-    <browser:page
-        for="lp.soyuz.interfaces.livefs.ILiveFS"
-        class="lp.soyuz.browser.livefs.LiveFSHierarchy"
-        permission="zope.Public"
-        name="+hierarchy"
-        template="../../app/templates/launchpad-hierarchy.pt"
-        />
     <adapter
         provides="lp.services.webapp.interfaces.IBreadcrumb"
         for="lp.soyuz.interfaces.livefs.ILiveFS"
-        factory="lp.services.webapp.breadcrumb.NameBreadcrumb"
+        factory="lp.soyuz.browser.livefs.LiveFSBreadcrumb"
         permission="zope.Public"
         />
     <browser:url

=== modified file 'lib/lp/soyuz/browser/livefs.py'
--- lib/lp/soyuz/browser/livefs.py	2014-06-19 11:16:59 +0000
+++ lib/lp/soyuz/browser/livefs.py	2014-11-16 20:58:35 +0000
@@ -20,16 +20,12 @@
     use_template,
     )
 from zope.component import getUtility
-from zope.interface import (
-    implements,
-    Interface,
-    )
+from zope.interface import Interface
 from zope.schema import (
     Choice,
     Text,
     )
 
-from lp.app.browser.launchpad import Hierarchy
 from lp.app.browser.launchpadform import (
     action,
     custom_widget,
@@ -55,7 +51,10 @@
     stepthrough,
     )
 from lp.services.webapp.authorization import check_permission
-from lp.services.webapp.breadcrumb import Breadcrumb
+from lp.services.webapp.breadcrumb import (
+    Breadcrumb,
+    NameBreadcrumb,
+    )
 from lp.soyuz.browser.build import get_build_by_id_str
 from lp.soyuz.interfaces.livefs import (
     ILiveFS,
@@ -78,42 +77,14 @@
         return build
 
 
-class ILiveFSesForPerson(Interface):
-    """A marker interface for live filesystem sets."""
-
-
-class LiveFSesForPersonBreadcrumb(Breadcrumb):
-    """A Breadcrumb to handle the "Live filesystems" link."""
-
-    rootsite = None
-    text = 'Live filesystems'
-
-    implements(ILiveFSesForPerson)
-
-    @property
-    def url(self):
-        return canonical_url(self.context, view_name="+livefs")
-
-
-class LiveFSHierarchy(Hierarchy):
-    """Hierarchy for live filesystems."""
-
-    @property
-    def objects(self):
-        """See `Hierarchy`."""
-        traversed = list(self.request.traversed_objects)
-        # Pop the root object.
-        yield traversed.pop(0)
-        # Pop until we find the live filesystem.
-        livefs = traversed.pop(0)
-        while not ILiveFS.providedBy(livefs):
-            yield livefs
-            livefs = traversed.pop(0)
-        # Pop in the "Live filesystems" link.
-        yield LiveFSesForPersonBreadcrumb(livefs.owner)
-        yield livefs
-        for item in traversed:
-            yield item
+class LiveFSBreadcrumb(NameBreadcrumb):
+
+    @property
+    def inside(self):
+        return Breadcrumb(
+            self.context.owner,
+            url=canonical_url(self.context.owner, view_name="+livefs"),
+            text="Live filesystems", inside=self.context.owner)
 
 
 class LiveFSNavigationMenu(NavigationMenu):


Follow ups