Merge lp:~rvb/launchpad/sync-bug-827608-add-synchronized-packages-redone into lp:launchpad

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
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-packages.

Description of the change

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Rubber-stamped after conversation in #launchpad-redsquad.

review: Approve

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>