Merge lp:~cjwatson/launchpad/singleton-listing-no-redirect into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17802
Proposed branch: lp:~cjwatson/launchpad/singleton-listing-no-redirect
Merge into: lp:launchpad
Diff against target: 476 lines (+173/-88)
11 files modified
lib/lp/code/browser/branch.py (+12/-4)
lib/lp/code/browser/sourcepackagerecipelisting.py (+0/-8)
lib/lp/code/browser/tests/test_branch.py (+26/-0)
lib/lp/code/browser/tests/test_sourcepackagerecipelisting.py (+2/-6)
lib/lp/code/templates/branch-recipes.pt (+1/-9)
lib/lp/registry/browser/person.py (+2/-6)
lib/lp/snappy/browser/hassnaps.py (+29/-11)
lib/lp/snappy/browser/snaplisting.py (+1/-7)
lib/lp/snappy/browser/tests/test_hassnaps.py (+86/-0)
lib/lp/snappy/browser/tests/test_snaplisting.py (+13/-30)
lib/lp/snappy/templates/snap-macros.pt (+1/-7)
To merge this branch: bzr merge lp:~cjwatson/launchpad/singleton-listing-no-redirect
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+273405@code.launchpad.net

Commit message

Link directly to the recipe/snap in the one-recipe/snap case, rather than redirecting from +recipes/+snaps listing views.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

