Merge ~pappacena/launchpad:package-publishing-copied-from-archive into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: f4f37066e823256ee2d046f3ff33c0d9ca1c2c1a
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:package-publishing-copied-from-archive
Merge into: launchpad:master
Diff against target: 569 lines (+276/-51)
10 files modified
lib/lp/_schema_circular_imports.py (+4/-0)
lib/lp/soyuz/browser/publishing.py (+31/-1)
lib/lp/soyuz/interfaces/publishing.py (+22/-2)
lib/lp/soyuz/model/publishing.py (+20/-4)
lib/lp/soyuz/scripts/tests/test_copypackage.py (+32/-1)
lib/lp/soyuz/stories/ppa/xx-copy-packages.txt (+2/-2)
lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt (+79/-2)
lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt (+1/-0)
lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt (+1/-0)
lib/lp/soyuz/templates/packagepublishing-details.pt (+84/-39)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+380382@code.launchpad.net

Commit message

Keeping track of the original archives of source and binary package copies on [Source|Binary]PackagePublishingHistory.

Description of the change

The tracking of "copied_from_archive" seems to be working fine, but it's missing showing this information on S/BPackagePublishingHistory page.

On the page, it's shown as "Previous archive was <archive>" text, right below where we show that the package was copied today. I'm not sure this is the right text to show to the user, but I couldn't think of anything different.

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

