Merge lp:~cjwatson/launchpad/snap-listings into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17745
Proposed branch: lp:~cjwatson/launchpad/snap-listings
Merge into: lp:launchpad
Diff against target: 1446 lines (+849/-61)
29 files modified
lib/lp/code/browser/branch.py (+7/-3)
lib/lp/code/browser/gitref.py (+2/-1)
lib/lp/code/browser/gitrepository.py (+3/-1)
lib/lp/code/model/branch.py (+2/-2)
lib/lp/code/model/gitrepository.py (+2/-1)
lib/lp/code/model/tests/test_hasrecipes.py (+2/-2)
lib/lp/code/templates/branch-index.pt (+2/-2)
lib/lp/code/templates/gitref-index.pt (+1/-0)
lib/lp/code/templates/gitrepository-index.pt (+8/-0)
lib/lp/registry/browser/person.py (+8/-2)
lib/lp/registry/browser/product.py (+4/-3)
lib/lp/registry/browser/team.py (+5/-2)
lib/lp/registry/model/product.py (+0/-2)
lib/lp/registry/personmerge.py (+2/-2)
lib/lp/registry/templates/product-index.pt (+4/-0)
lib/lp/registry/tests/test_personmerge.py (+2/-2)
lib/lp/snappy/browser/configure.zcml (+37/-0)
lib/lp/snappy/browser/hassnaps.py (+60/-0)
lib/lp/snappy/browser/snap.py (+0/-9)
lib/lp/snappy/browser/snaplisting.py (+84/-0)
lib/lp/snappy/browser/tests/test_snaplisting.py (+248/-0)
lib/lp/snappy/interfaces/snap.py (+50/-2)
lib/lp/snappy/model/snap.py (+129/-1)
lib/lp/snappy/model/snapbuild.py (+11/-1)
lib/lp/snappy/templates/snap-index.pt (+2/-1)
lib/lp/snappy/templates/snap-listing.pt (+40/-0)
lib/lp/snappy/templates/snap-macros.pt (+29/-0)
lib/lp/snappy/tests/test_snap.py (+94/-18)
lib/lp/soyuz/templates/person-portlet-ppas.pt (+11/-4)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-listings
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+271307@code.launchpad.net

Commit message

Add various snap package listing views.

Description of the change

Add snap package listing views for Branch, GitRepository, GitRef, Person, and Product.