Yes please.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branch.py'
2--- lib/lp/code/browser/branch.py 2015-09-29 18:02:29 +0000
3+++ lib/lp/code/browser/branch.py 2015-10-05 13:41:26 +0000
4@@ -523,14 +523,22 @@
5 if check_permission('launchpad.View', proposal)]
6
7 @property
8- def recipe_count_text(self):
9+ def recipes_link(self):
10+ """A link to recipes for this branch."""
11 count = self.context.recipes.count()
12 if count == 0:
13- return 'No recipes'
14+ # Nothing to link to.
15+ return 'No recipes using this branch.'
16 elif count == 1:
17- return '1 recipe'
18+ # Link to the single recipe.
19+ return structured(
20+ '<a href="%s">1 recipe</a> using this branch.',
21+ canonical_url(self.context.recipes.one())).escapedtext
22 else:
23- return '%s recipes' % count
24+ # Link to a recipe listing.
25+ return structured(
26+ '<a href="+recipes">%s recipes</a> using this branch.',
27+ count).escapedtext
28
29 @property
30 def is_import_branch_with_no_landing_candidates(self):
31
32=== modified file 'lib/lp/code/browser/sourcepackagerecipelisting.py'
33--- lib/lp/code/browser/sourcepackagerecipelisting.py 2015-02-24 13:46:46 +0000
34+++ lib/lp/code/browser/sourcepackagerecipelisting.py 2015-10-05 13:41:26 +0000
35@@ -16,7 +16,6 @@
36 from lp.code.browser.decorations import DecoratedBranch
37 from lp.services.feeds.browser import FeedsMixin
38 from lp.services.webapp import (
39- canonical_url,
40 LaunchpadView,
41 Link,
42 )
43@@ -46,13 +45,6 @@
44 return 'Source Package Recipes for %(displayname)s' % {
45 'displayname': self.context.displayname}
46
47- def initialize(self):
48- super(RecipeListingView, self).initialize()
49- recipes = self.context.recipes
50- if recipes.count() == 1:
51- recipe = recipes.one()
52- self.request.response.redirect(canonical_url(recipe))
53-
54
55 class BranchRecipeListingView(RecipeListingView):
56
57
58=== modified file 'lib/lp/code/browser/tests/test_branch.py'
59--- lib/lp/code/browser/tests/test_branch.py 2015-09-18 10:15:54 +0000
60+++ lib/lp/code/browser/tests/test_branch.py 2015-10-05 13:41:26 +0000
61@@ -266,6 +266,32 @@
62 login_person(branch.owner)
63 self.assertFalse(view.user_can_upload)
64
65+ def test_recipes_link_no_recipes(self):
66+ # A branch with no recipes does not show a recipes link.
67+ branch = self.factory.makeAnyBranch()
68+ view = create_initialized_view(branch, '+index')
69+ self.assertEqual('No recipes using this branch.', view.recipes_link)
70+
71+ def test_recipes_link_one_recipe(self):
72+ # A branch with one recipe shows a link to that recipe.
73+ branch = self.factory.makeAnyBranch()
74+ recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
75+ view = create_initialized_view(branch, '+index')
76+ expected_link = (
77+ '<a href="%s">1 recipe</a> using this branch.' %
78+ canonical_url(recipe))
79+ self.assertEqual(expected_link, view.recipes_link)
80+
81+ def test_recipes_link_more_recipes(self):
82+ # A branch with more than one recipe shows a link to a listing.
83+ branch = self.factory.makeAnyBranch()
84+ self.factory.makeSourcePackageRecipe(branches=[branch])
85+ self.factory.makeSourcePackageRecipe(branches=[branch])
86+ view = create_initialized_view(branch, '+index')
87+ self.assertEqual(
88+ '<a href="+recipes">2 recipes</a> using this branch.',
89+ view.recipes_link)
90+
91 def _addBugLinks(self, branch):
92 for status in BugTaskStatus.items:
93 bug = self.factory.makeBug(status=status)
94
95=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipelisting.py'
96--- lib/lp/code/browser/tests/test_sourcepackagerecipelisting.py 2015-02-24 13:46:46 +0000
97+++ lib/lp/code/browser/tests/test_sourcepackagerecipelisting.py 2015-10-05 13:41:26 +0000
98@@ -22,11 +22,9 @@
99 layer = DatabaseFunctionalLayer
100
101 def test_project_branch_recipe_listing(self):
102- # We can see recipes for the project. We need to create two, since
103- # only one will redirect to that recipe.
104+ # We can see recipes for the project.
105 branch = self.factory.makeProductBranch()
106 recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
107- self.factory.makeSourcePackageRecipe(branches=[branch])
108 text = self.getMainText(recipe.base_branch, '+recipes')
109 self.assertTextMatchesExpressionIgnoreWhitespace("""
110 Source Package Recipes for lp:.*
111@@ -34,11 +32,9 @@
112 spr-name.* Person-name""", text)
113
114 def test_package_branch_recipe_listing(self):
115- # We can see recipes for the package. We need to create two, since
116- # only one will redirect to that recipe.
117+ # We can see recipes for the package.
118 branch = self.factory.makePackageBranch()
119 recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
120- self.factory.makeSourcePackageRecipe(branches=[branch])
121 text = self.getMainText(recipe.base_branch, '+recipes')
122 self.assertTextMatchesExpressionIgnoreWhitespace("""
123 Source Package Recipes for lp:.*
124
125=== modified file 'lib/lp/code/templates/branch-recipes.pt'
126--- lib/lp/code/templates/branch-recipes.pt 2012-06-15 16:23:50 +0000
127+++ lib/lp/code/templates/branch-recipes.pt 2015-10-05 13:41:26 +0000
128@@ -10,15 +10,7 @@
129 <div id="recipe-links" class="actions">
130 <div id="recipe-summary">
131 <img src="/@@/source-package-recipe" />
132- <tal:no-recipes condition="not: view/context/recipes/count">
133- No recipes
134- </tal:no-recipes>
135- <tal:recipes condition="view/context/recipes/count">
136- <a href="+recipes" tal:content="structure view/recipe_count_text">
137- 1 branch
138- </a>
139- </tal:recipes>
140- using this branch.
141+ <tal:recipes replace="structure view/recipes_link" />
142
143 <a href="/+help-code/related-recipes.html" target="help"
144 class="sprite maybe action-icon">(?)</a>
145
146=== modified file 'lib/lp/registry/browser/person.py'
147--- lib/lp/registry/browser/person.py 2015-10-01 10:25:19 +0000
148+++ lib/lp/registry/browser/person.py 2015-10-05 13:41:26 +0000
149@@ -274,10 +274,7 @@
150 from lp.services.webapp.publisher import LaunchpadView
151 from lp.services.worlddata.interfaces.country import ICountry
152 from lp.services.worlddata.interfaces.language import ILanguageSet
153-from lp.snappy.browser.hassnaps import (
154- HasSnapsMenuMixin,
155- HasSnapsViewMixin,
156- )
157+from lp.snappy.browser.hassnaps import HasSnapsMenuMixin
158 from lp.snappy.interfaces.snap import ISnapSet
159 from lp.soyuz.browser.archivesubscription import (
160 traverse_archive_subscription_for_subscriber,
161@@ -1653,8 +1650,7 @@
162 raise AssertionError('Unknown group to contact.')
163
164
165-class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin,
166- HasSnapsViewMixin):
167+class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin):
168 """A View class used in almost all Person's pages."""
169
170 @property
171
172=== modified file 'lib/lp/snappy/browser/hassnaps.py'
173--- lib/lp/snappy/browser/hassnaps.py 2015-09-18 10:38:27 +0000
174+++ lib/lp/snappy/browser/hassnaps.py 2015-10-05 13:41:26 +0000
175@@ -12,9 +12,13 @@
176 from zope.component import getUtility
177
178 from lp.code.browser.decorations import DecoratedBranch
179+from lp.code.interfaces.gitrepository import IGitRepository
180 from lp.services.features import getFeatureFlag
181-from lp.services.propertycache import cachedproperty
182-from lp.services.webapp import Link
183+from lp.services.webapp import (
184+ canonical_url,
185+ Link,
186+ )
187+from lp.services.webapp.escaping import structured
188 from lp.snappy.interfaces.snap import (
189 ISnapSet,
190 SNAP_FEATURE_FLAG,
191@@ -45,24 +49,38 @@
192 class HasSnapsViewMixin:
193 """A view mixin for objects that implement IHasSnaps."""
194
195- @cachedproperty
196- def snap_count(self):
197+ @property
198+ def snaps(self):
199 context = self.context
200 if isinstance(context, DecoratedBranch):
201 context = context.branch
202 return getUtility(ISnapSet).findByContext(
203- context, visible_by_user=self.user).count()
204+ context, visible_by_user=self.user)
205
206 @property
207 def show_snap_information(self):
208- return bool(getFeatureFlag(SNAP_FEATURE_FLAG)) or self.snap_count != 0
209+ return (
210+ bool(getFeatureFlag(SNAP_FEATURE_FLAG)) or
211+ not self.snaps.is_empty())
212
213 @property
214- def snap_count_text(self):
215- count = self.snap_count
216+ def snaps_link(self):
217+ """A link to snap packages for this object."""
218+ count = self.snaps.count()
219+ if IGitRepository.providedBy(self.context):
220+ context_type = 'repository'
221+ else:
222+ context_type = 'branch'
223 if count == 0:
224- return 'No snap packages'
225+ # Nothing to link to.
226+ return 'No snap packages using this %s.' % context_type
227 elif count == 1:
228- return '1 snap package'
229+ # Link to the single snap package.
230+ return structured(
231+ '<a href="%s">1 snap package</a> using this %s.',
232+ canonical_url(self.snaps.one()), context_type).escapedtext
233 else:
234- return '%s snap packages' % count
235+ # Link to a snap package listing.
236+ return structured(
237+ '<a href="+snaps">%s snap packages</a> using this %s.',
238+ count, context_type).escapedtext
239
240=== modified file 'lib/lp/snappy/browser/snaplisting.py'
241--- lib/lp/snappy/browser/snaplisting.py 2015-09-17 08:52:02 +0000
242+++ lib/lp/snappy/browser/snaplisting.py 2015-10-05 13:41:26 +0000
243@@ -18,10 +18,7 @@
244 from lp.code.browser.decorations import DecoratedBranch
245 from lp.services.database.decoratedresultset import DecoratedResultSet
246 from lp.services.feeds.browser import FeedsMixin
247-from lp.services.webapp import (
248- canonical_url,
249- LaunchpadView,
250- )
251+from lp.services.webapp import LaunchpadView
252 from lp.snappy.interfaces.snap import ISnapSet
253
254
255@@ -48,9 +45,6 @@
256 loader = partial(
257 getUtility(ISnapSet).preloadDataForSnaps, user=self.user)
258 self.snaps = DecoratedResultSet(snaps, pre_iter_hook=loader)
259- if self.snaps.count() == 1:
260- snap = self.snaps.one()
261- self.request.response.redirect(canonical_url(snap))
262
263
264 class BranchSnapListingView(SnapListingView):
265
266=== added file 'lib/lp/snappy/browser/tests/test_hassnaps.py'
267--- lib/lp/snappy/browser/tests/test_hassnaps.py 1970-01-01 00:00:00 +0000
268+++ lib/lp/snappy/browser/tests/test_hassnaps.py 2015-10-05 13:41:26 +0000
269@@ -0,0 +1,86 @@
270+# Copyright 2015 Canonical Ltd. This software is licensed under the
271+# GNU Affero General Public License version 3 (see the file LICENSE).
272+
273+"""Test views for objects that have snap packages."""
274+
275+__metaclass__ = type
276+
277+from lp.services.features.testing import FeatureFixture
278+from lp.services.webapp import canonical_url
279+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
280+from lp.testing import TestCaseWithFactory
281+from lp.testing.layers import DatabaseFunctionalLayer
282+from lp.testing.views import create_initialized_view
283+
284+
285+class TestRelatedSnapsMixin:
286+
287+ layer = DatabaseFunctionalLayer
288+
289+ def setUp(self):
290+ super(TestRelatedSnapsMixin, self).setUp()
291+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
292+
293+ def test_snaps_link_no_snaps(self):
294+ # An object with no snap packages does not show a snap packages link.
295+ context = self.makeContext()
296+ view = create_initialized_view(context, "+index")
297+ self.assertEqual(
298+ "No snap packages using this %s." % self.context_type,
299+ view.snaps_link)
300+
301+ def test_snaps_link_one_snap(self):
302+ # An object with one snap package shows a link to that snap package.
303+ context = self.makeContext()
304+ snap = self.makeSnap(context)
305+ view = create_initialized_view(context, "+index")
306+ expected_link = (
307+ '<a href="%s">1 snap package</a> using this %s.' %
308+ (canonical_url(snap), self.context_type))
309+ self.assertEqual(expected_link, view.snaps_link)
310+
311+ def test_snaps_link_more_snaps(self):
312+ # An object with more than one snap package shows a link to a listing.
313+ context = self.makeContext()
314+ self.makeSnap(context)
315+ self.makeSnap(context)
316+ view = create_initialized_view(context, "+index")
317+ expected_link = (
318+ '<a href="+snaps">2 snap packages</a> using this %s.' %
319+ self.context_type)
320+ self.assertEqual(expected_link, view.snaps_link)
321+
322+
323+class TestRelatedSnapsBranch(TestRelatedSnapsMixin, TestCaseWithFactory):
324+
325+ context_type = "branch"
326+
327+ def makeContext(self):
328+ return self.factory.makeAnyBranch()
329+
330+ def makeSnap(self, context):
331+ return self.factory.makeSnap(branch=context)
332+
333+
334+class TestRelatedSnapsGitRepository(
335+ TestRelatedSnapsMixin, TestCaseWithFactory):
336+
337+ context_type = "repository"
338+
339+ def makeContext(self):
340+ return self.factory.makeGitRepository()
341+
342+ def makeSnap(self, context):
343+ [ref] = self.factory.makeGitRefs(repository=context)
344+ return self.factory.makeSnap(git_ref=ref)
345+
346+
347+class TestRelatedSnapsGitRef(TestRelatedSnapsMixin, TestCaseWithFactory):
348+
349+ context_type = "branch"
350+
351+ def makeContext(self):
352+ return self.factory.makeGitRefs()[0]
353+
354+ def makeSnap(self, context):
355+ return self.factory.makeSnap(git_ref=context)
356
357=== modified file 'lib/lp/snappy/browser/tests/test_snaplisting.py'
358--- lib/lp/snappy/browser/tests/test_snaplisting.py 2015-09-16 13:30:33 +0000
359+++ lib/lp/snappy/browser/tests/test_snaplisting.py 2015-10-05 13:41:26 +0000
360@@ -55,20 +55,21 @@
361 self.assertThat(self.getViewBrowser(context).contents, Not(matcher))
362 login(ANONYMOUS)
363 self.makeSnap(**kwargs)
364+ self.makeSnap(**kwargs)
365 self.assertThat(self.getViewBrowser(context).contents, matcher)
366
367 def test_branch_links_to_snaps(self):
368 branch = self.factory.makeAnyBranch()
369- self.assertSnapsLink(branch, "1 snap package", branch=branch)
370+ self.assertSnapsLink(branch, "2 snap packages", branch=branch)
371
372 def test_git_repository_links_to_snaps(self):
373 repository = self.factory.makeGitRepository()
374 [ref] = self.factory.makeGitRefs(repository=repository)
375- self.assertSnapsLink(repository, "1 snap package", git_ref=ref)
376+ self.assertSnapsLink(repository, "2 snap packages", git_ref=ref)
377
378 def test_git_ref_links_to_snaps(self):
379 [ref] = self.factory.makeGitRefs()
380- self.assertSnapsLink(ref, "1 snap package", git_ref=ref)
381+ self.assertSnapsLink(ref, "2 snap packages", git_ref=ref)
382
383 def test_person_links_to_snaps(self):
384 person = self.factory.makePerson()
385@@ -83,54 +84,38 @@
386 project, "View snap packages", link_has_context=True, git_ref=ref)
387
388 def test_branch_snap_listing(self):
389- # We can see snap packages for a Bazaar branch. We need to create
390- # two, since if there's only one then +snaps will redirect to that
391- # package.
392+ # We can see snap packages for a Bazaar branch.
393 branch = self.factory.makeAnyBranch()
394- for _ in range(2):
395- self.makeSnap(branch=branch)
396+ self.makeSnap(branch=branch)
397 text = self.getMainText(branch, "+snaps")
398 self.assertTextMatchesExpressionIgnoreWhitespace("""
399 Snap packages for lp:.*
400 Name Owner Registered
401- snap-name.* Team Name.* .*
402 snap-name.* Team Name.* .*""", text)
403
404 def test_git_repository_snap_listing(self):
405- # We can see snap packages for a Git repository. We need to create
406- # two, since if there's only one then +snaps will redirect to that
407- # package.
408+ # We can see snap packages for a Git repository.
409 repository = self.factory.makeGitRepository()
410- ref1, ref2 = self.factory.makeGitRefs(
411- repository=repository,
412- paths=[u"refs/heads/branch-1", u"refs/heads/branch-2"])
413- for ref in ref1, ref2:
414- self.makeSnap(git_ref=ref)
415+ [ref] = self.factory.makeGitRefs(repository=repository)
416+ self.makeSnap(git_ref=ref)
417 text = self.getMainText(repository, "+snaps")
418 self.assertTextMatchesExpressionIgnoreWhitespace("""
419 Snap packages for lp:~.*
420 Name Owner Registered
421- snap-name.* Team Name.* .*
422 snap-name.* Team Name.* .*""", text)
423
424 def test_git_ref_snap_listing(self):
425- # We can see snap packages for a Git reference. We need to create
426- # two, since if there's only one then +snaps will redirect to that
427- # package.
428+ # We can see snap packages for a Git reference.
429 [ref] = self.factory.makeGitRefs()
430- for _ in range(2):
431- self.makeSnap(git_ref=ref)
432+ self.makeSnap(git_ref=ref)
433 text = self.getMainText(ref, "+snaps")
434 self.assertTextMatchesExpressionIgnoreWhitespace("""
435 Snap packages for ~.*:.*
436 Name Owner Registered
437- snap-name.* Team Name.* .*
438 snap-name.* Team Name.* .*""", text)
439
440 def test_person_snap_listing(self):
441- # We can see snap packages for a person. We need to create two,
442- # since if there's only one then +snaps will redirect to that
443- # package.
444+ # We can see snap packages for a person.
445 owner = self.factory.makePerson(displayname="Snap Owner")
446 self.makeSnap(
447 registrant=owner, owner=owner, branch=self.factory.makeAnyBranch(),
448@@ -146,9 +131,7 @@
449 snap-name.* lp:.* .*""", text)
450
451 def test_project_snap_listing(self):
452- # We can see snap packages for a project. We need to create two,
453- # since if there's only one then +snaps will redirect to that
454- # package.
455+ # We can see snap packages for a project.
456 project = self.factory.makeProduct(displayname="Snappable")
457 self.makeSnap(
458 branch=self.factory.makeProductBranch(product=project),
459
460=== modified file 'lib/lp/snappy/templates/snap-macros.pt'
461--- lib/lp/snappy/templates/snap-macros.pt 2015-09-18 10:15:54 +0000
462+++ lib/lp/snappy/templates/snap-macros.pt 2015-10-05 13:41:26 +0000
463@@ -14,13 +14,7 @@
464
465 <div id="snap-links" class="actions">
466 <div id="snap-summary">
467- <tal:no-snaps condition="not: view/snap_count">
468- No snap packages
469- </tal:no-snaps>
470- <tal:snaps condition="view/snap_count">
471- <a href="+snaps" tal:content="view/snap_count_text">1 snap package</a>
472- </tal:snaps>
473- using this <metal:slot define-slot="context_type">branch</metal:slot>.
474+ <tal:snaps replace="structure view/snaps_link" />
475 </div>
476 </div>
477