cjwatson, while checking the template to show the "copied_from_archive", I've noticed that we actually have this info displayed at some situations: basically, we show it if the archive of the PackagePublishingHistory is not the same as the sourcepackagename's (or build's) archive, or if the distroseries are different.

Is there any situation not covered by this condition? Why exactly do we need to have this "copied_from_package"?

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

It should be showing both the "root" original package's archive and it's first "parent", from where it was copied.

Of course, if it's the first copy of the package, it doesn't show the message twice.

Revision history for this message
Colin Watson (cjwatson) wrote :

Thanks! Good start, I think, but let's clean some things up.

review: Needs Fixing
ca7c4ac... by Thiago F. Pappacena

Renaming view property to avoid naming confusion

abc38a0... by Thiago F. Pappacena

Always setting copied_from_archive on package publishing history

29b3ab5... by Thiago F. Pappacena

Renaming variable to match other parts of the system

c061ee0... by Thiago F. Pappacena

Merge branch 'master' into package-publishing-copied-from-archive

bb0ba8f... by Thiago F. Pappacena

Better formatting the "copied from"/"originally uploaded to" message on pkg publishing history

84337d9... by Thiago F. Pappacena

Compatibility with packages publishing history without copied_from_archive attribute set"

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

cjwatson, I've just pushed the requested changes.

18a4491... by Thiago F. Pappacena

Code style

4397657... by Thiago F. Pappacena

Removing redundant condition

a9c5c06... by Thiago F. Pappacena

Showing creator at the current position if we didn't show at the new on, at the top

7915de6... by Thiago F. Pappacena

Merge branch 'master' into package-publishing-copied-from-archive

38d0dc2... by Thiago F. Pappacena

New test to check what is shown publishing history for change-override of copied packages

e01cf5c... by Thiago F. Pappacena

Fixing broken test and missing condition

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
da6875b... by Thiago F. Pappacena

Coding style adjustments

65a082f... by Thiago F. Pappacena

Adjusting when to show sponsor on package publishing history

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes.

Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
f4f3706... by Thiago F. Pappacena

Typo and coding style change

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
2index 46247fc..a50ddd2 100644
3--- a/lib/lp/_schema_circular_imports.py
4+++ b/lib/lp/_schema_circular_imports.py
5@@ -363,6 +363,10 @@ patch_reference_property(
6 patch_reference_property(
7 ISourcePackagePublishingHistory, 'archive', IArchive)
8 patch_reference_property(
9+ IBinaryPackagePublishingHistory, 'copied_from_archive', IArchive)
10+patch_reference_property(
11+ ISourcePackagePublishingHistory, 'copied_from_archive', IArchive)
12+patch_reference_property(
13 ISourcePackagePublishingHistory, 'ancestor',
14 ISourcePackagePublishingHistory)
15 patch_reference_property(
16diff --git a/lib/lp/soyuz/browser/publishing.py b/lib/lp/soyuz/browser/publishing.py
17index 1a23bc3..7733190 100644
18--- a/lib/lp/soyuz/browser/publishing.py
19+++ b/lib/lp/soyuz/browser/publishing.py
20@@ -1,4 +1,4 @@
21-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
22+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
23 # GNU Affero General Public License version 3 (see the file LICENSE).
24
25 """Browser views for Soyuz publishing records."""
26@@ -202,6 +202,18 @@ class BasePublishingRecordView(LaunchpadView):
27 return u"%d%% of users" % self.context.phased_update_percentage
28 return u""
29
30+ @property
31+ def linkify_copied_from_archive(self):
32+ """Return True if the copied_from_archive should be linkified.
33+
34+ The copied_from_archive should be linkified if it's a PPA and the
35+ user has permission to see it.
36+ """
37+ archive = self.context.copied_from_archive
38+ if archive is None:
39+ return False
40+ return archive.is_ppa and check_permission('launchpad.View', archive)
41+
42
43 class SourcePublishingRecordView(BasePublishingRecordView):
44 """View class for `ISourcePackagePublishingHistory`."""
45@@ -266,6 +278,15 @@ class SourcePublishingRecordView(BasePublishingRecordView):
46 return False
47
48 @property
49+ def upload_archive(self):
50+ """Get the original archive from this binary build if this was a
51+ copied publication.
52+ """
53+ if not self.wasCopied():
54+ return None
55+ return self.context.sourcepackagerelease.upload_archive
56+
57+ @property
58 def allow_selection(self):
59 """Do not render the checkbox corresponding to this record."""
60 return False
61@@ -406,3 +427,12 @@ class BinaryPublishingRecordView(BasePublishingRecordView):
62 return True
63
64 return False
65+
66+ @property
67+ def upload_archive(self):
68+ """Get the original archive from this binary build if this was a
69+ copied publication.
70+ """
71+ if not self.wasCopied():
72+ return None
73+ return self.context.binarypackagerelease.build.archive
74diff --git a/lib/lp/soyuz/interfaces/publishing.py b/lib/lp/soyuz/interfaces/publishing.py
75index 4697209..6945260 100644
76--- a/lib/lp/soyuz/interfaces/publishing.py
77+++ b/lib/lp/soyuz/interfaces/publishing.py
78@@ -269,6 +269,13 @@ class ISourcePackagePublishingHistoryPublic(IPublishingView):
79 Interface,
80 title=_('Archive ID'), required=True, readonly=True,
81 ))
82+ copied_from_archive = exported(
83+ Reference(
84+ # Really IArchive (fixed in _schema_circular_imports.py).
85+ Interface,
86+ title=_('Original archive ID where this package was copied from.'),
87+ required=False, readonly=True,
88+ ))
89 supersededby = Int(
90 title=_('The sourcepackagerelease which superseded this one'),
91 required=False, readonly=False,
92@@ -711,6 +718,13 @@ class IBinaryPackagePublishingHistoryPublic(IPublishingView):
93 description=_("The context archive for this publication."),
94 required=True, readonly=True,
95 ))
96+ copied_from_archive = exported(
97+ Reference(
98+ # Really IArchive (fixed in _schema_circular_imports.py).
99+ Interface,
100+ title=_('Original archive ID where this package was copied from.'),
101+ required=False, readonly=True,
102+ ))
103 removed_by = exported(
104 Reference(
105 IPerson,
106@@ -875,7 +889,8 @@ class IBinaryPackagePublishingHistory(IBinaryPackagePublishingHistoryPublic,
107 class IPublishingSet(Interface):
108 """Auxiliary methods for dealing with sets of publications."""
109
110- def publishBinaries(archive, distroseries, pocket, binaries):
111+ def publishBinaries(archive, distroseries, pocket, binaries,
112+ copied_from_archives=None):
113 """Efficiently publish multiple BinaryPackageReleases in an Archive.
114
115 Creates `IBinaryPackagePublishingHistory` records for each
116@@ -889,6 +904,8 @@ class IPublishingSet(Interface):
117 :param binaries: A dict mapping `BinaryPackageReleases` to their
118 desired overrides as (`Component`, `Section`,
119 `PackagePublishingPriority`, `phased_update_percentage`) tuples.
120+ :param copied_from_archives: A dict mapping `BinaryPackageReleases`
121+ to their original archives (for copy operations).
122
123 :return: A list of new `IBinaryPackagePublishingHistory` records.
124 """
125@@ -913,7 +930,8 @@ class IPublishingSet(Interface):
126
127 def newSourcePublication(archive, sourcepackagerelease, distroseries,
128 component, section, pocket, ancestor,
129- create_dsd_job=True):
130+ create_dsd_job=True, copied_from_archive=None,
131+ creator=None, sponsor=None, packageupload=None):
132 """Create a new `SourcePackagePublishingHistory`.
133
134 :param archive: An `IArchive`
135@@ -926,6 +944,8 @@ class IPublishingSet(Interface):
136 version of this publishing record
137 :param create_dsd_job: A boolean indicating whether or not a dsd job
138 should be created for the new source publication.
139+ :param copied_from_archive: For copy operations, this should be the
140+ source archive (from where this new publication is coming from).
141 :param creator: An optional `IPerson`. If this is None, the
142 sourcepackagerelease's creator will be used.
143 :param sponsor: An optional `IPerson` indicating the sponsor of this
144diff --git a/lib/lp/soyuz/model/publishing.py b/lib/lp/soyuz/model/publishing.py
145index ae7b414..fafd7f9 100644
146--- a/lib/lp/soyuz/model/publishing.py
147+++ b/lib/lp/soyuz/model/publishing.py
148@@ -258,6 +258,8 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
149 default=PackagePublishingPocket.RELEASE,
150 notNull=True)
151 archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)
152+ copied_from_archive = ForeignKey(
153+ dbName="copied_from_archive", foreignKey="Archive", notNull=False)
154 removed_by = ForeignKey(
155 dbName="removed_by", foreignKey="Person",
156 storm_validator=validate_public_person, default=None)
157@@ -531,6 +533,7 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
158 component = override.component
159 if override.section is not None:
160 section = override.section
161+
162 return getUtility(IPublishingSet).newSourcePublication(
163 archive,
164 self.sourcepackagerelease,
165@@ -542,6 +545,7 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
166 create_dsd_job=create_dsd_job,
167 creator=creator,
168 sponsor=sponsor,
169+ copied_from_archive=self.archive,
170 packageupload=packageupload)
171
172 def getStatusSummaryForBuilds(self):
173@@ -643,6 +647,8 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
174 dateremoved = UtcDateTimeCol(default=None)
175 pocket = EnumCol(dbName='pocket', schema=PackagePublishingPocket)
176 archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)
177+ copied_from_archive = ForeignKey(
178+ dbName="copied_from_archive", foreignKey="Archive", notNull=False)
179 removed_by = ForeignKey(
180 dbName="removed_by", foreignKey="Person",
181 storm_validator=validate_public_person, default=None)
182@@ -1024,8 +1030,11 @@ def expand_binary_requests(distroseries, binaries):
183 class PublishingSet:
184 """Utilities for manipulating publications in batches."""
185
186- def publishBinaries(self, archive, distroseries, pocket, binaries):
187+ def publishBinaries(self, archive, distroseries, pocket, binaries,
188+ copied_from_archives=None):
189 """See `IPublishingSet`."""
190+ if copied_from_archives is None:
191+ copied_from_archives = {}
192 # Expand the dict of binaries into a list of tuples including the
193 # architecture.
194 if distroseries.distribution != archive.distribution:
195@@ -1076,11 +1085,13 @@ class PublishingSet:
196
197 BPPH = BinaryPackagePublishingHistory
198 return bulk.create(
199- (BPPH.archive, BPPH.distroarchseries, BPPH.pocket,
200+ (BPPH.archive, BPPH.copied_from_archive,
201+ BPPH.distroarchseries, BPPH.pocket,
202 BPPH.binarypackagerelease, BPPH.binarypackagename,
203 BPPH.component, BPPH.section, BPPH.priority,
204 BPPH.phased_update_percentage, BPPH.status, BPPH.datecreated),
205- [(archive, das, pocket, bpr, bpr.binarypackagename,
206+ [(archive, copied_from_archives.get(bpr), das, pocket, bpr,
207+ bpr.binarypackagename,
208 get_component(archive, das.distroseries, component),
209 section, priority, phased_update_percentage,
210 PackagePublishingStatus.PENDING, UTC_NOW)
211@@ -1158,12 +1169,16 @@ class PublishingSet:
212 bpph.priority, None)) for bpph in bpphs)
213 if not with_overrides:
214 return list()
215+ copied_from_archives = {
216+ bpph.binarypackagerelease: bpph.archive for bpph in bpphs}
217 return self.publishBinaries(
218- archive, distroseries, pocket, with_overrides)
219+ archive, distroseries, pocket, with_overrides,
220+ copied_from_archives)
221
222 def newSourcePublication(self, archive, sourcepackagerelease,
223 distroseries, component, section, pocket,
224 ancestor=None, create_dsd_job=True,
225+ copied_from_archive=None,
226 creator=None, sponsor=None, packageupload=None):
227 """See `IPublishingSet`."""
228 # Circular import.
229@@ -1179,6 +1194,7 @@ class PublishingSet:
230 pub = SourcePackagePublishingHistory(
231 distroseries=distroseries,
232 pocket=pocket,
233+ copied_from_archive=copied_from_archive,
234 archive=archive,
235 sourcepackagename=sourcepackagerelease.sourcepackagename,
236 sourcepackagerelease=sourcepackagerelease,
237diff --git a/lib/lp/soyuz/scripts/tests/test_copypackage.py b/lib/lp/soyuz/scripts/tests/test_copypackage.py
238index b5bd4c6..388fcf7 100644
239--- a/lib/lp/soyuz/scripts/tests/test_copypackage.py
240+++ b/lib/lp/soyuz/scripts/tests/test_copypackage.py
241@@ -1,4 +1,4 @@
242-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
243+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
244 # GNU Affero General Public License version 3 (see the file LICENSE).
245
246 __metaclass__ = type
247@@ -11,6 +11,7 @@ from testtools.content import text_content
248 from testtools.matchers import (
249 Equals,
250 LessThan,
251+ MatchesListwise,
252 MatchesStructure,
253 )
254 import transaction
255@@ -1015,6 +1016,36 @@ class CopyCheckerTestCase(TestCaseWithFactory):
256 copied_source, copied_source.distroseries, copied_source.pocket,
257 None, False)
258
259+ def test_copy_to_another_archive_tracks_source_archive(self):
260+ """Checks that creating a copy of a package publishing keeps track
261+ of origin archive.
262+ """
263+ # Create a source with binaries in ubuntutest/breezy-autotest.
264+ source = self.test_publisher.getPubSource(architecturehintlist='i386')
265+ binary = self.test_publisher.getPubBinaries(pub_source=source)[0]
266+
267+ hoary = self.test_publisher.ubuntutest.getSeries('hoary-test')
268+ self.test_publisher.addFakeChroots(hoary)
269+
270+ target_archive = self.factory.makeArchive(
271+ distribution=self.test_publisher.ubuntutest,
272+ purpose=ArchivePurpose.PPA)
273+
274+ copied_source = source.copyTo(hoary, source.pocket, target_archive)
275+ self.assertThat(
276+ copied_source, MatchesStructure.byEquality(
277+ archive=target_archive,
278+ copied_from_archive=source.archive))
279+
280+ copied_binaries = binary.copyTo(hoary, source.pocket, target_archive)
281+
282+ self.assertThat(
283+ copied_binaries, MatchesListwise([
284+ MatchesStructure.byEquality(
285+ archive=target_archive,
286+ copied_from_archive=binary.archive),
287+ ]))
288+
289
290 class BaseDoCopyTests:
291
292diff --git a/lib/lp/soyuz/stories/ppa/xx-copy-packages.txt b/lib/lp/soyuz/stories/ppa/xx-copy-packages.txt
293index 8159d2d..31d8915 100644
294--- a/lib/lp/soyuz/stories/ppa/xx-copy-packages.txt
295+++ b/lib/lp/soyuz/stories/ppa/xx-copy-packages.txt
296@@ -287,8 +287,8 @@ context.
297 >>> jblack_extra_browser.open(expander_url)
298 >>> print(extract_text(jblack_extra_browser.contents))
299 Publishing details
300- Copied from ubuntu hoary in Primary Archive for Ubuntu Linux by James
301- Blackwell
302+ Copied from PPA for Celso Providelo by James Blackwell
303+ Originally uploaded to ubuntu hoary in Primary Archive for Ubuntu Linux
304 Changelog
305 pmount (0.1-1) hoary; urgency=low
306 * Fix description (Malone #1)
307diff --git a/lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt b/lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt
308index 51a629b..0866dff 100644
309--- a/lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt
310+++ b/lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt
311@@ -28,13 +28,91 @@ shows the complete history of a package in all series.
312 >>> print(table.findAll("tr")[2].td["colspan"])
313 8
314
315-A change-override request should show who made the request
316+Copy the package to a new distribution named "foo-distro". The publishing
317+history page of Foo-distro should show that the package was copied, but no
318+intermediate archive information.
319+
320+ >>> login('foo.bar@canonical.com')
321+ >>> from lp.soyuz.model.archive import ArchivePurpose
322+ >>> copy_creator = stp.factory.makePerson(name='person123')
323+ >>> new_distro = stp.factory.makeDistribution(name='foo-distro')
324+ >>> new_distroseries = stp.factory.makeDistroSeries(
325+ ... name='foo-series', distribution=new_distro)
326+ >>> new_archive = stp.factory.makeArchive(
327+ ... distribution=new_distro,
328+ ... purpose=ArchivePurpose.PRIMARY)
329+ >>> new_pub1 = source_pub.copyTo(
330+ ... new_distroseries, source_pub.pocket, new_archive,
331+ ... creator=copy_creator)
332+ >>> logout()
333+
334+ >>> anon_browser.open(
335+ ... 'http://launchpad.test/%s/+source/test-history/'
336+ ... '+publishinghistory' % new_distro.name)
337+
338+ >>> table = find_tag_by_id(anon_browser.contents, 'publishing-summary')
339+ >>> print(extract_text(table))
340+ Date Status Target Pocket Component Section Version
341+ ... UTC Pending Foo-series release main base 666
342+ Copied from ubuntutest breezy-autotest in Primary Archive for Ubuntu Test by Person123
343+
344+
345+Copying from "Foo-distro" to a new distribution "Another-distro". It should
346+show the intermediate archive Foo-distro on Another-distro's history page,
347+since we copied the package from there. It should also show that the
348+original distribution was Ubuntutest breezy-autotest.
349+
350+ >>> login('foo.bar@canonical.com')
351+ >>> from lp.soyuz.model.archive import ArchivePurpose
352+ >>> new_distro = stp.factory.makeDistribution(name='another-distro')
353+ >>> new_distroseries = stp.factory.makeDistroSeries(
354+ ... name='another-series', distribution=new_distro)
355+ >>> new_archive = stp.factory.makeArchive(
356+ ... distribution=new_distro,
357+ ... purpose=ArchivePurpose.PRIMARY)
358+ >>> new_pub2 = new_pub1.copyTo(
359+ ... new_distroseries, source_pub.pocket, new_archive,
360+ ... creator=copy_creator)
361+ >>> logout()
362+
363+ >>> anon_browser.open(
364+ ... 'http://launchpad.test/%s/+source/test-history/'
365+ ... '+publishinghistory' % new_distro.name)
366+ >>> table = find_tag_by_id(anon_browser.contents, 'publishing-summary')
367+ >>> print(extract_text(table))
368+ Date Status Target Pocket Component Section Version
369+ ... UTC Pending Another-series release main base 666
370+ Copied from Primary Archive for Foo-distro by Person123
371+ Originally uploaded to ubuntutest breezy-autotest in Primary Archive for Ubuntu Test
372+
373+And in this new distro, a change-override on a copied archive should show both
374+messages.
375
376 >>> from lp.registry.interfaces.person import IPersonSet
377 >>> from zope.component import getUtility
378+ >>> from zope.security.proxy import removeSecurityProxy
379
380 >>> login('foo.bar@canonical.com')
381 >>> person = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com')
382+ >>> new_pub2_changed = removeSecurityProxy(new_pub2).changeOverride(
383+ ... new_component='universe', creator=person)
384+ >>> logout()
385+ >>> anon_browser.open(
386+ ... 'http://launchpad.test/%s/+source/test-history/'
387+ ... '+publishinghistory' % new_distro.name)
388+ >>> table = find_tag_by_id(anon_browser.contents, 'publishing-summary')
389+ >>> print(extract_text(table))
390+ Date Status Target Pocket Component Section Version
391+ ... UTC Pending Another-series release universe base 666
392+ Copied from ubuntutest breezy-autotest in Primary Archive for Ubuntu Test by Foo Bar
393+ ... UTC Pending Another-series release main base 666
394+ Copied from Primary Archive for Foo-distro by Person123
395+ Originally uploaded to ubuntutest breezy-autotest in Primary Archive for Ubuntu Test
396+
397+Going back to the original distribution, a change-override request should
398+show who made the request.
399+
400+ >>> login('foo.bar@canonical.com')
401 >>> new_pub = source_pub.changeOverride(
402 ... new_component='universe', creator=person)
403 >>> logout()
404@@ -51,7 +129,6 @@ A change-override request should show who made the request
405 Created ... ago by Foo Bar
406 Published ... ago
407
408-
409 A publishing record will be shown as deleted in the publishing history after a
410 request for deletion by a user.
411
412diff --git a/lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt b/lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt
413index 722c4c0..0d04c43 100644
414--- a/lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt
415+++ b/lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt
416@@ -71,6 +71,7 @@ Each binary publication exposes a number of properties:
417 binary_package_version: u'1.0'
418 build_link: u'http://.../~cprov/+archive/ubuntu/ppa/+build/28'
419 component_name: u'main'
420+ copied_from_archive_link: None
421 creator_link: None
422 date_created: u'2007-08-10T13:00:00+00:00'
423 date_made_pending: None
424diff --git a/lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt b/lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt
425index 0d47a00..b26e605 100644
426--- a/lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt
427+++ b/lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt
428@@ -121,6 +121,7 @@ publication to play with first.
429 >>> pprint_entry(pubs['entries'][0])
430 archive_link: u'http://.../~cprov/+archive/ubuntu/ppa'
431 component_name: u'main'
432+ copied_from_archive_link: None
433 creator_link: u'http://api.launchpad.test/beta/~name16'
434 date_created: ...
435 date_made_pending: None
436diff --git a/lib/lp/soyuz/templates/packagepublishing-details.pt b/lib/lp/soyuz/templates/packagepublishing-details.pt
437index 4c65323..b6a9a42 100644
438--- a/lib/lp/soyuz/templates/packagepublishing-details.pt
439+++ b/lib/lp/soyuz/templates/packagepublishing-details.pt
440@@ -42,45 +42,90 @@
441 <span tal:attributes="title context/datepublished/fmt:datetime"
442 tal:content="context/datepublished/fmt:displaydate" />
443 </li>
444- <li tal:condition="view/wasCopied">
445- Copied from
446- <tal:source_original_location condition="view/is_source">
447- <tal:define
448- define="linkify_archive view/linkify_source_archive;
449- source context/sourcepackagerelease">
450- <tal:message
451- define="series source/upload_distroseries;
452- distro series/distribution;
453- message string:${distro/name} ${series/name} in "
454- replace="message" />
455- <a tal:condition="linkify_archive"
456- tal:attributes="href source/upload_archive/fmt:url"
457- tal:content="source/upload_archive/displayname" />
458- <tal:message
459- condition="not:linkify_archive"
460- define="archive source/upload_archive;
461- message string:${archive/displayname}"
462- replace="message" />
463- </tal:define>
464- <tal:source_creator condition="context/creator">
465- by <a tal:replace="structure context/creator/fmt:link"/>
466- </tal:source_creator>
467- <tal:source_sponsor condition="context/sponsor">
468- (sponsored by <a tal:replace="structure context/sponsor/fmt:link"/>)
469- </tal:source_sponsor>
470- </tal:source_original_location>
471- <tal:binary_build_location condition="view/is_binary">
472- <tal:message
473- define="build context/binarypackagerelease/build;
474- archive build/archive;
475- pocket build/pocket;
476- arch build/distro_arch_series;
477- series arch/distroseries;
478- distro series/distribution;
479- message string:${distro/name} ${series/name}-${pocket/name/fmt:lower} ${arch/architecturetag} in ${archive/displayname}"
480- replace="message" />
481- </tal:binary_build_location>
482- </li>
483
484+ <tal:comment condition="nothing">
485+ For package copies, we have a distinction between what is the
486+ "copied_from_archive" (the archive from where we directly copied a
487+ publishing) and the "upload_archive" (the archive where the package
488+ was originally uploaded to).
489+ </tal:comment>
490+ <tal:copied tal:define="
491+ copied_from_archive context/copied_from_archive;
492+ upload_archive view/upload_archive;
493+ chained_copies python: copied_from_archive and copied_from_archive != upload_archive"
494+ tal:condition="view/wasCopied">
495+ <li tal:define="linkify_archive view/linkify_copied_from_archive"
496+ tal:condition="chained_copies">
497+ Copied from
498+ <a tal:condition="linkify_archive"
499+ tal:attributes="href copied_from_archive/fmt:url"
500+ tal:content="copied_from_archive/displayname" />
501+ <tal:message
502+ condition="not:linkify_archive"
503+ define="message string:${copied_from_archive/displayname}"
504+ replace="message" />
505+ <tal:creator condition="context/creator">
506+ by <a tal:replace="structure context/creator/fmt:link"/>
507+ </tal:creator>
508+ <tal:source_sponsor condition="python: view.is_source and context.sponsor">
509+ (sponsored by <a tal:replace="structure context/sponsor/fmt:link"/>)
510+ </tal:source_sponsor>
511+ </li>
512+
513+ <li>
514+ <span tal:condition="chained_copies">
515+ Originally
516+ <p tal:replace="python: 'uploaded to' if view.is_source
517+ else 'built as'" />
518+ </span>
519+ <span tal:condition="not: chained_copies">
520+ Copied from
521+ </span>
522+
523+ <tal:source_original_location condition="view/is_source">
524+ <tal:define
525+ define="linkify_archive view/linkify_source_archive;
526+ source context/sourcepackagerelease">
527+ <tal:message
528+ define="series source/upload_distroseries;
529+ distro series/distribution;
530+ message string:${distro/name} ${series/name} in "
531+ replace="message" />
532+ <a tal:condition="linkify_archive"
533+ tal:attributes="href upload_archive/fmt:url"
534+ tal:content="upload_archive/displayname" />
535+ <tal:message
536+ condition="not:linkify_archive"
537+ define="message string:${upload_archive/displayname}"
538+ replace="message" />
539+ </tal:define>
540+ </tal:source_original_location>
541+
542+ <tal:comment condition="nothing">
543+ Only show "creator" if we didn't show above, at the
544+ previous copied_from_archive "li" tag.
545+ </tal:comment>
546+
547+ <tal:source_creator_and_sponsor condition="not: chained_copies">
548+ <tal:source_creator condition="context/creator">
549+ by <a tal:replace="structure context/creator/fmt:link"/>
550+ </tal:source_creator>
551+ <tal:source_sponsor condition="python: view.is_source and context.sponsor">
552+ (sponsored by <a tal:replace="structure context/sponsor/fmt:link"/>)
553+ </tal:source_sponsor>
554+ </tal:source_creator_and_sponsor>
555+
556+ <tal:binary_build_location condition="view/is_binary">
557+ <tal:message
558+ define="build context/binarypackagerelease/build;
559+ pocket build/pocket;
560+ arch build/distro_arch_series;
561+ series arch/distroseries;
562+ distro series/distribution;
563+ message string:${distro/name} ${series/name}-${pocket/name/fmt:lower} ${arch/architecturetag} in ${upload_archive/displayname}"
564+ replace="message" />
565+ </tal:binary_build_location>
566+ </li>
567+ </tal:copied>
568 </ul>
569 </tal:root>