Merge lp:~rvb/launchpad/sync-bug-827608-add-synchronized-packages-redone into lp:launchpad
- sync-bug-827608-add-synchronized-packages-redone
- Merge into devel
Proposed by
Raphaël Badin
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Raphaël Badin | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 14021 | ||||
Proposed branch: | lp:~rvb/launchpad/sync-bug-827608-add-synchronized-packages-redone | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~rvb/launchpad/sync-bug-827608-populate-ancestor-redone | ||||
Diff against target: |
822 lines (+548/-14) 10 files modified
lib/lp/registry/browser/person.py (+116/-8) lib/lp/registry/browser/tests/test_person_view.py (+149/-6) lib/lp/registry/interfaces/person.py (+8/-0) lib/lp/registry/model/person.py (+35/-0) lib/lp/registry/templates/person-macros.pt (+62/-0) lib/lp/registry/templates/person-related-software-navlinks.pt (+4/-0) lib/lp/registry/templates/person-related-software.pt (+26/-0) lib/lp/registry/tests/test_person.py (+83/-0) lib/lp/soyuz/browser/configure.zcml (+6/-0) lib/lp/soyuz/templates/person-synchronised-packages.pt (+59/-0) |
||||
To merge this branch: | bzr merge lp:~rvb/launchpad/sync-bug-827608-add-synchronized-packages-redone | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email: mp+76690@code.launchpad.net |
Commit message
[r=allenap][bug=827608] Display synchronised packages on +related-package and +synchronised-
Description of the change
This is exactly the same MP as the one that has been approved: https:/
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/registry/browser/person.py' |
2 | --- lib/lp/registry/browser/person.py 2011-09-21 01:41:08 +0000 |
3 | +++ lib/lp/registry/browser/person.py 2011-09-23 08:02:27 +0000 |
4 | @@ -326,6 +326,7 @@ |
5 | from lp.soyuz.interfaces.archive import IArchiveSet |
6 | from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet |
7 | from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet |
8 | +from lp.soyuz.interfaces.publishing import ISourcePackagePublishingHistory |
9 | from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease |
10 | |
11 | |
12 | @@ -990,6 +991,13 @@ |
13 | enabled = bool(self.person.getLatestUploadedPPAPackages()) |
14 | return Link(target, text, enabled=enabled, icon='info') |
15 | |
16 | + def synchronised(self): |
17 | + target = '+synchronised-packages' |
18 | + text = 'Synchronised packages' |
19 | + enabled = bool( |
20 | + self.person.getLatestSynchronisedPublishings()) |
21 | + return Link(target, text, enabled=enabled, icon='info') |
22 | + |
23 | def projects(self): |
24 | target = '+related-projects' |
25 | text = 'Related projects' |
26 | @@ -1058,6 +1066,7 @@ |
27 | 'projects', |
28 | 'activate_ppa', |
29 | 'maintained', |
30 | + 'synchronised', |
31 | 'view_ppa_subscriptions', |
32 | 'ppa', |
33 | 'oauth_tokens', |
34 | @@ -1193,7 +1202,7 @@ |
35 | usedfor = IPersonRelatedSoftwareMenu |
36 | facet = 'overview' |
37 | links = ('related_software_summary', 'maintained', 'uploaded', 'ppa', |
38 | - 'projects') |
39 | + 'synchronised', 'projects') |
40 | |
41 | @property |
42 | def person(self): |
43 | @@ -5160,23 +5169,38 @@ |
44 | return Link('+subscribedquestions', text, summary, icon='question') |
45 | |
46 | |
47 | -class SourcePackageReleaseWithStats: |
48 | - """An ISourcePackageRelease, with extra stats added.""" |
49 | - |
50 | - implements(ISourcePackageRelease) |
51 | - delegates(ISourcePackageRelease) |
52 | +class BaseWithStats: |
53 | + """An ISourcePackageRelease or a ISourcePackagePublishingHistory, |
54 | + with extra stats added. |
55 | + |
56 | + """ |
57 | + |
58 | failed_builds = None |
59 | needs_building = None |
60 | |
61 | - def __init__(self, sourcepackage_release, open_bugs, open_questions, |
62 | + def __init__(self, object, open_bugs, open_questions, |
63 | failed_builds, needs_building): |
64 | - self.context = sourcepackage_release |
65 | + self.context = object |
66 | self.open_bugs = open_bugs |
67 | self.open_questions = open_questions |
68 | self.failed_builds = failed_builds |
69 | self.needs_building = needs_building |
70 | |
71 | |
72 | +class SourcePackageReleaseWithStats(BaseWithStats): |
73 | + """An ISourcePackageRelease, with extra stats added.""" |
74 | + |
75 | + implements(ISourcePackageRelease) |
76 | + delegates(ISourcePackageRelease) |
77 | + |
78 | + |
79 | +class SourcePackagePublishingHistoryWithStats(BaseWithStats): |
80 | + """An ISourcePackagePublishingHistory, with extra stats added.""" |
81 | + |
82 | + implements(ISourcePackagePublishingHistory) |
83 | + delegates(ISourcePackagePublishingHistory) |
84 | + |
85 | + |
86 | class PersonRelatedSoftwareView(LaunchpadView): |
87 | """View for +related-software.""" |
88 | implements(IPersonRelatedSoftwareMenu) |
89 | @@ -5302,6 +5326,23 @@ |
90 | header_message = self._tableHeaderMessage(packages.count()) |
91 | return results, header_message |
92 | |
93 | + def _getDecoratedPublishingsSummary(self, publishings): |
94 | + """Helper returning decorated publishings for the summary page. |
95 | + |
96 | + :param publishings: A SelectResults that contains the query |
97 | + :return: A tuple of (publishings, header_message). |
98 | + |
99 | + The publishings returned are limited to self.max_results_to_display |
100 | + and decorated with the stats required in the page template. |
101 | + The header_message is the text to be displayed at the top of the |
102 | + results table in the template. |
103 | + """ |
104 | + # This code causes two SQL queries to be generated. |
105 | + results = self._addStatsToPublishings( |
106 | + publishings[:self.max_results_to_display]) |
107 | + header_message = self._tableHeaderMessage(publishings.count()) |
108 | + return results, header_message |
109 | + |
110 | @property |
111 | def latest_uploaded_ppa_packages_with_stats(self): |
112 | """Return the sourcepackagereleases uploaded to PPAs by this person. |
113 | @@ -5333,6 +5374,17 @@ |
114 | self.uploaded_packages_header_message = header_message |
115 | return results |
116 | |
117 | + @property |
118 | + def latest_synchronised_publishings_with_stats(self): |
119 | + """Return the latest synchronised publishings, including stats. |
120 | + |
121 | + """ |
122 | + publishings = self.context.getLatestSynchronisedPublishings() |
123 | + results, header_message = self._getDecoratedPublishingsSummary( |
124 | + publishings) |
125 | + self.synchronised_packages_header_message = header_message |
126 | + return results |
127 | + |
128 | def _calculateBuildStats(self, package_releases): |
129 | """Calculate failed builds and needs_build state. |
130 | |
131 | @@ -5394,6 +5446,38 @@ |
132 | needs_build_by_package[package]) |
133 | for package in package_releases] |
134 | |
135 | + def _addStatsToPublishings(self, publishings): |
136 | + """Add stats to the given publishings, and return them.""" |
137 | + filtered_spphs = [ |
138 | + spph for spph in publishings if |
139 | + check_permission('launchpad.View', spph)] |
140 | + distro_packages = [ |
141 | + spph.meta_sourcepackage.distribution_sourcepackage |
142 | + for spph in filtered_spphs] |
143 | + package_bug_counts = getUtility(IBugTaskSet).getBugCountsForPackages( |
144 | + self.user, distro_packages) |
145 | + open_bugs = {} |
146 | + for bug_count in package_bug_counts: |
147 | + distro_package = bug_count['package'] |
148 | + open_bugs[distro_package] = bug_count['open'] |
149 | + |
150 | + question_set = getUtility(IQuestionSet) |
151 | + package_question_counts = question_set.getOpenQuestionCountByPackages( |
152 | + distro_packages) |
153 | + |
154 | + builds_by_package, needs_build_by_package = self._calculateBuildStats( |
155 | + [spph.sourcepackagerelease for spph in filtered_spphs]) |
156 | + |
157 | + return [ |
158 | + SourcePackagePublishingHistoryWithStats( |
159 | + spph, |
160 | + open_bugs[spph.meta_sourcepackage.distribution_sourcepackage], |
161 | + package_question_counts[ |
162 | + spph.meta_sourcepackage.distribution_sourcepackage], |
163 | + builds_by_package[spph.sourcepackagerelease], |
164 | + needs_build_by_package[spph.sourcepackagerelease]) |
165 | + for spph in filtered_spphs] |
166 | + |
167 | def setUpBatch(self, packages): |
168 | """Set up the batch navigation for the page being viewed. |
169 | |
170 | @@ -5454,6 +5538,30 @@ |
171 | return "PPA packages" |
172 | |
173 | |
174 | +class PersonSynchronisedPackagesView(PersonRelatedSoftwareView): |
175 | + """View for +synchronised-packages.""" |
176 | + _max_results_key = 'default_batch_size' |
177 | + |
178 | + def initialize(self): |
179 | + """Set up the batch navigation.""" |
180 | + publishings = self.context.getLatestSynchronisedPublishings() |
181 | + self.setUpBatch(publishings) |
182 | + |
183 | + def setUpBatch(self, publishings): |
184 | + """Set up the batch navigation for the page being viewed. |
185 | + |
186 | + This method creates the BatchNavigator and converts its |
187 | + results batch into a list of decorated sourcepackagepublishinghistory. |
188 | + """ |
189 | + self.batchnav = BatchNavigator(publishings, self.request) |
190 | + publishings_batch = list(self.batchnav.currentBatch()) |
191 | + self.batch = self._addStatsToPublishings(publishings_batch) |
192 | + |
193 | + @property |
194 | + def page_title(self): |
195 | + return "Synchronised packages" |
196 | + |
197 | + |
198 | class PersonRelatedProjectsView(PersonRelatedSoftwareView): |
199 | """View for +related-projects.""" |
200 | _max_results_key = 'default_batch_size' |
201 | |
202 | === modified file 'lib/lp/registry/browser/tests/test_person_view.py' |
203 | --- lib/lp/registry/browser/tests/test_person_view.py 2011-09-21 01:41:08 +0000 |
204 | +++ lib/lp/registry/browser/tests/test_person_view.py 2011-09-23 08:02:27 +0000 |
205 | @@ -1,15 +1,17 @@ |
206 | -# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
207 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
208 | # GNU Affero General Public License version 3 (see the file LICENSE). |
209 | |
210 | __metaclass__ = type |
211 | |
212 | import doctest |
213 | |
214 | +import soupmatchers |
215 | from storm.expr import LeftJoin |
216 | from storm.store import Store |
217 | from testtools.matchers import ( |
218 | DocTestMatches, |
219 | LessThan, |
220 | + Not, |
221 | ) |
222 | import transaction |
223 | from zope.component import getUtility |
224 | @@ -23,6 +25,7 @@ |
225 | from canonical.launchpad.interfaces.authtoken import LoginTokenType |
226 | from canonical.launchpad.interfaces.logintoken import ILoginTokenSet |
227 | from canonical.launchpad.testing.pages import extract_text |
228 | +from canonical.launchpad.webapp import canonical_url |
229 | from canonical.launchpad.webapp.interfaces import ILaunchBag |
230 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
231 | from canonical.testing.layers import ( |
232 | @@ -45,6 +48,7 @@ |
233 | PersonVisibility, |
234 | ) |
235 | from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource |
236 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
237 | from lp.registry.interfaces.teammembership import ( |
238 | ITeamMembershipSet, |
239 | TeamMembershipStatus, |
240 | @@ -528,20 +532,31 @@ |
241 | self.warty = self.ubuntu.getSeries('warty') |
242 | self.view = create_initialized_view(self.user, '+related-software') |
243 | |
244 | - def publishSource(self, archive, maintainer): |
245 | + def publishSources(self, archive, maintainer): |
246 | publisher = SoyuzTestPublisher() |
247 | publisher.person = self.user |
248 | login('foo.bar@canonical.com') |
249 | + spphs = [] |
250 | for count in range(0, self.view.max_results_to_display + 3): |
251 | source_name = "foo" + str(count) |
252 | - publisher.getPubSource( |
253 | + spph = publisher.getPubSource( |
254 | sourcename=source_name, |
255 | status=PackagePublishingStatus.PUBLISHED, |
256 | archive=archive, |
257 | maintainer=maintainer, |
258 | creator=self.user, |
259 | distroseries=self.warty) |
260 | + spphs.append(spph) |
261 | login(ANONYMOUS) |
262 | + return spphs |
263 | + |
264 | + def copySources(self, spphs, copier, dest_distroseries): |
265 | + self.copier = self.factory.makePerson() |
266 | + for spph in spphs: |
267 | + spph.copyTo( |
268 | + dest_distroseries, creator=copier, |
269 | + pocket=PackagePublishingPocket.UPDATES, |
270 | + archive=dest_distroseries.main_archive) |
271 | |
272 | def test_view_helper_attributes(self): |
273 | # Verify view helper attributes. |
274 | @@ -563,24 +578,34 @@ |
275 | def test_latest_uploaded_ppa_packages_with_stats(self): |
276 | # Verify number of PPA packages to display. |
277 | ppa = self.factory.makeArchive(owner=self.user) |
278 | - self.publishSource(ppa, self.user) |
279 | + self.publishSources(ppa, self.user) |
280 | count = len(self.view.latest_uploaded_ppa_packages_with_stats) |
281 | self.assertEqual(self.view.max_results_to_display, count) |
282 | |
283 | def test_latest_maintained_packages_with_stats(self): |
284 | # Verify number of maintained packages to display. |
285 | - self.publishSource(self.warty.main_archive, self.user) |
286 | + self.publishSources(self.warty.main_archive, self.user) |
287 | count = len(self.view.latest_maintained_packages_with_stats) |
288 | self.assertEqual(self.view.max_results_to_display, count) |
289 | |
290 | def test_latest_uploaded_nonmaintained_packages_with_stats(self): |
291 | # Verify number of non maintained packages to display. |
292 | maintainer = self.factory.makePerson() |
293 | - self.publishSource(self.warty.main_archive, maintainer) |
294 | + self.publishSources(self.warty.main_archive, maintainer) |
295 | count = len( |
296 | self.view.latest_uploaded_but_not_maintained_packages_with_stats) |
297 | self.assertEqual(self.view.max_results_to_display, count) |
298 | |
299 | + def test_latest_synchronised_publishings_with_stats(self): |
300 | + # Verify number of non synchronised publishings to display. |
301 | + creator = self.factory.makePerson() |
302 | + spphs = self.publishSources(self.warty.main_archive, creator) |
303 | + dest_distroseries = self.factory.makeDistroSeries() |
304 | + self.copySources(spphs, self.user, dest_distroseries) |
305 | + count = len( |
306 | + self.view.latest_synchronised_publishings_with_stats) |
307 | + self.assertEqual(self.view.max_results_to_display, count) |
308 | + |
309 | |
310 | class TestPersonMaintainedPackagesView(TestCaseWithFactory): |
311 | """Test the maintained packages view.""" |
312 | @@ -654,6 +679,55 @@ |
313 | self.view.max_results_to_display) |
314 | |
315 | |
316 | +class TestPersonSynchronisedPackagesView(TestCaseWithFactory): |
317 | + """Test the synchronised packages view.""" |
318 | + |
319 | + layer = DatabaseFunctionalLayer |
320 | + |
321 | + def setUp(self): |
322 | + super(TestPersonSynchronisedPackagesView, self).setUp() |
323 | + user = self.factory.makePerson() |
324 | + archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY) |
325 | + spr = self.factory.makeSourcePackageRelease( |
326 | + creator=user, archive=archive) |
327 | + spph = self.factory.makeSourcePackagePublishingHistory( |
328 | + sourcepackagerelease=spr, archive=archive) |
329 | + self.copier = self.factory.makePerson() |
330 | + dest_distroseries = self.factory.makeDistroSeries() |
331 | + self.copied_spph = spph.copyTo( |
332 | + dest_distroseries, creator=self.copier, |
333 | + pocket=PackagePublishingPocket.UPDATES, |
334 | + archive=dest_distroseries.main_archive) |
335 | + self.view = create_initialized_view( |
336 | + self.copier, '+synchronised-packages') |
337 | + |
338 | + def test_view_helper_attributes(self): |
339 | + # Verify view helper attributes. |
340 | + self.assertEqual('Synchronised packages', self.view.page_title) |
341 | + self.assertEqual('default_batch_size', self.view._max_results_key) |
342 | + self.assertEqual( |
343 | + config.launchpad.default_batch_size, |
344 | + self.view.max_results_to_display) |
345 | + |
346 | + def test_verify_bugs_and_answers_links(self): |
347 | + # Verify the links for bugs and answers point to locations that |
348 | + # exist. |
349 | + html = self.view() |
350 | + expected_base = '/%s/+source/%s' % ( |
351 | + self.copied_spph.distroseries.distribution.name, |
352 | + self.copied_spph.source_package_name) |
353 | + bug_matcher = soupmatchers.HTMLContains( |
354 | + soupmatchers.Tag( |
355 | + 'Bugs link', 'a', |
356 | + attrs={'href': expected_base + '/+bugs'})) |
357 | + question_matcher = soupmatchers.HTMLContains( |
358 | + soupmatchers.Tag( |
359 | + 'Questions link', 'a', |
360 | + attrs={'href': expected_base + '/+questions'})) |
361 | + self.assertThat(html, bug_matcher) |
362 | + self.assertThat(html, question_matcher) |
363 | + |
364 | + |
365 | class TestPersonRelatedProjectsView(TestCaseWithFactory): |
366 | """Test the maintained packages view.""" |
367 | |
368 | @@ -718,6 +792,75 @@ |
369 | self.build.id) in html) |
370 | |
371 | |
372 | +class TestPersonRelatedSoftwareSynchronisedPackages(TestCaseWithFactory): |
373 | + """The related software views display links to synchronised packages.""" |
374 | + |
375 | + layer = LaunchpadFunctionalLayer |
376 | + |
377 | + def setUp(self): |
378 | + super(TestPersonRelatedSoftwareSynchronisedPackages, self).setUp() |
379 | + self.user = self.factory.makePerson() |
380 | + self.spph = self.factory.makeSourcePackagePublishingHistory() |
381 | + |
382 | + def createCopiedSource(self, copier, spph): |
383 | + self.copier = self.factory.makePerson() |
384 | + dest_distroseries = self.factory.makeDistroSeries() |
385 | + return spph.copyTo( |
386 | + dest_distroseries, creator=copier, |
387 | + pocket=PackagePublishingPocket.UPDATES, |
388 | + archive=dest_distroseries.main_archive) |
389 | + |
390 | + def getLinkToSynchronisedMatcher(self): |
391 | + person_url = canonical_url(self.user) |
392 | + return soupmatchers.HTMLContains( |
393 | + soupmatchers.Tag( |
394 | + 'Synchronised packages link', 'a', |
395 | + attrs={'href': person_url + '/+synchronised-packages'}, |
396 | + text='Synchronised packages')) |
397 | + |
398 | + def test_related_software_no_link_synchronised_packages(self): |
399 | + # No link to the synchronised packages page if no synchronised |
400 | + # packages. |
401 | + view = create_view(self.user, name='+related-software') |
402 | + synced_package_link_matcher = self.getLinkToSynchronisedMatcher() |
403 | + self.assertThat(view(), Not(synced_package_link_matcher)) |
404 | + |
405 | + def test_related_software_link_synchronised_packages(self): |
406 | + # If this person has synced packages, the link to the synchronised |
407 | + # packages page is present. |
408 | + self.createCopiedSource(self.user, self.spph) |
409 | + view = create_view(self.user, name='+related-software') |
410 | + synced_package_link_matcher = self.getLinkToSynchronisedMatcher() |
411 | + self.assertThat(view(), synced_package_link_matcher) |
412 | + |
413 | + def test_related_software_displays_synchronised_packages(self): |
414 | + copied_spph = self.createCopiedSource(self.user, self.spph) |
415 | + view = create_view(self.user, name='+related-software') |
416 | + synced_packages_title = soupmatchers.HTMLContains( |
417 | + soupmatchers.Tag( |
418 | + 'Synchronised packages title', 'h2', |
419 | + text='Synchronised packages')) |
420 | + expected_base = '/%s/+source/%s' % ( |
421 | + copied_spph.distroseries.distribution.name, |
422 | + copied_spph.source_package_name) |
423 | + source_link = soupmatchers.HTMLContains( |
424 | + soupmatchers.Tag( |
425 | + 'Source package link', 'a', |
426 | + text=copied_spph.sourcepackagerelease.name, |
427 | + attrs={'href': expected_base})) |
428 | + version_url = (expected_base + '/%s' % |
429 | + copied_spph.sourcepackagerelease.version) |
430 | + version_link = soupmatchers.HTMLContains( |
431 | + soupmatchers.Tag( |
432 | + 'Source package version link', 'a', |
433 | + text=copied_spph.sourcepackagerelease.version, |
434 | + attrs={'href': version_url})) |
435 | + |
436 | + self.assertThat(view(), synced_packages_title) |
437 | + self.assertThat(view(), source_link) |
438 | + self.assertThat(view(), version_link) |
439 | + |
440 | + |
441 | class TestPersonDeactivateAccountView(TestCaseWithFactory): |
442 | """Tests for the PersonDeactivateAccountView.""" |
443 | |
444 | |
445 | === modified file 'lib/lp/registry/interfaces/person.py' |
446 | --- lib/lp/registry/interfaces/person.py 2011-09-21 01:41:08 +0000 |
447 | +++ lib/lp/registry/interfaces/person.py 2011-09-23 08:02:27 +0000 |
448 | @@ -1240,6 +1240,14 @@ |
449 | for each source package name, distribution series combination. |
450 | """ |
451 | |
452 | + def getLatestSynchronisedPublishings(): |
453 | + """Return `SourcePackagePublishingHistory`s synchronised by this |
454 | + person. |
455 | + |
456 | + This method will only include the latest publishings for each source |
457 | + package name, distribution series combination. |
458 | + """ |
459 | + |
460 | def getLatestUploadedButNotMaintainedPackages(): |
461 | """Return `SourcePackageRelease`s created by this person but |
462 | not maintained by him. |
463 | |
464 | === modified file 'lib/lp/registry/model/person.py' |
465 | --- lib/lp/registry/model/person.py 2011-09-21 01:41:08 +0000 |
466 | +++ lib/lp/registry/model/person.py 2011-09-23 08:02:27 +0000 |
467 | @@ -297,6 +297,7 @@ |
468 | from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet |
469 | from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet |
470 | from lp.soyuz.model.archive import Archive |
471 | +from lp.soyuz.model.publishing import SourcePackagePublishingHistory |
472 | from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease |
473 | from lp.translations.model.hastranslationimports import ( |
474 | HasTranslationImportsMixin, |
475 | @@ -2586,6 +2587,40 @@ |
476 | """See `IPerson`.""" |
477 | return self._latestSeriesQuery() |
478 | |
479 | + def getLatestSynchronisedPublishings(self): |
480 | + """See `IPerson`.""" |
481 | + query = """ |
482 | + SourcePackagePublishingHistory.id IN ( |
483 | + SELECT DISTINCT ON (spph.distroseries, |
484 | + spr.sourcepackagename) |
485 | + spph.id |
486 | + FROM |
487 | + SourcePackagePublishingHistory as spph, archive, |
488 | + SourcePackagePublishingHistory as ancestor_spph, |
489 | + SourcePackageRelease as spr |
490 | + WHERE |
491 | + spph.sourcepackagerelease = spr.id AND |
492 | + spph.creator = %(creator)s AND |
493 | + spph.ancestor = ancestor_spph.id AND |
494 | + spph.archive = archive.id AND |
495 | + ancestor_spph.archive != spph.archive AND |
496 | + archive.purpose = %(archive_purpose)s |
497 | + ORDER BY spph.distroseries, |
498 | + spr.sourcepackagename, |
499 | + spph.datecreated DESC, |
500 | + spph.id DESC |
501 | + ) |
502 | + """ % dict( |
503 | + creator=quote(self.id), |
504 | + archive_purpose=quote(ArchivePurpose.PRIMARY), |
505 | + ) |
506 | + |
507 | + return SourcePackagePublishingHistory.select( |
508 | + query, |
509 | + orderBy=['-SourcePackagePublishingHistory.datecreated', |
510 | + '-SourcePackagePublishingHistory.id'], |
511 | + prejoins=['sourcepackagerelease', 'archive']) |
512 | + |
513 | def getLatestUploadedButNotMaintainedPackages(self): |
514 | """See `IPerson`.""" |
515 | return self._latestSeriesQuery(uploader_only=True) |
516 | |
517 | === modified file 'lib/lp/registry/templates/person-macros.pt' |
518 | --- lib/lp/registry/templates/person-macros.pt 2011-09-21 01:41:08 +0000 |
519 | +++ lib/lp/registry/templates/person-macros.pt 2011-09-23 08:02:27 +0000 |
520 | @@ -181,6 +181,68 @@ |
521 | </tr> |
522 | </metal:macro> |
523 | |
524 | +<metal:macro define-macro="spphs-rows"> |
525 | + |
526 | + <tal:comment replace="nothing"> |
527 | + This macro expects the following variables defined: |
528 | + :spphs: A list of SourcePackagePublishingHistory objects |
529 | + </tal:comment> |
530 | + |
531 | + <tr tal:repeat="spph spphs"> |
532 | + <tal:define define="spr spph/sourcepackagerelease; |
533 | + distroseries spph/distroseries"> |
534 | + <td> |
535 | + <a tal:attributes="href string:${distroseries/distribution/fmt:url}/+source/${spr/name}" |
536 | + class="distrosrcpackage" |
537 | + tal:content="spr/sourcepackagename/name"> |
538 | + </a> |
539 | + </td> |
540 | + <td> |
541 | + <a tal:attributes="href string:${distroseries/fmt:url}/+source/${spr/name}" |
542 | + class="distroseriessrcpackage" |
543 | + tal:content="distroseries/fullseriesname"> |
544 | + </a> |
545 | + </td> |
546 | + <td> |
547 | + <a tal:attributes="href string:${distroseries/distribution/fmt:url}/+source/${spr/name}/${spr/version}" |
548 | + class="distrosrcpackagerelease" |
549 | + tal:content="spr/version"> |
550 | + </a> |
551 | + </td> |
552 | + <td |
553 | + tal:attributes="title spph/datecreated/fmt:datetime" |
554 | + tal:content="spph/datecreated/fmt:approximatedate"> |
555 | + 2005-10-24 |
556 | + </td> |
557 | + <td> |
558 | + <tal:needs_building condition="spph/needs_building"> |
559 | + Not yet built |
560 | + </tal:needs_building> |
561 | + <tal:built condition="not: spph/needs_building"> |
562 | + <tal:failed repeat="build spph/failed_builds"> |
563 | + <a tal:attributes="href build/fmt:url" |
564 | + tal:content="build/distro_arch_series/architecturetag" /> |
565 | + </tal:failed> |
566 | + <tal:not_failed condition="not: spph/failed_builds"> |
567 | + None |
568 | + </tal:not_failed> |
569 | + </tal:built> |
570 | + </td> |
571 | + <td style="text-align: right"> |
572 | + <a tal:attributes="href string:${spph/meta_sourcepackage/distribution_sourcepackage/fmt:url}/+bugs" |
573 | + tal:content="spph/open_bugs"> |
574 | + </a> |
575 | + </td> |
576 | + <td style="text-align: right"> |
577 | + <a tal:attributes="href string:${spph/meta_sourcepackage/distribution_sourcepackage/fmt:url}/+questions" |
578 | + tal:content="spph/open_questions"> |
579 | + </a> |
580 | + </td> |
581 | + </tal:define> |
582 | + </tr> |
583 | +</metal:macro> |
584 | + |
585 | + |
586 | <metal:macro define-macro="private-team-js"> |
587 | <tal:comment replace="nothing"> |
588 | This macro inserts the javascript necessary to automatically insert the |
589 | |
590 | === modified file 'lib/lp/registry/templates/person-related-software-navlinks.pt' |
591 | --- lib/lp/registry/templates/person-related-software-navlinks.pt 2009-10-16 00:47:43 +0000 |
592 | +++ lib/lp/registry/templates/person-related-software-navlinks.pt 2011-09-23 08:02:27 +0000 |
593 | @@ -22,6 +22,10 @@ |
594 | tal:condition="link/enabled" |
595 | tal:content="structure link/fmt:link" /> |
596 | <li |
597 | + tal:define="link view/menu:navigation/synchronised" |
598 | + tal:condition="link/enabled" |
599 | + tal:content="structure link/fmt:link" /> |
600 | + <li |
601 | tal:define="link view/menu:navigation/projects" |
602 | tal:condition="link/enabled" |
603 | tal:content="structure link/fmt:link" /> |
604 | |
605 | === modified file 'lib/lp/registry/templates/person-related-software.pt' |
606 | --- lib/lp/registry/templates/person-related-software.pt 2011-09-21 01:41:08 +0000 |
607 | +++ lib/lp/registry/templates/person-related-software.pt 2011-09-23 08:02:27 +0000 |
608 | @@ -99,6 +99,32 @@ |
609 | </div> |
610 | </tal:ppa-packages> |
611 | |
612 | + <tal:synchronised-packages |
613 | + define="spphs view/latest_synchronised_publishings_with_stats" |
614 | + condition="spphs"> |
615 | + |
616 | + <div class="top-portlet"> |
617 | + <h2>Synchronised packages</h2> |
618 | + |
619 | + <tal:message replace="view/synchronised_packages_header_message"/> |
620 | + <table class="listing"> |
621 | + <thead> |
622 | + <tr> |
623 | + <th>Name</th> |
624 | + <th>Uploaded to</th> |
625 | + <th>Version</th> |
626 | + <th>When</th> |
627 | + <th>Failures</th> |
628 | + <th>Bugs</th> |
629 | + <th>Questions</th> |
630 | + </tr> |
631 | + </thead> |
632 | + |
633 | + <div metal:use-macro="context/@@+person-macros/spphs-rows" /> |
634 | + </table> |
635 | + </div> |
636 | + </tal:synchronised-packages> |
637 | + |
638 | </div><!--id packages--> |
639 | |
640 | <div id="projects" class="top-portlet"> |
641 | |
642 | === modified file 'lib/lp/registry/tests/test_person.py' |
643 | --- lib/lp/registry/tests/test_person.py 2011-09-21 01:41:08 +0000 |
644 | +++ lib/lp/registry/tests/test_person.py 2011-09-23 08:02:27 +0000 |
645 | @@ -61,6 +61,7 @@ |
646 | PersonVisibility, |
647 | ) |
648 | from lp.registry.interfaces.personnotification import IPersonNotificationSet |
649 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
650 | from lp.registry.interfaces.product import IProductSet |
651 | from lp.registry.model.karma import ( |
652 | KarmaCategory, |
653 | @@ -437,6 +438,88 @@ |
654 | list(user.getBugSubscriberPackages()) |
655 | self.assertThat(recorder, HasQueryCount(Equals(1))) |
656 | |
657 | + def createCopiedPackage(self, spph, copier, dest_distroseries=None, |
658 | + dest_archive=None): |
659 | + if dest_distroseries is None: |
660 | + dest_distroseries = self.factory.makeDistroSeries() |
661 | + if dest_archive is None: |
662 | + dest_archive = dest_distroseries.main_archive |
663 | + return spph.copyTo( |
664 | + dest_distroseries, creator=copier, |
665 | + pocket=PackagePublishingPocket.UPDATES, |
666 | + archive=dest_archive) |
667 | + |
668 | + def test_getLatestSynchronisedPublishings_most_recent_first(self): |
669 | + # getLatestSynchronisedPublishings returns the latest copies sorted |
670 | + # by most recent first. |
671 | + spph = self.factory.makeSourcePackagePublishingHistory() |
672 | + copier = self.factory.makePerson() |
673 | + copied_spph1 = self.createCopiedPackage(spph, copier) |
674 | + copied_spph2 = self.createCopiedPackage(spph, copier) |
675 | + synchronised_spphs = copier.getLatestSynchronisedPublishings() |
676 | + |
677 | + self.assertContentEqual( |
678 | + [copied_spph2, copied_spph1], |
679 | + synchronised_spphs) |
680 | + |
681 | + def test_getLatestSynchronisedPublishings_other_creator(self): |
682 | + spph = self.factory.makeSourcePackagePublishingHistory() |
683 | + copier = self.factory.makePerson() |
684 | + self.createCopiedPackage(spph, copier) |
685 | + someone_else = self.factory.makePerson() |
686 | + synchronised_spphs = someone_else.getLatestSynchronisedPublishings() |
687 | + |
688 | + self.assertEqual( |
689 | + 0, |
690 | + synchronised_spphs.count()) |
691 | + |
692 | + def test_getLatestSynchronisedPublishings_latest(self): |
693 | + # getLatestSynchronisedPublishings returns only the latest copy of |
694 | + # a package in a distroseries |
695 | + spph = self.factory.makeSourcePackagePublishingHistory() |
696 | + copier = self.factory.makePerson() |
697 | + dest_distroseries = self.factory.makeDistroSeries() |
698 | + self.createCopiedPackage( |
699 | + spph, copier, dest_distroseries) |
700 | + copied_spph2 = self.createCopiedPackage( |
701 | + spph, copier, dest_distroseries) |
702 | + synchronised_spphs = copier.getLatestSynchronisedPublishings() |
703 | + |
704 | + self.assertContentEqual( |
705 | + [copied_spph2], |
706 | + synchronised_spphs) |
707 | + |
708 | + def test_getLatestSynchronisedPublishings_cross_archive_copies(self): |
709 | + # getLatestSynchronisedPublishings returns only the copies copied |
710 | + # cross archive. |
711 | + spph = self.factory.makeSourcePackagePublishingHistory() |
712 | + copier = self.factory.makePerson() |
713 | + dest_distroseries2 = self.factory.makeDistroSeries( |
714 | + distribution=spph.distroseries.distribution) |
715 | + self.createCopiedPackage( |
716 | + spph, copier, dest_distroseries2) |
717 | + synchronised_spphs = copier.getLatestSynchronisedPublishings() |
718 | + |
719 | + self.assertEqual( |
720 | + 0, |
721 | + synchronised_spphs.count()) |
722 | + |
723 | + def test_getLatestSynchronisedPublishings_main_archive(self): |
724 | + # getLatestSynchronisedPublishings returns only the copies copied in |
725 | + # a primary archive (as opposed to a ppa). |
726 | + spph = self.factory.makeSourcePackagePublishingHistory() |
727 | + copier = self.factory.makePerson() |
728 | + dest_distroseries = self.factory.makeDistroSeries() |
729 | + ppa = self.factory.makeArchive( |
730 | + distribution=dest_distroseries.distribution) |
731 | + self.createCopiedPackage( |
732 | + spph, copier, dest_distroseries, ppa) |
733 | + synchronised_spphs = copier.getLatestSynchronisedPublishings() |
734 | + |
735 | + self.assertEqual( |
736 | + 0, |
737 | + synchronised_spphs.count()) |
738 | + |
739 | |
740 | class TestPersonStates(TestCaseWithFactory): |
741 | |
742 | |
743 | === modified file 'lib/lp/soyuz/browser/configure.zcml' |
744 | --- lib/lp/soyuz/browser/configure.zcml 2011-09-22 17:31:46 +0000 |
745 | +++ lib/lp/soyuz/browser/configure.zcml 2011-09-23 08:02:27 +0000 |
746 | @@ -686,6 +686,12 @@ |
747 | name="+ppa-packages" |
748 | template="../templates/person-ppa-packages.pt"/> |
749 | <browser:page |
750 | + for="lp.registry.interfaces.person.IPerson" |
751 | + permission="zope.Public" |
752 | + class="lp.registry.browser.person.PersonSynchronisedPackagesView" |
753 | + name="+synchronised-packages" |
754 | + template="../templates/person-synchronised-packages.pt"/> |
755 | + <browser:page |
756 | name="+archivesubscriptions" |
757 | for="lp.registry.interfaces.person.IPerson" |
758 | class="lp.soyuz.browser.archivesubscription.PersonArchiveSubscriptionsView" |
759 | |
760 | === added file 'lib/lp/soyuz/templates/person-synchronised-packages.pt' |
761 | --- lib/lp/soyuz/templates/person-synchronised-packages.pt 1970-01-01 00:00:00 +0000 |
762 | +++ lib/lp/soyuz/templates/person-synchronised-packages.pt 2011-09-23 08:02:27 +0000 |
763 | @@ -0,0 +1,59 @@ |
764 | + |
765 | +<html |
766 | + xmlns="http://www.w3.org/1999/xhtml" |
767 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
768 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
769 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
770 | + metal:use-macro="view/macro:page/main_only" |
771 | + i18n:domain="launchpad" |
772 | +> |
773 | + |
774 | +<body> |
775 | + |
776 | +<div metal:fill-slot="heading"> |
777 | + <h1 tal:content="view/page_title"/> |
778 | +</div> |
779 | + |
780 | +<div metal:fill-slot="main"> |
781 | + <div class="top-portlet"> |
782 | + <tal:navlinks replace="structure context/@@+related-software-navlinks"/> |
783 | + </div> |
784 | + |
785 | + <div id="packages" class="top-portlet"> |
786 | + |
787 | + <tal:navigation_top |
788 | + replace="structure view/batchnav/@@+navigation-links-upper" /> |
789 | + |
790 | + <tal:synchronised-packages |
791 | + define="spphs view/batch"> |
792 | + |
793 | + <table class="listing" tal:condition="spphs"> |
794 | + <thead> |
795 | + <tr> |
796 | + <th>Name</th> |
797 | + <th>Uploaded to</th> |
798 | + <th>Version</th> |
799 | + <th>When</th> |
800 | + <th>Failures</th> |
801 | + <th>Bugs</th> |
802 | + <th>Questions</th> |
803 | + </tr> |
804 | + </thead> |
805 | + <tbody> |
806 | + <div metal:use-macro="context/@@+person-macros/spphs-rows" /> |
807 | + </tbody> |
808 | + </table> |
809 | + |
810 | + <tal:navigation_bottom |
811 | + replace="structure view/batchnav/@@+navigation-links-lower" /> |
812 | + |
813 | + <tal:no_packages condition="not: spphs"> |
814 | + <tal:name replace="context/fmt:displayname"/> has not synchronised any packages. |
815 | + </tal:no_packages> |
816 | + |
817 | + </tal:synchronised-packages> |
818 | + </div> |
819 | +</div> |
820 | + |
821 | +</body> |
822 | +</html> |
Rubber-stamped after conversation in #launchpad- redsquad.