There's no batch navigation at present, partly because I based some of this on recipes which don't have that either, and partly because none of the listings seem likely to grow very large; we can of course add that later if need be.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
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-07-08 16:05:11 +0000
3+++ lib/lp/code/browser/branch.py 2015-09-17 12:55:22 +0000
4@@ -155,6 +155,10 @@
5 from lp.services.webapp.breadcrumb import NameBreadcrumb
6 from lp.services.webapp.escaping import structured
7 from lp.services.webapp.interfaces import ICanonicalUrlData
8+from lp.snappy.browser.hassnaps import (
9+ HasSnapsMenuMixin,
10+ HasSnapsViewMixin,
11+ )
12 from lp.translations.interfaces.translationtemplatesbuild import (
13 ITranslationTemplatesBuildSource,
14 )
15@@ -267,7 +271,7 @@
16 return Link('+reviewer', text, icon='edit')
17
18
19-class BranchContextMenu(ContextMenu, HasRecipesMenuMixin):
20+class BranchContextMenu(ContextMenu, HasRecipesMenuMixin, HasSnapsMenuMixin):
21 """Context menu for branches."""
22
23 usedfor = IBranch
24@@ -276,7 +280,7 @@
25 'add_subscriber', 'browse_revisions', 'create_recipe', 'link_bug',
26 'link_blueprint', 'register_merge', 'source', 'subscription',
27 'edit_status', 'edit_import', 'upgrade_branch', 'view_recipes',
28- 'visibility']
29+ 'view_snaps', 'visibility']
30
31 @enabled_with_permission('launchpad.Edit')
32 def edit_status(self):
33@@ -397,7 +401,7 @@
34
35
36 class BranchView(InformationTypePortletMixin, FeedsMixin, BranchMirrorMixin,
37- LaunchpadView):
38+ LaunchpadView, HasSnapsViewMixin):
39
40 feed_types = (
41 BranchFeedLink,
42
43=== modified file 'lib/lp/code/browser/gitref.py'
44--- lib/lp/code/browser/gitref.py 2015-05-26 12:18:12 +0000
45+++ lib/lp/code/browser/gitref.py 2015-09-17 12:55:22 +0000
46@@ -54,6 +54,7 @@
47 stepthrough,
48 )
49 from lp.services.webapp.authorization import check_permission
50+from lp.snappy.browser.hassnaps import HasSnapsViewMixin
51
52
53 # XXX cjwatson 2015-05-26: We can get rid of this after a short while, since
54@@ -88,7 +89,7 @@
55 return Link('+register-merge', text, icon='add', enabled=enabled)
56
57
58-class GitRefView(LaunchpadView):
59+class GitRefView(LaunchpadView, HasSnapsViewMixin):
60
61 @property
62 def label(self):
63
64=== modified file 'lib/lp/code/browser/gitrepository.py'
65--- lib/lp/code/browser/gitrepository.py 2015-09-10 11:20:58 +0000
66+++ lib/lp/code/browser/gitrepository.py 2015-09-17 12:55:22 +0000
67@@ -91,6 +91,7 @@
68 from lp.services.webapp.escaping import structured
69 from lp.services.webapp.interfaces import ICanonicalUrlData
70 from lp.services.webhooks.browser import WebhookTargetNavigationMixin
71+from lp.snappy.browser.hassnaps import HasSnapsViewMixin
72
73
74 @implementer(ICanonicalUrlData)
75@@ -256,7 +257,8 @@
76 return "listing sortable"
77
78
79-class GitRepositoryView(InformationTypePortletMixin, LaunchpadView):
80+class GitRepositoryView(InformationTypePortletMixin, LaunchpadView,
81+ HasSnapsViewMixin):
82
83 @property
84 def page_title(self):
85
86=== modified file 'lib/lp/code/model/branch.py'
87--- lib/lp/code/model/branch.py 2015-08-06 12:03:36 +0000
88+++ lib/lp/code/model/branch.py 2015-09-17 12:55:22 +0000
89@@ -10,7 +10,6 @@
90
91 from datetime import datetime
92 import operator
93-from urllib import quote_plus
94
95 from bzrlib import urlutils
96 from bzrlib.revision import NULL_REVISION
97@@ -180,7 +179,6 @@
98 from lp.services.propertycache import cachedproperty
99 from lp.services.webapp import urlappend
100 from lp.services.webapp.authorization import check_permission
101-from lp.snappy.interfaces.snap import ISnapSet
102
103
104 @implementer(IBranch, IPrivacy, IInformationType)
105@@ -778,6 +776,8 @@
106 As well as the dictionaries, this method returns two list of callables
107 that may be called to perform the alterations and deletions needed.
108 """
109+ from lp.snappy.interfaces.snap import ISnapSet
110+
111 alteration_operations = []
112 deletion_operations = []
113 # Merge proposals require their source and target branches to exist.
114
115=== modified file 'lib/lp/code/model/gitrepository.py'
116--- lib/lp/code/model/gitrepository.py 2015-08-10 06:39:16 +0000
117+++ lib/lp/code/model/gitrepository.py 2015-09-17 12:55:22 +0000
118@@ -149,7 +149,6 @@
119 from lp.services.webapp.authorization import available_with_permission
120 from lp.services.webhooks.interfaces import IWebhookSet
121 from lp.services.webhooks.model import WebhookTargetMixin
122-from lp.snappy.interfaces.snap import ISnapSet
123
124
125 object_type_map = {
126@@ -983,6 +982,8 @@
127 As well as the dictionaries, this method returns two list of callables
128 that may be called to perform the alterations and deletions needed.
129 """
130+ from lp.snappy.interfaces.snap import ISnapSet
131+
132 alteration_operations = []
133 deletion_operations = []
134 # Merge proposals require their source and target repositories to
135
136=== modified file 'lib/lp/code/model/tests/test_hasrecipes.py'
137--- lib/lp/code/model/tests/test_hasrecipes.py 2014-06-10 16:13:03 +0000
138+++ lib/lp/code/model/tests/test_hasrecipes.py 2015-09-17 12:55:22 +0000
139@@ -1,4 +1,4 @@
140-# Copyright 2010-2014 Canonical Ltd. This software is licensed under the
141+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
142 # GNU Affero General Public License version 3 (see the file LICENSE).
143
144 """Tests for classes that implement IHasRecipes."""
145@@ -41,7 +41,7 @@
146
147 def test_person_implements_hasrecipes(self):
148 # Person should implement IHasRecipes.
149- person = self.factory.makeBranch()
150+ person = self.factory.makePerson()
151 self.assertProvides(person, IHasRecipes)
152
153 def test_person_recipes(self):
154
155=== modified file 'lib/lp/code/templates/branch-index.pt'
156--- lib/lp/code/templates/branch-index.pt 2014-12-06 02:16:30 +0000
157+++ lib/lp/code/templates/branch-index.pt 2015-09-17 12:55:22 +0000
158@@ -93,8 +93,8 @@
159 <tal:branch-pending-merges
160 replace="structure context/@@++branch-pending-merges" />
161 <tal:branch-recipes
162- replace="structure context/@@++branch-recipes"
163- />
164+ replace="structure context/@@++branch-recipes" />
165+ <div metal:use-macro="context/@@+snap-macros/related-snaps" />
166 <tal:related-bugs-specs
167 replace="structure context/@@++branch-related-bugs-specs" />
168 </div>
169
170=== modified file 'lib/lp/code/templates/gitref-index.pt'
171--- lib/lp/code/templates/gitref-index.pt 2015-04-29 15:06:39 +0000
172+++ lib/lp/code/templates/gitref-index.pt 2015-09-17 12:55:22 +0000
173@@ -20,6 +20,7 @@
174 <div id="ref-relations" class="portlet">
175 <tal:ref-pending-merges
176 replace="structure context/@@++ref-pending-merges" />
177+ <div metal:use-macro="context/@@+snap-macros/related-snaps" />
178 </div>
179 </div>
180
181
182=== modified file 'lib/lp/code/templates/gitrepository-index.pt'
183--- lib/lp/code/templates/gitrepository-index.pt 2015-06-12 12:12:01 +0000
184+++ lib/lp/code/templates/gitrepository-index.pt 2015-09-17 12:55:22 +0000
185@@ -41,6 +41,14 @@
186 </div>
187
188 <div class="yui-g">
189+ <div id="repository-relations" class="portlet">
190+ <div metal:use-macro="context/@@+snap-macros/related-snaps">
191+ <metal:context-type fill-slot="context_type">repository</metal:context-type>
192+ </div>
193+ </div>
194+ </div>
195+
196+ <div class="yui-g">
197 <div id="repository-branches" class="portlet"
198 tal:define="branches view/branches">
199 <h2>Branches</h2>
200
201=== modified file 'lib/lp/registry/browser/person.py'
202--- lib/lp/registry/browser/person.py 2015-09-11 12:20:23 +0000
203+++ lib/lp/registry/browser/person.py 2015-09-17 12:55:22 +0000
204@@ -274,6 +274,10 @@
205 from lp.services.webapp.publisher import LaunchpadView
206 from lp.services.worlddata.interfaces.country import ICountry
207 from lp.services.worlddata.interfaces.language import ILanguageSet
208+from lp.snappy.browser.hassnaps import (
209+ HasSnapsMenuMixin,
210+ HasSnapsViewMixin,
211+ )
212 from lp.snappy.interfaces.snap import ISnapSet
213 from lp.soyuz.browser.archivesubscription import (
214 traverse_archive_subscription_for_subscriber,
215@@ -776,7 +780,7 @@
216
217
218 class PersonOverviewMenu(ApplicationMenu, PersonMenuMixin,
219- HasRecipesMenuMixin):
220+ HasRecipesMenuMixin, HasSnapsMenuMixin):
221
222 usedfor = IPerson
223 facet = 'overview'
224@@ -807,6 +811,7 @@
225 'oauth_tokens',
226 'related_software_summary',
227 'view_recipes',
228+ 'view_snaps',
229 'subscriptions',
230 'structural_subscriptions',
231 ]
232@@ -1648,7 +1653,8 @@
233 raise AssertionError('Unknown group to contact.')
234
235
236-class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin):
237+class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin,
238+ HasSnapsViewMixin):
239 """A View class used in almost all Person's pages."""
240
241 @property
242
243=== modified file 'lib/lp/registry/browser/product.py'
244--- lib/lp/registry/browser/product.py 2015-07-15 04:26:30 +0000
245+++ lib/lp/registry/browser/product.py 2015-09-17 12:55:22 +0000
246@@ -144,6 +144,7 @@
247 from lp.code.browser.branchref import BranchRef
248 from lp.code.browser.codeimport import validate_import_url
249 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
250+from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
251 from lp.code.enums import (
252 BranchType,
253 RevisionControlSystems,
254@@ -160,8 +161,6 @@
255 ICodeImportSet,
256 )
257 from lp.code.interfaces.gitrepository import IGitRepositorySet
258-from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
259-from lp.code.interfaces.gitrepository import IGitRepositorySet
260 from lp.registry.browser import (
261 add_subscribe_link,
262 BaseRdfView,
263@@ -225,6 +224,7 @@
264 from lp.services.webapp.vhosts import allvhosts
265 from lp.services.worlddata.helpers import browser_languages
266 from lp.services.worlddata.interfaces.country import ICountry
267+from lp.snappy.browser.hassnaps import HasSnapsMenuMixin
268 from lp.translations.browser.customlanguagecode import (
269 HasCustomLanguageCodesTraversalMixin,
270 )
271@@ -520,7 +520,7 @@
272
273
274 class ProductOverviewMenu(ApplicationMenu, ProductEditLinksMixin,
275- HasRecipesMenuMixin):
276+ HasRecipesMenuMixin, HasSnapsMenuMixin):
277
278 usedfor = IProduct
279 facet = 'overview'
280@@ -546,6 +546,7 @@
281 'rdf',
282 'branding',
283 'view_recipes',
284+ 'view_snaps',
285 ]
286
287 def top_contributors(self):
288
289=== modified file 'lib/lp/registry/browser/team.py'
290--- lib/lp/registry/browser/team.py 2015-07-08 16:05:11 +0000
291+++ lib/lp/registry/browser/team.py 2015-09-17 12:55:22 +0000
292@@ -1,4 +1,4 @@
293-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
294+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
295 # GNU Affero General Public License version 3 (see the file LICENSE).
296
297 __metaclass__ = type
298@@ -177,6 +177,7 @@
299 ILaunchBag,
300 IMultiFacetedBreadcrumb,
301 )
302+from lp.snappy.browser.hassnaps import HasSnapsMenuMixin
303
304
305 @implementer(IObjectPrivacy)
306@@ -1614,7 +1615,8 @@
307 return Link(target, text, icon='team', enabled=enabled)
308
309
310-class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin):
311+class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin,
312+ HasSnapsMenuMixin):
313
314 usedfor = ITeam
315 facet = 'overview'
316@@ -1643,6 +1645,7 @@
317 'ppa',
318 'related_software_summary',
319 'view_recipes',
320+ 'view_snaps',
321 'subscriptions',
322 'structural_subscriptions',
323 'upcomingwork',
324
325=== modified file 'lib/lp/registry/model/product.py'
326--- lib/lp/registry/model/product.py 2015-07-09 20:06:17 +0000
327+++ lib/lp/registry/model/product.py 2015-09-17 12:55:22 +0000
328@@ -44,8 +44,6 @@
329 SQL,
330 )
331 from storm.locals import (
332- Int,
333- List,
334 Store,
335 Unicode,
336 )
337
338=== modified file 'lib/lp/registry/personmerge.py'
339--- lib/lp/registry/personmerge.py 2015-08-03 14:22:23 +0000
340+++ lib/lp/registry/personmerge.py 2015-09-17 12:55:22 +0000
341@@ -619,9 +619,9 @@
342
343 def _mergeSnap(cur, from_person, to_person):
344 # This shouldn't use removeSecurityProxy.
345- snaps = getUtility(ISnapSet).findByPerson(from_person)
346+ snaps = getUtility(ISnapSet).findByOwner(from_person)
347 existing_names = [
348- s.name for s in getUtility(ISnapSet).findByPerson(to_person)]
349+ s.name for s in getUtility(ISnapSet).findByOwner(to_person)]
350 for snap in snaps:
351 new_name = snap.name
352 count = 1
353
354=== modified file 'lib/lp/registry/templates/product-index.pt'
355--- lib/lp/registry/templates/product-index.pt 2015-06-21 22:06:53 +0000
356+++ lib/lp/registry/templates/product-index.pt 2015-09-17 12:55:22 +0000
357@@ -203,6 +203,10 @@
358 tal:condition="link/enabled">
359 <a tal:replace="structure link/fmt:link" />
360 </li>
361+ <li tal:define="link context/menu:overview/view_snaps"
362+ tal:condition="link/enabled">
363+ <a tal:replace="structure link/fmt:link" />
364+ </li>
365 </ul>
366 </div>
367 </div>
368
369=== modified file 'lib/lp/registry/tests/test_personmerge.py'
370--- lib/lp/registry/tests/test_personmerge.py 2015-08-03 14:22:23 +0000
371+++ lib/lp/registry/tests/test_personmerge.py 2015-09-17 12:55:22 +0000
372@@ -574,7 +574,7 @@
373 self._do_premerge(duplicate, mergee)
374 login_person(mergee)
375 duplicate, mergee = self._do_merge(duplicate, mergee)
376- self.assertEqual(1, getUtility(ISnapSet).findByPerson(mergee).count())
377+ self.assertEqual(1, getUtility(ISnapSet).findByOwner(mergee).count())
378
379 def test_merge_with_duplicated_snaps(self):
380 # If both the from and to people have snap packages with the same
381@@ -592,7 +592,7 @@
382 login_person(mergee)
383 duplicate, mergee = self._do_merge(duplicate, mergee)
384 snaps = sorted(
385- getUtility(ISnapSet).findByPerson(mergee), key=attrgetter("name"))
386+ getUtility(ISnapSet).findByOwner(mergee), key=attrgetter("name"))
387 self.assertEqual(2, len(snaps))
388 self.assertIsNone(snaps[0].branch)
389 self.assertEqual(ref.repository, snaps[0].git_repository)
390
391=== modified file 'lib/lp/snappy/browser/configure.zcml'
392--- lib/lp/snappy/browser/configure.zcml 2015-09-04 16:20:26 +0000
393+++ lib/lp/snappy/browser/configure.zcml 2015-09-17 12:55:22 +0000
394@@ -91,5 +91,42 @@
395 for="lp.snappy.interfaces.snapbuild.ISnapBuild"
396 factory="lp.services.webapp.breadcrumb.TitleBreadcrumb"
397 permission="zope.Public" />
398+
399+ <browser:page
400+ for="*"
401+ class="lp.app.browser.launchpad.Macro"
402+ permission="zope.Public"
403+ name="+snap-macros"
404+ template="../templates/snap-macros.pt" />
405+ <browser:page
406+ for="lp.code.interfaces.branch.IBranch"
407+ class="lp.snappy.browser.snaplisting.BranchSnapListingView"
408+ permission="launchpad.View"
409+ name="+snaps"
410+ template="../templates/snap-listing.pt" />
411+ <browser:page
412+ for="lp.code.interfaces.gitrepository.IGitRepository"
413+ class="lp.snappy.browser.snaplisting.GitSnapListingView"
414+ permission="launchpad.View"
415+ name="+snaps"
416+ template="../templates/snap-listing.pt" />
417+ <browser:page
418+ for="lp.code.interfaces.gitref.IGitRef"
419+ class="lp.snappy.browser.snaplisting.GitSnapListingView"
420+ permission="launchpad.View"
421+ name="+snaps"
422+ template="../templates/snap-listing.pt" />
423+ <browser:page
424+ for="lp.registry.interfaces.person.IPerson"
425+ class="lp.snappy.browser.snaplisting.PersonSnapListingView"
426+ permission="launchpad.View"
427+ name="+snaps"
428+ template="../templates/snap-listing.pt" />
429+ <browser:page
430+ for="lp.registry.interfaces.product.IProduct"
431+ class="lp.snappy.browser.snaplisting.ProjectSnapListingView"
432+ permission="launchpad.View"
433+ name="+snaps"
434+ template="../templates/snap-listing.pt" />
435 </facet>
436 </configure>
437
438=== added file 'lib/lp/snappy/browser/hassnaps.py'
439--- lib/lp/snappy/browser/hassnaps.py 1970-01-01 00:00:00 +0000
440+++ lib/lp/snappy/browser/hassnaps.py 2015-09-17 12:55:22 +0000
441@@ -0,0 +1,60 @@
442+# Copyright 2015 Canonical Ltd. This software is licensed under the
443+# GNU Affero General Public License version 3 (see the file LICENSE).
444+
445+"""Mixins for browser classes for objects that implement IHasSnaps."""
446+
447+__metaclass__ = type
448+__all__ = [
449+ 'HasSnapsMenuMixin',
450+ 'HasSnapsViewMixin',
451+ ]
452+
453+from zope.component import getUtility
454+
455+from lp.code.browser.decorations import DecoratedBranch
456+from lp.services.features import getFeatureFlag
457+from lp.services.propertycache import cachedproperty
458+from lp.services.webapp import Link
459+from lp.snappy.interfaces.snap import (
460+ ISnapSet,
461+ SNAP_FEATURE_FLAG,
462+ )
463+
464+
465+class HasSnapsMenuMixin:
466+ """A mixin for context menus for objects that implement IHasSnaps."""
467+
468+ def view_snaps(self):
469+ text = 'View snap packages'
470+ context = self.context
471+ if isinstance(context, DecoratedBranch):
472+ context = context.branch
473+ enabled = not getUtility(ISnapSet).findByContext(
474+ context, visible_by_user=self.user).is_empty()
475+ return Link('+snaps', text, icon='info', enabled=enabled)
476+
477+
478+class HasSnapsViewMixin:
479+ """A view mixin for objects that implement IHasSnaps."""
480+
481+ @cachedproperty
482+ def snap_count(self):
483+ context = self.context
484+ if isinstance(context, DecoratedBranch):
485+ context = context.branch
486+ return getUtility(ISnapSet).findByContext(
487+ context, visible_by_user=self.user).count()
488+
489+ @property
490+ def show_snap_information(self):
491+ return bool(getFeatureFlag(SNAP_FEATURE_FLAG)) or self.snap_count != 0
492+
493+ @property
494+ def snap_count_text(self):
495+ count = self.snap_count
496+ if count == 0:
497+ return 'No snap packages'
498+ elif count == 1:
499+ return '1 snap package'
500+ else:
501+ return '%s snap packages' % count
502
503=== modified file 'lib/lp/snappy/browser/snap.py'
504--- lib/lp/snappy/browser/snap.py 2015-09-09 14:17:46 +0000
505+++ lib/lp/snappy/browser/snap.py 2015-09-17 12:55:22 +0000
506@@ -113,15 +113,6 @@
507 self.context, field, format_link(self.context.owner),
508 header='Change owner', step_title='Select a new owner')
509
510- @property
511- def source(self):
512- if self.context.branch is not None:
513- return self.context.branch
514- elif self.context.git_ref is not None:
515- return self.context.git_ref
516- else:
517- return None
518-
519
520 def builds_for_snap(snap):
521 """A list of interesting builds.
522
523=== added file 'lib/lp/snappy/browser/snaplisting.py'
524--- lib/lp/snappy/browser/snaplisting.py 1970-01-01 00:00:00 +0000
525+++ lib/lp/snappy/browser/snaplisting.py 2015-09-17 12:55:22 +0000
526@@ -0,0 +1,84 @@
527+# Copyright 2015 Canonical Ltd. This software is licensed under the
528+# GNU Affero General Public License version 3 (see the file LICENSE).
529+
530+"""Base class view for snap listings."""
531+
532+__metaclass__ = type
533+
534+__all__ = [
535+ 'BranchSnapListingView',
536+ 'GitSnapListingView',
537+ 'PersonSnapListingView',
538+ ]
539+
540+from functools import partial
541+
542+from zope.component import getUtility
543+
544+from lp.code.browser.decorations import DecoratedBranch
545+from lp.services.database.decoratedresultset import DecoratedResultSet
546+from lp.services.feeds.browser import FeedsMixin
547+from lp.services.webapp import (
548+ canonical_url,
549+ LaunchpadView,
550+ )
551+from lp.snappy.interfaces.snap import ISnapSet
552+
553+
554+class SnapListingView(LaunchpadView, FeedsMixin):
555+
556+ feed_types = ()
557+
558+ source_enabled = True
559+ owner_enabled = True
560+
561+ @property
562+ def page_title(self):
563+ return 'Snap packages'
564+
565+ @property
566+ def label(self):
567+ return 'Snap packages for %(displayname)s' % {
568+ 'displayname': self.context.displayname}
569+
570+ def initialize(self):
571+ super(SnapListingView, self).initialize()
572+ snaps = getUtility(ISnapSet).findByContext(
573+ self.context, visible_by_user=self.user)
574+ loader = partial(
575+ getUtility(ISnapSet).preloadDataForSnaps, user=self.user)
576+ self.snaps = DecoratedResultSet(snaps, pre_iter_hook=loader)
577+ if self.snaps.count() == 1:
578+ snap = self.snaps.one()
579+ self.request.response.redirect(canonical_url(snap))
580+
581+
582+class BranchSnapListingView(SnapListingView):
583+
584+ source_enabled = False
585+
586+ def initialize(self):
587+ super(BranchSnapListingView, self).initialize()
588+ # Replace our context with a decorated branch, if it is not already
589+ # decorated.
590+ if not isinstance(self.context, DecoratedBranch):
591+ self.context = DecoratedBranch(self.context)
592+
593+
594+class GitSnapListingView(SnapListingView):
595+
596+ source_enabled = False
597+
598+ @property
599+ def label(self):
600+ return 'Snap packages for %(display_name)s' % {
601+ 'display_name': self.context.display_name}
602+
603+
604+class PersonSnapListingView(SnapListingView):
605+
606+ owner_enabled = False
607+
608+
609+class ProjectSnapListingView(SnapListingView):
610+ pass
611
612=== added file 'lib/lp/snappy/browser/tests/test_snaplisting.py'
613--- lib/lp/snappy/browser/tests/test_snaplisting.py 1970-01-01 00:00:00 +0000
614+++ lib/lp/snappy/browser/tests/test_snaplisting.py 2015-09-17 12:55:22 +0000
615@@ -0,0 +1,248 @@
616+# Copyright 2015 Canonical Ltd. This software is licensed under the
617+# GNU Affero General Public License version 3 (see the file LICENSE).
618+
619+"""Test snap package listings."""
620+
621+__metaclass__ = type
622+
623+import soupmatchers
624+from testtools.matchers import (
625+ Equals,
626+ Not,
627+ )
628+
629+from lp.services.database.constants import (
630+ ONE_DAY_AGO,
631+ UTC_NOW,
632+ )
633+from lp.services.features.testing import FeatureFixture
634+from lp.services.webapp import canonical_url
635+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
636+from lp.testing import (
637+ ANONYMOUS,
638+ BrowserTestCase,
639+ login,
640+ person_logged_in,
641+ record_two_runs,
642+ )
643+from lp.testing.layers import DatabaseFunctionalLayer
644+from lp.testing.matchers import HasQueryCount
645+
646+
647+class TestSnapListing(BrowserTestCase):
648+
649+ layer = DatabaseFunctionalLayer
650+
651+ def makeSnap(self, **kwargs):
652+ """Create a snap package, enabling the feature flag.
653+
654+ We do things this way rather than by calling self.useFixture because
655+ opening a URL in a test browser loses the thread-local feature flag.
656+ """
657+ with FeatureFixture({SNAP_FEATURE_FLAG: u"on"}):
658+ return self.factory.makeSnap(**kwargs)
659+
660+ def assertSnapsLink(self, context, link_text, link_has_context=False,
661+ **kwargs):
662+ if link_has_context:
663+ expected_href = canonical_url(context, view_name="+snaps")
664+ else:
665+ expected_href = "+snaps"
666+ matcher = soupmatchers.HTMLContains(
667+ soupmatchers.Tag(
668+ "View snap packages link", "a", text=link_text,
669+ attrs={"href": expected_href}))
670+ self.assertThat(self.getViewBrowser(context).contents, Not(matcher))
671+ login(ANONYMOUS)
672+ self.makeSnap(**kwargs)
673+ self.assertThat(self.getViewBrowser(context).contents, matcher)
674+
675+ def test_branch_links_to_snaps(self):
676+ branch = self.factory.makeAnyBranch()
677+ self.assertSnapsLink(branch, "1 snap package", branch=branch)
678+
679+ def test_git_repository_links_to_snaps(self):
680+ repository = self.factory.makeGitRepository()
681+ [ref] = self.factory.makeGitRefs(repository=repository)
682+ self.assertSnapsLink(repository, "1 snap package", git_ref=ref)
683+
684+ def test_git_ref_links_to_snaps(self):
685+ [ref] = self.factory.makeGitRefs()
686+ self.assertSnapsLink(ref, "1 snap package", git_ref=ref)
687+
688+ def test_person_links_to_snaps(self):
689+ person = self.factory.makePerson()
690+ self.assertSnapsLink(
691+ person, "View snap packages", link_has_context=True,
692+ registrant=person, owner=person)
693+
694+ def test_project_links_to_snaps(self):
695+ project = self.factory.makeProduct()
696+ [ref] = self.factory.makeGitRefs(target=project)
697+ self.assertSnapsLink(
698+ project, "View snap packages", link_has_context=True, git_ref=ref)
699+
700+ def test_branch_snap_listing(self):
701+ # We can see snap packages for a Bazaar branch. We need to create
702+ # two, since if there's only one then +snaps will redirect to that
703+ # package.
704+ branch = self.factory.makeAnyBranch()
705+ for _ in range(2):
706+ self.makeSnap(branch=branch)
707+ text = self.getMainText(branch, "+snaps")
708+ self.assertTextMatchesExpressionIgnoreWhitespace("""
709+ Snap packages for lp:.*
710+ Name Owner Registered
711+ snap-name.* Team Name.* .*
712+ snap-name.* Team Name.* .*""", text)
713+
714+ def test_git_repository_snap_listing(self):
715+ # We can see snap packages for a Git repository. We need to create
716+ # two, since if there's only one then +snaps will redirect to that
717+ # package.
718+ repository = self.factory.makeGitRepository()
719+ ref1, ref2 = self.factory.makeGitRefs(
720+ repository=repository,
721+ paths=[u"refs/heads/branch-1", u"refs/heads/branch-2"])
722+ for ref in ref1, ref2:
723+ self.makeSnap(git_ref=ref)
724+ text = self.getMainText(repository, "+snaps")
725+ self.assertTextMatchesExpressionIgnoreWhitespace("""
726+ Snap packages for lp:~.*
727+ Name Owner Registered
728+ snap-name.* Team Name.* .*
729+ snap-name.* Team Name.* .*""", text)
730+
731+ def test_git_ref_snap_listing(self):
732+ # We can see snap packages for a Git reference. We need to create
733+ # two, since if there's only one then +snaps will redirect to that
734+ # package.
735+ [ref] = self.factory.makeGitRefs()
736+ for _ in range(2):
737+ self.makeSnap(git_ref=ref)
738+ text = self.getMainText(ref, "+snaps")
739+ self.assertTextMatchesExpressionIgnoreWhitespace("""
740+ Snap packages for ~.*:.*
741+ Name Owner Registered
742+ snap-name.* Team Name.* .*
743+ snap-name.* Team Name.* .*""", text)
744+
745+ def test_person_snap_listing(self):
746+ # We can see snap packages for a person. We need to create two,
747+ # since if there's only one then +snaps will redirect to that
748+ # package.
749+ owner = self.factory.makePerson(displayname="Snap Owner")
750+ self.makeSnap(
751+ registrant=owner, owner=owner, branch=self.factory.makeAnyBranch(),
752+ date_created=ONE_DAY_AGO)
753+ [ref] = self.factory.makeGitRefs()
754+ self.makeSnap(
755+ registrant=owner, owner=owner, git_ref=ref, date_created=UTC_NOW)
756+ text = self.getMainText(owner, "+snaps")
757+ self.assertTextMatchesExpressionIgnoreWhitespace("""
758+ Snap packages for Snap Owner
759+ Name Source Registered
760+ snap-name.* ~.*:.* .*
761+ snap-name.* lp:.* .*""", text)
762+
763+ def test_project_snap_listing(self):
764+ # We can see snap packages for a project. We need to create two,
765+ # since if there's only one then +snaps will redirect to that
766+ # package.
767+ project = self.factory.makeProduct(displayname="Snappable")
768+ self.makeSnap(
769+ branch=self.factory.makeProductBranch(product=project),
770+ date_created=ONE_DAY_AGO)
771+ [ref] = self.factory.makeGitRefs(target=project)
772+ self.makeSnap(git_ref=ref, date_created=UTC_NOW)
773+ text = self.getMainText(project, "+snaps")
774+ self.assertTextMatchesExpressionIgnoreWhitespace("""
775+ Snap packages for Snappable
776+ Name Owner Source Registered
777+ snap-name.* Team Name.* ~.*:.* .*
778+ snap-name.* Team Name.* lp:.* .*""", text)
779+
780+ def assertSnapsQueryCount(self, context, item_creator):
781+ recorder1, recorder2 = record_two_runs(
782+ lambda: self.getMainText(context, "+snaps"), item_creator, 5)
783+ self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
784+
785+ def test_branch_query_count(self):
786+ # The number of queries required to render the list of all snap
787+ # packages for a Bazaar branch is constant in the number of owners
788+ # and snap packages.
789+ person = self.factory.makePerson()
790+ branch = self.factory.makeAnyBranch(owner=person)
791+
792+ def create_snap():
793+ with person_logged_in(person):
794+ self.makeSnap(branch=branch)
795+
796+ self.assertSnapsQueryCount(branch, create_snap)
797+
798+ def test_git_repository_query_count(self):
799+ # The number of queries required to render the list of all snap
800+ # packages for a Git repository is constant in the number of owners
801+ # and snap packages.
802+ person = self.factory.makePerson()
803+ repository = self.factory.makeGitRepository(owner=person)
804+
805+ def create_snap():
806+ with person_logged_in(person):
807+ [ref] = self.factory.makeGitRefs(repository=repository)
808+ self.makeSnap(git_ref=ref)
809+
810+ self.assertSnapsQueryCount(repository, create_snap)
811+
812+ def test_git_ref_query_count(self):
813+ # The number of queries required to render the list of all snap
814+ # packages for a Git reference is constant in the number of owners
815+ # and snap packages.
816+ person = self.factory.makePerson()
817+ [ref] = self.factory.makeGitRefs(owner=person)
818+
819+ def create_snap():
820+ with person_logged_in(person):
821+ self.makeSnap(git_ref=ref)
822+
823+ self.assertSnapsQueryCount(ref, create_snap)
824+
825+ def test_person_query_count(self):
826+ # The number of queries required to render the list of all snap
827+ # packages for a person is constant in the number of projects,
828+ # sources, and snap packages.
829+ person = self.factory.makePerson()
830+ i = 0
831+
832+ def create_snap():
833+ with person_logged_in(person):
834+ project = self.factory.makeProduct()
835+ if (i % 2) == 0:
836+ branch = self.factory.makeProductBranch(
837+ owner=person, product=project)
838+ self.makeSnap(branch=branch)
839+ else:
840+ [ref] = self.factory.makeGitRefs(
841+ owner=person, target=project)
842+ self.makeSnap(git_ref=ref)
843+
844+ self.assertSnapsQueryCount(person, create_snap)
845+
846+ def test_project_query_count(self):
847+ # The number of queries required to render the list of all snap
848+ # packages for a person is constant in the number of owners,
849+ # sources, and snap packages.
850+ person = self.factory.makePerson()
851+ project = self.factory.makeProduct(owner=person)
852+ i = 0
853+
854+ def create_snap():
855+ with person_logged_in(person):
856+ if (i % 2) == 0:
857+ branch = self.factory.makeProductBranch(product=project)
858+ self.makeSnap(branch=branch)
859+ else:
860+ [ref] = self.factory.makeGitRefs(target=project)
861+ self.makeSnap(git_ref=ref)
862+
863+ self.assertSnapsQueryCount(project, create_snap)
864
865=== modified file 'lib/lp/snappy/interfaces/snap.py'
866--- lib/lp/snappy/interfaces/snap.py 2015-09-09 14:17:46 +0000
867+++ lib/lp/snappy/interfaces/snap.py 2015-09-17 12:55:22 +0000
868@@ -6,6 +6,7 @@
869 __metaclass__ = type
870
871 __all__ = [
872+ 'BadSnapSearchContext',
873 'CannotDeleteSnap',
874 'DuplicateSnapName',
875 'ISnap',
876@@ -45,7 +46,10 @@
877 Reference,
878 ReferenceChoice,
879 )
880-from zope.interface import Interface
881+from zope.interface import (
882+ Attribute,
883+ Interface,
884+ )
885 from zope.schema import (
886 Bool,
887 Choice,
888@@ -163,6 +167,10 @@
889 """This snap package cannot be deleted."""
890
891
892+class BadSnapSearchContext(Exception):
893+ """The context is not valid for a snap package search."""
894+
895+
896 class ISnapView(Interface):
897 """`ISnap` attributes that require launchpad.View permission."""
898
899@@ -176,6 +184,9 @@
900 vocabulary="ValidPersonOrTeam",
901 description=_("The person who registered this snap package.")))
902
903+ source = Attribute(
904+ "The source branch for this snap package (VCS-agnostic).")
905+
906 @call_with(requester=REQUEST_USER)
907 @operation_parameters(
908 archive=Reference(schema=IArchive),
909@@ -351,15 +362,52 @@
910 def getByName(owner, name):
911 """Return the appropriate `ISnap` for the given objects."""
912
913- def findByPerson(owner):
914+ def findByOwner(owner):
915 """Return all snap packages with the given `owner`."""
916
917+ def findByPerson(person, visible_by_user=None):
918+ """Return all snap packages relevant to `person`.
919+
920+ This returns snap packages for Bazaar or Git branches owned by
921+ `person`, or where `person` is the owner of the snap package.
922+
923+ :param person: An `IPerson`.
924+ :param visible_by_user: If not None, only return packages visible by
925+ this user.
926+ """
927+
928+ def findByProject(project, visible_by_user=None):
929+ """Return all snap packages for the given project.
930+
931+ :param project: An `IProduct`.
932+ :param visible_by_user: If not None, only return packages visible by
933+ this user.
934+ """
935+
936 def findByBranch(branch):
937 """Return all snap packages for the given Bazaar branch."""
938
939 def findByGitRepository(repository):
940 """Return all snap packages for the given Git repository."""
941
942+ def findByGitRef(ref):
943+ """Return all snap packages for the given Git reference."""
944+
945+ def findByContext(context, visible_by_user=None, order_by_date=True):
946+ """Return all snap packages for the given context.
947+
948+ :param context: An `IPerson`, `IProduct, `IBranch`,
949+ `IGitRepository`, or `IGitRef`.
950+ :param visible_by_user: If not None, only return packages visible by
951+ this user.
952+ :param order_by_date: If True, order packages by descending
953+ modification date.
954+ :raises BadSnapSearchContext: if the context is not understood.
955+ """
956+
957+ def preloadDataForSnaps(snaps, user):
958+ """Load the data related to a list of snap packages."""
959+
960 def detachFromBranch(branch):
961 """Detach all snap packages from the given Bazaar branch.
962
963
964=== modified file 'lib/lp/snappy/model/snap.py'
965--- lib/lp/snappy/model/snap.py 2015-09-10 17:15:45 +0000
966+++ lib/lp/snappy/model/snap.py 2015-09-17 12:55:22 +0000
967@@ -26,7 +26,28 @@
968 from lp.buildmaster.enums import BuildStatus
969 from lp.buildmaster.interfaces.processor import IProcessorSet
970 from lp.buildmaster.model.processor import Processor
971+from lp.code.interfaces.branch import IBranch
972+from lp.code.interfaces.branchcollection import (
973+ IAllBranches,
974+ IBranchCollection,
975+ )
976+from lp.code.interfaces.gitcollection import (
977+ IAllGitRepositories,
978+ IGitCollection,
979+ )
980+from lp.code.interfaces.gitref import IGitRef
981+from lp.code.interfaces.gitrepository import IGitRepository
982+from lp.code.model.branch import Branch
983+from lp.code.model.branchcollection import GenericBranchCollection
984+from lp.code.model.gitcollection import GenericGitCollection
985+from lp.code.model.gitrepository import GitRepository
986+from lp.registry.interfaces.person import (
987+ IPerson,
988+ IPersonSet,
989+ )
990+from lp.registry.interfaces.product import IProduct
991 from lp.registry.interfaces.role import IHasOwner
992+from lp.services.database.bulk import load_related
993 from lp.services.database.constants import (
994 DEFAULT,
995 UTC_NOW,
996@@ -42,6 +63,7 @@
997 from lp.services.features import getFeatureFlag
998 from lp.services.webapp.interfaces import ILaunchBag
999 from lp.snappy.interfaces.snap import (
1000+ BadSnapSearchContext,
1001 CannotDeleteSnap,
1002 DuplicateSnapName,
1003 ISnap,
1004@@ -146,6 +168,15 @@
1005 self.git_repository = None
1006 self.git_path = None
1007
1008+ @property
1009+ def source(self):
1010+ if self.branch is not None:
1011+ return self.branch
1012+ elif self.git_ref is not None:
1013+ return self.git_ref
1014+ else:
1015+ return None
1016+
1017 def _getProcessors(self):
1018 return list(Store.of(self).find(
1019 Processor,
1020@@ -348,10 +379,44 @@
1021 raise NoSuchSnap(name)
1022 return snap
1023
1024- def findByPerson(self, owner):
1025+ def _getSnapsFromCollection(self, collection, owner=None):
1026+ if IBranchCollection.providedBy(collection):
1027+ id_column = Snap.branch_id
1028+ ids = collection.getBranchIds()
1029+ else:
1030+ id_column = Snap.git_repository_id
1031+ ids = collection.getRepositoryIds()
1032+ expressions = [id_column.is_in(ids._get_select())]
1033+ if owner is not None:
1034+ expressions.append(Snap.owner == owner)
1035+ return IStore(Snap).find(Snap, *expressions)
1036+
1037+ def findByOwner(self, owner):
1038 """See `ISnapSet`."""
1039 return IStore(Snap).find(Snap, Snap.owner == owner)
1040
1041+ def findByPerson(self, person, visible_by_user=None):
1042+ """See `ISnapSet`."""
1043+ def _getSnaps(collection):
1044+ collection = collection.visibleByUser(visible_by_user)
1045+ owned = self._getSnapsFromCollection(collection.ownedBy(person))
1046+ packaged = self._getSnapsFromCollection(collection, owner=person)
1047+ return owned.union(packaged)
1048+
1049+ bzr_collection = removeSecurityProxy(getUtility(IAllBranches))
1050+ git_collection = removeSecurityProxy(getUtility(IAllGitRepositories))
1051+ return _getSnaps(bzr_collection).union(_getSnaps(git_collection))
1052+
1053+ def findByProject(self, project, visible_by_user=None):
1054+ """See `ISnapSet`."""
1055+ def _getSnaps(collection):
1056+ return self._getSnapsFromCollection(
1057+ collection.visibleByUser(visible_by_user))
1058+
1059+ bzr_collection = removeSecurityProxy(IBranchCollection(project))
1060+ git_collection = removeSecurityProxy(IGitCollection(project))
1061+ return _getSnaps(bzr_collection).union(_getSnaps(git_collection))
1062+
1063 def findByBranch(self, branch):
1064 """See `ISnapSet`."""
1065 return IStore(Snap).find(Snap, Snap.branch == branch)
1066@@ -360,6 +425,69 @@
1067 """See `ISnapSet`."""
1068 return IStore(Snap).find(Snap, Snap.git_repository == repository)
1069
1070+ def findByGitRef(self, ref):
1071+ """See `ISnapSet`."""
1072+ return IStore(Snap).find(
1073+ Snap,
1074+ Snap.git_repository == ref.repository, Snap.git_path == ref.path)
1075+
1076+ def findByContext(self, context, visible_by_user=None, order_by_date=True):
1077+ if IPerson.providedBy(context):
1078+ snaps = self.findByPerson(context, visible_by_user=visible_by_user)
1079+ elif IProduct.providedBy(context):
1080+ snaps = self.findByProject(
1081+ context, visible_by_user=visible_by_user)
1082+ # XXX cjwatson 2015-09-15: At the moment we can assume that if you
1083+ # can see the source context then you can see the snap packages
1084+ # based on it. This will cease to be true if snap packages gain
1085+ # privacy of their own.
1086+ elif IBranch.providedBy(context):
1087+ snaps = self.findByBranch(context)
1088+ elif IGitRepository.providedBy(context):
1089+ snaps = self.findByGitRepository(context)
1090+ elif IGitRef.providedBy(context):
1091+ snaps = self.findByGitRef(context)
1092+ else:
1093+ raise BadSnapSearchContext(context)
1094+ if order_by_date:
1095+ snaps.order_by(Desc(Snap.date_last_modified))
1096+ return snaps
1097+
1098+ def preloadDataForSnaps(self, snaps, user=None):
1099+ """See `ISnapSet`."""
1100+ snaps = [removeSecurityProxy(snap) for snap in snaps]
1101+
1102+ branch_ids = set()
1103+ git_repository_ids = set()
1104+ person_ids = set()
1105+ for snap in snaps:
1106+ if snap.branch_id is not None:
1107+ branch_ids.add(snap.branch_id)
1108+ if snap.git_repository_id is not None:
1109+ git_repository_ids.add(snap.git_repository_id)
1110+ person_ids.add(snap.registrant_id)
1111+ person_ids.add(snap.owner_id)
1112+
1113+ branches = load_related(Branch, snaps, ["branch_id"])
1114+ repositories = load_related(
1115+ GitRepository, snaps, ["git_repository_id"])
1116+ if branches:
1117+ GenericBranchCollection.preloadDataForBranches(branches)
1118+ if repositories:
1119+ GenericGitCollection.preloadDataForRepositories(repositories)
1120+ # The stacked-on branches are used to check branch visibility.
1121+ GenericBranchCollection.preloadVisibleStackedOnBranches(branches, user)
1122+ GenericGitCollection.preloadVisibleRepositories(repositories, user)
1123+
1124+ # Add branch/repository owners to the list of pre-loaded persons.
1125+ # We need the target repository owner as well; unlike branches,
1126+ # repository unique names aren't trigger-maintained.
1127+ person_ids.update(branch.ownerID for branch in branches)
1128+ person_ids.update(repository.owner_id for repository in repositories)
1129+
1130+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
1131+ person_ids, need_validity=True))
1132+
1133 def detachFromBranch(self, branch):
1134 """See `ISnapSet`."""
1135 self.findByBranch(branch).set(
1136
1137=== modified file 'lib/lp/snappy/model/snapbuild.py'
1138--- lib/lp/snappy/model/snapbuild.py 2015-08-03 13:20:45 +0000
1139+++ lib/lp/snappy/model/snapbuild.py 2015-09-17 12:55:22 +0000
1140@@ -33,6 +33,8 @@
1141 from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin
1142 from lp.buildmaster.model.packagebuild import PackageBuildMixin
1143 from lp.registry.interfaces.pocket import PackagePublishingPocket
1144+from lp.registry.model.distribution import Distribution
1145+from lp.registry.model.distroseries import DistroSeries
1146 from lp.registry.model.person import Person
1147 from lp.services.config import config
1148 from lp.services.database.bulk import load_related
1149@@ -50,6 +52,7 @@
1150 LibraryFileContent,
1151 )
1152 from lp.snappy.interfaces.snap import (
1153+ ISnapSet,
1154 SNAP_FEATURE_FLAG,
1155 SnapFeatureDisabled,
1156 )
1157@@ -61,6 +64,7 @@
1158 from lp.snappy.mail.snapbuild import SnapBuildMailer
1159 from lp.soyuz.interfaces.component import IComponentSet
1160 from lp.soyuz.model.archive import Archive
1161+from lp.soyuz.model.distroarchseries import DistroArchSeries
1162
1163
1164 @implementer(ISnapFile)
1165@@ -357,7 +361,13 @@
1166 load_related(LibraryFileAlias, builds, ["log_id"])
1167 archives = load_related(Archive, builds, ["archive_id"])
1168 load_related(Person, archives, ["ownerID"])
1169- load_related(Snap, builds, ["snap_id"])
1170+ distroarchseries = load_related(
1171+ DistroArchSeries, builds, ['distro_arch_series_id'])
1172+ distroseries = load_related(
1173+ DistroSeries, distroarchseries, ['distroseriesID'])
1174+ load_related(Distribution, distroseries, ['distributionID'])
1175+ snaps = load_related(Snap, builds, ["snap_id"])
1176+ getUtility(ISnapSet).preloadDataForSnaps(snaps)
1177
1178 def getByBuildFarmJobs(self, build_farm_jobs):
1179 """See `ISpecificBuildFarmJobSource`."""
1180
1181=== modified file 'lib/lp/snappy/templates/snap-index.pt'
1182--- lib/lp/snappy/templates/snap-index.pt 2015-09-07 15:29:00 +0000
1183+++ lib/lp/snappy/templates/snap-index.pt 2015-09-17 12:55:22 +0000
1184@@ -40,7 +40,8 @@
1185 <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
1186 </dd>
1187 </dl>
1188- <dl id="source" tal:define="source view/source" tal:condition="source">
1189+ <dl id="source"
1190+ tal:define="source context/source" tal:condition="source">
1191 <dt>Source:</dt>
1192 <dd>
1193 <a tal:replace="structure source/fmt:link"/>
1194
1195=== added file 'lib/lp/snappy/templates/snap-listing.pt'
1196--- lib/lp/snappy/templates/snap-listing.pt 1970-01-01 00:00:00 +0000
1197+++ lib/lp/snappy/templates/snap-listing.pt 2015-09-17 12:55:22 +0000
1198@@ -0,0 +1,40 @@
1199+<html
1200+ xmlns="http://www.w3.org/1999/xhtml"
1201+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1202+ xmlns:metal="http://xml.zope.org/namespaces/metal"
1203+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1204+ metal:use-macro="view/macro:page/main_only"
1205+ i18n:domain="launchpad">
1206+
1207+<body>
1208+
1209+ <div metal:fill-slot="main">
1210+
1211+ <table id="snaptable" class="listing sortable">
1212+ <thead>
1213+ <tr>
1214+ <th colspan="2">Name</th>
1215+ <th tal:condition="view/owner_enabled">Owner</th>
1216+ <th tal:condition="view/source_enabled">Source</th>
1217+ <th>Registered</th>
1218+ </tr>
1219+ </thead>
1220+ <tbody>
1221+ <tal:snaps repeat="snap view/snaps">
1222+ <tr>
1223+ <td colspan="2">
1224+ <a tal:attributes="href snap/fmt:url" tal:content="snap/name" />
1225+ </td>
1226+ <td tal:condition="view/owner_enabled"
1227+ tal:content="structure snap/owner/fmt:link" />
1228+ <td tal:condition="view/source_enabled"
1229+ tal:content="structure snap/source/fmt:link" />
1230+ <td tal:content="snap/date_created/fmt:datetime" />
1231+ </tr>
1232+ </tal:snaps>
1233+ </tbody>
1234+ </table>
1235+
1236+ </div>
1237+</body>
1238+</html>
1239
1240=== added file 'lib/lp/snappy/templates/snap-macros.pt'
1241--- lib/lp/snappy/templates/snap-macros.pt 1970-01-01 00:00:00 +0000
1242+++ lib/lp/snappy/templates/snap-macros.pt 2015-09-17 12:55:22 +0000
1243@@ -0,0 +1,29 @@
1244+<tal:root
1245+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1246+ xmlns:metal="http://xml.zope.org/namespaces/metal"
1247+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1248+ omit-tag="">
1249+
1250+<div
1251+ metal:define-macro="related-snaps"
1252+ tal:condition="view/show_snap_information"
1253+ tal:define="context_menu context/menu:context"
1254+ id="related-snaps">
1255+
1256+ <h3>Related snap packages</h3>
1257+
1258+ <div id="snap-links" class="actions">
1259+ <div id="snap-summary">
1260+ <tal:no-snaps condition="not: view/snap_count">
1261+ No snap packages
1262+ </tal:no-snaps>
1263+ <tal:snaps condition="view/snap_count">
1264+ <a href="+snaps" tal:content="view/snap_count_text">1 snap package</a>
1265+ </tal:snaps>
1266+ using this <metal:slot define-slot="context_type">branch</metal:slot>.
1267+ </div>
1268+ </div>
1269+
1270+</div>
1271+
1272+</tal:root>
1273
1274=== modified file 'lib/lp/snappy/tests/test_snap.py'
1275--- lib/lp/snappy/tests/test_snap.py 2015-09-09 14:17:46 +0000
1276+++ lib/lp/snappy/tests/test_snap.py 2015-09-17 12:55:22 +0000
1277@@ -31,6 +31,7 @@
1278 from lp.services.features.testing import FeatureFixture
1279 from lp.services.webapp.interfaces import OAuthPermission
1280 from lp.snappy.interfaces.snap import (
1281+ BadSnapSearchContext,
1282 CannotDeleteSnap,
1283 ISnap,
1284 ISnapSet,
1285@@ -434,18 +435,51 @@
1286 getUtility(ISnapSet).exists(self.factory.makePerson(), snap.name))
1287 self.assertFalse(getUtility(ISnapSet).exists(snap.owner, u"different"))
1288
1289+ def test_findByOwner(self):
1290+ # ISnapSet.findByOwner returns all Snaps with the given owner.
1291+ owners = [self.factory.makePerson() for i in range(2)]
1292+ snaps = []
1293+ for owner in owners:
1294+ for i in range(2):
1295+ snaps.append(self.factory.makeSnap(
1296+ registrant=owner, owner=owner))
1297+ snap_set = getUtility(ISnapSet)
1298+ self.assertContentEqual(snaps[:2], snap_set.findByOwner(owners[0]))
1299+ self.assertContentEqual(snaps[2:], snap_set.findByOwner(owners[1]))
1300+
1301 def test_findByPerson(self):
1302- # ISnapSet.findByPerson returns all Snaps with the given owner.
1303+ # ISnapSet.findByPerson returns all Snaps with the given owner or
1304+ # based on branches or repositories with the given owner.
1305 owners = [self.factory.makePerson() for i in range(2)]
1306 snaps = []
1307 for owner in owners:
1308- for i in range(2):
1309- snaps.append(self.factory.makeSnap(
1310- registrant=owner, owner=owner))
1311- self.assertContentEqual(
1312- snaps[:2], getUtility(ISnapSet).findByPerson(owners[0]))
1313- self.assertContentEqual(
1314- snaps[2:], getUtility(ISnapSet).findByPerson(owners[1]))
1315+ snaps.append(self.factory.makeSnap(registrant=owner, owner=owner))
1316+ snaps.append(self.factory.makeSnap(
1317+ branch=self.factory.makeAnyBranch(owner=owner)))
1318+ [ref] = self.factory.makeGitRefs(owner=owner)
1319+ snaps.append(self.factory.makeSnap(git_ref=ref))
1320+ snap_set = getUtility(ISnapSet)
1321+ self.assertContentEqual(snaps[:3], snap_set.findByPerson(owners[0]))
1322+ self.assertContentEqual(snaps[3:], snap_set.findByPerson(owners[1]))
1323+
1324+ def test_findByProject(self):
1325+ # ISnapSet.findByProject returns all Snaps based on branches or
1326+ # repositories for the given project.
1327+ projects = [self.factory.makeProduct() for i in range(2)]
1328+ snaps = []
1329+ for project in projects:
1330+ snaps.append(self.factory.makeSnap(
1331+ branch=self.factory.makeProductBranch(product=project)))
1332+ [ref] = self.factory.makeGitRefs(target=project)
1333+ snaps.append(self.factory.makeSnap(git_ref=ref))
1334+ snaps.append(self.factory.makeSnap(
1335+ branch=self.factory.makePersonalBranch()))
1336+ [ref] = self.factory.makeGitRefs(target=None)
1337+ snaps.append(self.factory.makeSnap(git_ref=ref))
1338+ snap_set = getUtility(ISnapSet)
1339+ self.assertContentEqual(snaps[:2], snap_set.findByProject(projects[0]))
1340+ self.assertContentEqual(
1341+ snaps[2:4], snap_set.findByProject(projects[1]))
1342
1343 def test_findByBranch(self):
1344 # ISnapSet.findByBranch returns all Snaps with the given Bazaar branch.
1345@@ -454,10 +488,9 @@
1346 for branch in branches:
1347 for i in range(2):
1348 snaps.append(self.factory.makeSnap(branch=branch))
1349- self.assertContentEqual(
1350- snaps[:2], getUtility(ISnapSet).findByBranch(branches[0]))
1351- self.assertContentEqual(
1352- snaps[2:], getUtility(ISnapSet).findByBranch(branches[1]))
1353+ snap_set = getUtility(ISnapSet)
1354+ self.assertContentEqual(snaps[:2], snap_set.findByBranch(branches[0]))
1355+ self.assertContentEqual(snaps[2:], snap_set.findByBranch(branches[1]))
1356
1357 def test_findByGitRepository(self):
1358 # ISnapSet.findByGitRepository returns all Snaps with the given Git
1359@@ -468,12 +501,55 @@
1360 for i in range(2):
1361 [ref] = self.factory.makeGitRefs(repository=repository)
1362 snaps.append(self.factory.makeSnap(git_ref=ref))
1363- self.assertContentEqual(
1364- snaps[:2],
1365- getUtility(ISnapSet).findByGitRepository(repositories[0]))
1366- self.assertContentEqual(
1367- snaps[2:],
1368- getUtility(ISnapSet).findByGitRepository(repositories[1]))
1369+ snap_set = getUtility(ISnapSet)
1370+ self.assertContentEqual(
1371+ snaps[:2], snap_set.findByGitRepository(repositories[0]))
1372+ self.assertContentEqual(
1373+ snaps[2:], snap_set.findByGitRepository(repositories[1]))
1374+
1375+ def test_findByGitRef(self):
1376+ # ISnapSet.findByGitRef returns all Snaps with the given Git
1377+ # reference.
1378+ repositories = [self.factory.makeGitRepository() for i in range(2)]
1379+ refs = []
1380+ snaps = []
1381+ for repository in repositories:
1382+ refs.extend(self.factory.makeGitRefs(
1383+ paths=[u"refs/heads/master", u"refs/heads/other"]))
1384+ snaps.append(self.factory.makeSnap(git_ref=refs[-2]))
1385+ snaps.append(self.factory.makeSnap(git_ref=refs[-1]))
1386+ snap_set = getUtility(ISnapSet)
1387+ for ref, snap in zip(refs, snaps):
1388+ self.assertContentEqual([snap], snap_set.findByGitRef(ref))
1389+
1390+ def test_findByContext(self):
1391+ # ISnapSet.findByContext returns all Snaps with the given context.
1392+ person = self.factory.makePerson()
1393+ project = self.factory.makeProduct()
1394+ branch = self.factory.makeProductBranch(owner=person, product=project)
1395+ other_branch = self.factory.makeProductBranch()
1396+ repository = self.factory.makeGitRepository(target=project)
1397+ refs = self.factory.makeGitRefs(
1398+ repository=repository,
1399+ paths=[u"refs/heads/master", u"refs/heads/other"])
1400+ snaps = []
1401+ snaps.append(self.factory.makeSnap(branch=branch))
1402+ snaps.append(self.factory.makeSnap(branch=other_branch))
1403+ snaps.append(
1404+ self.factory.makeSnap(
1405+ registrant=person, owner=person, git_ref=refs[0]))
1406+ snaps.append(self.factory.makeSnap(git_ref=refs[1]))
1407+ snap_set = getUtility(ISnapSet)
1408+ self.assertContentEqual(
1409+ [snaps[0], snaps[2]], snap_set.findByContext(person))
1410+ self.assertContentEqual(
1411+ [snaps[0], snaps[2], snaps[3]], snap_set.findByContext(project))
1412+ self.assertContentEqual([snaps[0]], snap_set.findByContext(branch))
1413+ self.assertContentEqual(snaps[2:], snap_set.findByContext(repository))
1414+ self.assertContentEqual([snaps[2]], snap_set.findByContext(refs[0]))
1415+ self.assertRaises(
1416+ BadSnapSearchContext, snap_set.findByContext,
1417+ self.factory.makeDistribution())
1418
1419 def test_detachFromBranch(self):
1420 # ISnapSet.detachFromBranch clears the given Bazaar branch from all
1421
1422=== modified file 'lib/lp/soyuz/templates/person-portlet-ppas.pt'
1423--- lib/lp/soyuz/templates/person-portlet-ppas.pt 2012-11-01 03:41:36 +0000
1424+++ lib/lp/soyuz/templates/person-portlet-ppas.pt 2015-09-17 12:55:22 +0000
1425@@ -31,10 +31,17 @@
1426
1427 </ul>
1428 </div>
1429- <div tal:define="link context/menu:overview/view_recipes"
1430- tal:condition="link/enabled">
1431- <a tal:replace="structure link/fmt:link" />
1432- </div>
1433+ <ul class="horizontal" style="margin-top: 0;"
1434+ tal:define="recipes_link context/menu:overview/view_recipes;
1435+ snaps_link context/menu:overview/view_snaps"
1436+ tal:condition="python: recipes_link.enabled or snaps_link.enabled">
1437+ <li tal:condition="recipes_link/enabled">
1438+ <a tal:replace="structure recipes_link/fmt:link" />
1439+ </li>
1440+ <li tal:condition="snaps_link/enabled">
1441+ <a tal:replace="structure snaps_link/fmt:link" />
1442+ </li>
1443+ </ul>
1444
1445
1446 </tal:root>