launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19560
[Merge] lp:~cjwatson/launchpad/singleton-listing-no-redirect into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/singleton-listing-no-redirect into lp:launchpad.
Commit message:
Link directly to the recipe/snap in the one-recipe/snap case, rather than redirecting from +recipes/+snaps listing views.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #891918 in Launchpad itself: "redirect from /~/+recipes to recipe when only one recipe can be confusing"
https://bugs.launchpad.net/launchpad/+bug/891918
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/singleton-listing-no-redirect/+merge/273405
Link directly to the recipe/snap in the one-recipe/snap case, rather than redirecting from +recipes/+snaps listing views. Listing views should show listings even if they have only one item.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/singleton-listing-no-redirect into lp:launchpad.
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2015-09-29 18:02:29 +0000
+++ lib/lp/code/browser/branch.py 2015-10-05 13:41:26 +0000
@@ -523,14 +523,22 @@
if check_permission('launchpad.View', proposal)]
@property
- def recipe_count_text(self):
+ def recipes_link(self):
+ """A link to recipes for this branch."""
count = self.context.recipes.count()
if count == 0:
- return 'No recipes'
+ # Nothing to link to.
+ return 'No recipes using this branch.'
elif count == 1:
- return '1 recipe'
+ # Link to the single recipe.
+ return structured(
+ '<a href="%s">1 recipe</a> using this branch.',
+ canonical_url(self.context.recipes.one())).escapedtext
else:
- return '%s recipes' % count
+ # Link to a recipe listing.
+ return structured(
+ '<a href="+recipes">%s recipes</a> using this branch.',
+ count).escapedtext
@property
def is_import_branch_with_no_landing_candidates(self):
=== modified file 'lib/lp/code/browser/sourcepackagerecipelisting.py'
--- lib/lp/code/browser/sourcepackagerecipelisting.py 2015-02-24 13:46:46 +0000
+++ lib/lp/code/browser/sourcepackagerecipelisting.py 2015-10-05 13:41:26 +0000
@@ -16,7 +16,6 @@
from lp.code.browser.decorations import DecoratedBranch
from lp.services.feeds.browser import FeedsMixin
from lp.services.webapp import (
- canonical_url,
LaunchpadView,
Link,
)
@@ -46,13 +45,6 @@
return 'Source Package Recipes for %(displayname)s' % {
'displayname': self.context.displayname}
- def initialize(self):
- super(RecipeListingView, self).initialize()
- recipes = self.context.recipes
- if recipes.count() == 1:
- recipe = recipes.one()
- self.request.response.redirect(canonical_url(recipe))
-
class BranchRecipeListingView(RecipeListingView):
=== modified file 'lib/lp/code/browser/tests/test_branch.py'
--- lib/lp/code/browser/tests/test_branch.py 2015-09-18 10:15:54 +0000
+++ lib/lp/code/browser/tests/test_branch.py 2015-10-05 13:41:26 +0000
@@ -266,6 +266,32 @@
login_person(branch.owner)
self.assertFalse(view.user_can_upload)
+ def test_recipes_link_no_recipes(self):
+ # A branch with no recipes does not show a recipes link.
+ branch = self.factory.makeAnyBranch()
+ view = create_initialized_view(branch, '+index')
+ self.assertEqual('No recipes using this branch.', view.recipes_link)
+
+ def test_recipes_link_one_recipe(self):
+ # A branch with one recipe shows a link to that recipe.
+ branch = self.factory.makeAnyBranch()
+ recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
+ view = create_initialized_view(branch, '+index')
+ expected_link = (
+ '<a href="%s">1 recipe</a> using this branch.' %
+ canonical_url(recipe))
+ self.assertEqual(expected_link, view.recipes_link)
+
+ def test_recipes_link_more_recipes(self):
+ # A branch with more than one recipe shows a link to a listing.
+ branch = self.factory.makeAnyBranch()
+ self.factory.makeSourcePackageRecipe(branches=[branch])
+ self.factory.makeSourcePackageRecipe(branches=[branch])
+ view = create_initialized_view(branch, '+index')
+ self.assertEqual(
+ '<a href="+recipes">2 recipes</a> using this branch.',
+ view.recipes_link)
+
def _addBugLinks(self, branch):
for status in BugTaskStatus.items:
bug = self.factory.makeBug(status=status)
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipelisting.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipelisting.py 2015-02-24 13:46:46 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipelisting.py 2015-10-05 13:41:26 +0000
@@ -22,11 +22,9 @@
layer = DatabaseFunctionalLayer
def test_project_branch_recipe_listing(self):
- # We can see recipes for the project. We need to create two, since
- # only one will redirect to that recipe.
+ # We can see recipes for the project.
branch = self.factory.makeProductBranch()
recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
- self.factory.makeSourcePackageRecipe(branches=[branch])
text = self.getMainText(recipe.base_branch, '+recipes')
self.assertTextMatchesExpressionIgnoreWhitespace("""
Source Package Recipes for lp:.*
@@ -34,11 +32,9 @@
spr-name.* Person-name""", text)
def test_package_branch_recipe_listing(self):
- # We can see recipes for the package. We need to create two, since
- # only one will redirect to that recipe.
+ # We can see recipes for the package.
branch = self.factory.makePackageBranch()
recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
- self.factory.makeSourcePackageRecipe(branches=[branch])
text = self.getMainText(recipe.base_branch, '+recipes')
self.assertTextMatchesExpressionIgnoreWhitespace("""
Source Package Recipes for lp:.*
=== modified file 'lib/lp/code/templates/branch-recipes.pt'
--- lib/lp/code/templates/branch-recipes.pt 2012-06-15 16:23:50 +0000
+++ lib/lp/code/templates/branch-recipes.pt 2015-10-05 13:41:26 +0000
@@ -10,15 +10,7 @@
<div id="recipe-links" class="actions">
<div id="recipe-summary">
<img src="/@@/source-package-recipe" />
- <tal:no-recipes condition="not: view/context/recipes/count">
- No recipes
- </tal:no-recipes>
- <tal:recipes condition="view/context/recipes/count">
- <a href="+recipes" tal:content="structure view/recipe_count_text">
- 1 branch
- </a>
- </tal:recipes>
- using this branch.
+ <tal:recipes replace="structure view/recipes_link" />
<a href="/+help-code/related-recipes.html" target="help"
class="sprite maybe action-icon">(?)</a>
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2015-10-01 10:25:19 +0000
+++ lib/lp/registry/browser/person.py 2015-10-05 13:41:26 +0000
@@ -274,10 +274,7 @@
from lp.services.webapp.publisher import LaunchpadView
from lp.services.worlddata.interfaces.country import ICountry
from lp.services.worlddata.interfaces.language import ILanguageSet
-from lp.snappy.browser.hassnaps import (
- HasSnapsMenuMixin,
- HasSnapsViewMixin,
- )
+from lp.snappy.browser.hassnaps import HasSnapsMenuMixin
from lp.snappy.interfaces.snap import ISnapSet
from lp.soyuz.browser.archivesubscription import (
traverse_archive_subscription_for_subscriber,
@@ -1653,8 +1650,7 @@
raise AssertionError('Unknown group to contact.')
-class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin,
- HasSnapsViewMixin):
+class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin):
"""A View class used in almost all Person's pages."""
@property
=== modified file 'lib/lp/snappy/browser/hassnaps.py'
--- lib/lp/snappy/browser/hassnaps.py 2015-09-18 10:38:27 +0000
+++ lib/lp/snappy/browser/hassnaps.py 2015-10-05 13:41:26 +0000
@@ -12,9 +12,13 @@
from zope.component import getUtility
from lp.code.browser.decorations import DecoratedBranch
+from lp.code.interfaces.gitrepository import IGitRepository
from lp.services.features import getFeatureFlag
-from lp.services.propertycache import cachedproperty
-from lp.services.webapp import Link
+from lp.services.webapp import (
+ canonical_url,
+ Link,
+ )
+from lp.services.webapp.escaping import structured
from lp.snappy.interfaces.snap import (
ISnapSet,
SNAP_FEATURE_FLAG,
@@ -45,24 +49,38 @@
class HasSnapsViewMixin:
"""A view mixin for objects that implement IHasSnaps."""
- @cachedproperty
- def snap_count(self):
+ @property
+ def snaps(self):
context = self.context
if isinstance(context, DecoratedBranch):
context = context.branch
return getUtility(ISnapSet).findByContext(
- context, visible_by_user=self.user).count()
+ context, visible_by_user=self.user)
@property
def show_snap_information(self):
- return bool(getFeatureFlag(SNAP_FEATURE_FLAG)) or self.snap_count != 0
+ return (
+ bool(getFeatureFlag(SNAP_FEATURE_FLAG)) or
+ not self.snaps.is_empty())
@property
- def snap_count_text(self):
- count = self.snap_count
+ def snaps_link(self):
+ """A link to snap packages for this object."""
+ count = self.snaps.count()
+ if IGitRepository.providedBy(self.context):
+ context_type = 'repository'
+ else:
+ context_type = 'branch'
if count == 0:
- return 'No snap packages'
+ # Nothing to link to.
+ return 'No snap packages using this %s.' % context_type
elif count == 1:
- return '1 snap package'
+ # Link to the single snap package.
+ return structured(
+ '<a href="%s">1 snap package</a> using this %s.',
+ canonical_url(self.snaps.one()), context_type).escapedtext
else:
- return '%s snap packages' % count
+ # Link to a snap package listing.
+ return structured(
+ '<a href="+snaps">%s snap packages</a> using this %s.',
+ count, context_type).escapedtext
=== modified file 'lib/lp/snappy/browser/snaplisting.py'
--- lib/lp/snappy/browser/snaplisting.py 2015-09-17 08:52:02 +0000
+++ lib/lp/snappy/browser/snaplisting.py 2015-10-05 13:41:26 +0000
@@ -18,10 +18,7 @@
from lp.code.browser.decorations import DecoratedBranch
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.feeds.browser import FeedsMixin
-from lp.services.webapp import (
- canonical_url,
- LaunchpadView,
- )
+from lp.services.webapp import LaunchpadView
from lp.snappy.interfaces.snap import ISnapSet
@@ -48,9 +45,6 @@
loader = partial(
getUtility(ISnapSet).preloadDataForSnaps, user=self.user)
self.snaps = DecoratedResultSet(snaps, pre_iter_hook=loader)
- if self.snaps.count() == 1:
- snap = self.snaps.one()
- self.request.response.redirect(canonical_url(snap))
class BranchSnapListingView(SnapListingView):
=== added file 'lib/lp/snappy/browser/tests/test_hassnaps.py'
--- lib/lp/snappy/browser/tests/test_hassnaps.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/tests/test_hassnaps.py 2015-10-05 13:41:26 +0000
@@ -0,0 +1,86 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test views for objects that have snap packages."""
+
+__metaclass__ = type
+
+from lp.services.features.testing import FeatureFixture
+from lp.services.webapp import canonical_url
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.views import create_initialized_view
+
+
+class TestRelatedSnapsMixin:
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestRelatedSnapsMixin, self).setUp()
+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+
+ def test_snaps_link_no_snaps(self):
+ # An object with no snap packages does not show a snap packages link.
+ context = self.makeContext()
+ view = create_initialized_view(context, "+index")
+ self.assertEqual(
+ "No snap packages using this %s." % self.context_type,
+ view.snaps_link)
+
+ def test_snaps_link_one_snap(self):
+ # An object with one snap package shows a link to that snap package.
+ context = self.makeContext()
+ snap = self.makeSnap(context)
+ view = create_initialized_view(context, "+index")
+ expected_link = (
+ '<a href="%s">1 snap package</a> using this %s.' %
+ (canonical_url(snap), self.context_type))
+ self.assertEqual(expected_link, view.snaps_link)
+
+ def test_snaps_link_more_snaps(self):
+ # An object with more than one snap package shows a link to a listing.
+ context = self.makeContext()
+ self.makeSnap(context)
+ self.makeSnap(context)
+ view = create_initialized_view(context, "+index")
+ expected_link = (
+ '<a href="+snaps">2 snap packages</a> using this %s.' %
+ self.context_type)
+ self.assertEqual(expected_link, view.snaps_link)
+
+
+class TestRelatedSnapsBranch(TestRelatedSnapsMixin, TestCaseWithFactory):
+
+ context_type = "branch"
+
+ def makeContext(self):
+ return self.factory.makeAnyBranch()
+
+ def makeSnap(self, context):
+ return self.factory.makeSnap(branch=context)
+
+
+class TestRelatedSnapsGitRepository(
+ TestRelatedSnapsMixin, TestCaseWithFactory):
+
+ context_type = "repository"
+
+ def makeContext(self):
+ return self.factory.makeGitRepository()
+
+ def makeSnap(self, context):
+ [ref] = self.factory.makeGitRefs(repository=context)
+ return self.factory.makeSnap(git_ref=ref)
+
+
+class TestRelatedSnapsGitRef(TestRelatedSnapsMixin, TestCaseWithFactory):
+
+ context_type = "branch"
+
+ def makeContext(self):
+ return self.factory.makeGitRefs()[0]
+
+ def makeSnap(self, context):
+ return self.factory.makeSnap(git_ref=context)
=== modified file 'lib/lp/snappy/browser/tests/test_snaplisting.py'
--- lib/lp/snappy/browser/tests/test_snaplisting.py 2015-09-16 13:30:33 +0000
+++ lib/lp/snappy/browser/tests/test_snaplisting.py 2015-10-05 13:41:26 +0000
@@ -55,20 +55,21 @@
self.assertThat(self.getViewBrowser(context).contents, Not(matcher))
login(ANONYMOUS)
self.makeSnap(**kwargs)
+ self.makeSnap(**kwargs)
self.assertThat(self.getViewBrowser(context).contents, matcher)
def test_branch_links_to_snaps(self):
branch = self.factory.makeAnyBranch()
- self.assertSnapsLink(branch, "1 snap package", branch=branch)
+ self.assertSnapsLink(branch, "2 snap packages", branch=branch)
def test_git_repository_links_to_snaps(self):
repository = self.factory.makeGitRepository()
[ref] = self.factory.makeGitRefs(repository=repository)
- self.assertSnapsLink(repository, "1 snap package", git_ref=ref)
+ self.assertSnapsLink(repository, "2 snap packages", git_ref=ref)
def test_git_ref_links_to_snaps(self):
[ref] = self.factory.makeGitRefs()
- self.assertSnapsLink(ref, "1 snap package", git_ref=ref)
+ self.assertSnapsLink(ref, "2 snap packages", git_ref=ref)
def test_person_links_to_snaps(self):
person = self.factory.makePerson()
@@ -83,54 +84,38 @@
project, "View snap packages", link_has_context=True, git_ref=ref)
def test_branch_snap_listing(self):
- # We can see snap packages for a Bazaar branch. We need to create
- # two, since if there's only one then +snaps will redirect to that
- # package.
+ # We can see snap packages for a Bazaar branch.
branch = self.factory.makeAnyBranch()
- for _ in range(2):
- self.makeSnap(branch=branch)
+ self.makeSnap(branch=branch)
text = self.getMainText(branch, "+snaps")
self.assertTextMatchesExpressionIgnoreWhitespace("""
Snap packages for lp:.*
Name Owner Registered
- snap-name.* Team Name.* .*
snap-name.* Team Name.* .*""", text)
def test_git_repository_snap_listing(self):
- # We can see snap packages for a Git repository. We need to create
- # two, since if there's only one then +snaps will redirect to that
- # package.
+ # We can see snap packages for a Git repository.
repository = self.factory.makeGitRepository()
- ref1, ref2 = self.factory.makeGitRefs(
- repository=repository,
- paths=[u"refs/heads/branch-1", u"refs/heads/branch-2"])
- for ref in ref1, ref2:
- self.makeSnap(git_ref=ref)
+ [ref] = self.factory.makeGitRefs(repository=repository)
+ self.makeSnap(git_ref=ref)
text = self.getMainText(repository, "+snaps")
self.assertTextMatchesExpressionIgnoreWhitespace("""
Snap packages for lp:~.*
Name Owner Registered
- snap-name.* Team Name.* .*
snap-name.* Team Name.* .*""", text)
def test_git_ref_snap_listing(self):
- # We can see snap packages for a Git reference. We need to create
- # two, since if there's only one then +snaps will redirect to that
- # package.
+ # We can see snap packages for a Git reference.
[ref] = self.factory.makeGitRefs()
- for _ in range(2):
- self.makeSnap(git_ref=ref)
+ self.makeSnap(git_ref=ref)
text = self.getMainText(ref, "+snaps")
self.assertTextMatchesExpressionIgnoreWhitespace("""
Snap packages for ~.*:.*
Name Owner Registered
- snap-name.* Team Name.* .*
snap-name.* Team Name.* .*""", text)
def test_person_snap_listing(self):
- # We can see snap packages for a person. We need to create two,
- # since if there's only one then +snaps will redirect to that
- # package.
+ # We can see snap packages for a person.
owner = self.factory.makePerson(displayname="Snap Owner")
self.makeSnap(
registrant=owner, owner=owner, branch=self.factory.makeAnyBranch(),
@@ -146,9 +131,7 @@
snap-name.* lp:.* .*""", text)
def test_project_snap_listing(self):
- # We can see snap packages for a project. We need to create two,
- # since if there's only one then +snaps will redirect to that
- # package.
+ # We can see snap packages for a project.
project = self.factory.makeProduct(displayname="Snappable")
self.makeSnap(
branch=self.factory.makeProductBranch(product=project),
=== modified file 'lib/lp/snappy/templates/snap-macros.pt'
--- lib/lp/snappy/templates/snap-macros.pt 2015-09-18 10:15:54 +0000
+++ lib/lp/snappy/templates/snap-macros.pt 2015-10-05 13:41:26 +0000
@@ -14,13 +14,7 @@
<div id="snap-links" class="actions">
<div id="snap-summary">
- <tal:no-snaps condition="not: view/snap_count">
- No snap packages
- </tal:no-snaps>
- <tal:snaps condition="view/snap_count">
- <a href="+snaps" tal:content="view/snap_count_text">1 snap package</a>
- </tal:snaps>
- using this <metal:slot define-slot="context_type">branch</metal:slot>.
+ <tal:snaps replace="structure view/snaps_link" />
</div>
</div>
Follow ups