Merge lp:~cjwatson/launchpad/snap-listings into lp:launchpad
- snap-listings
- Merge into devel
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 | ||||
Related bugs: |
|
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> |