Merge ~cjwatson/launchpad:move-package into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 0ed27c6fcfba459ec61213fb6fd6a7a6b33c2c1a
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:move-package
Merge into: launchpad:master
Diff against target: 808 lines (+312/-50)
11 files modified
lib/lp/code/vocabularies/sourcepackagerecipe.py (+2/-2)
lib/lp/soyuz/browser/archive.py (+2/-11)
lib/lp/soyuz/interfaces/archive.py (+3/-1)
lib/lp/soyuz/interfaces/packagecopyjob.py (+7/-2)
lib/lp/soyuz/model/archive.py (+9/-6)
lib/lp/soyuz/model/packagecopyjob.py (+15/-9)
lib/lp/soyuz/scripts/packagecopier.py (+63/-9)
lib/lp/soyuz/scripts/tests/test_copypackage.py (+44/-0)
lib/lp/soyuz/tests/test_archive.py (+124/-5)
lib/lp/soyuz/tests/test_packagecopyjob.py (+28/-3)
lib/lp/soyuz/vocabularies.py (+15/-2)
Reviewer Review Type Date Requested Status
Kristian Glass (community) Approve
Review via email: mp+373942@code.launchpad.net

Commit message

Add an atomic "move package" operation

Archive.copyPackage and Archive.copyPackages now take a move=True
argument, which causes the source publication to be deleted if the copy
succeeds.

This allows us to fix a long-standing problem with Ubuntu's
proposed-migration process: it needs to do a copy and delete when
migrating packages from devel-proposed to devel, but since the copy is
asynchronous it can fail without proposed-migration being aware of this,
leading to the package in question simply being removed. Moving the
deletion into the copier avoids this problem.

LP: #1329052

To post a comment you must log in.
Revision history for this message
Kristian Glass (doismellburning) wrote :

> "[this] is ~800 lines of code motion [and test] and passing parameters down through multiple layers and 6 lines of actually doing the removal after the copy if instructed to do so

Yes, yes it is!

And it looks good to me

review: Approve
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/code/vocabularies/sourcepackagerecipe.py b/lib/lp/code/vocabularies/sourcepackagerecipe.py
index 0c56101..31cf4bc 100644
--- a/lib/lp/code/vocabularies/sourcepackagerecipe.py
+++ b/lib/lp/code/vocabularies/sourcepackagerecipe.py
@@ -1,4 +1,4 @@
1# Copyright 2010-2018 Canonical Ltd. This software is licensed under the1# Copyright 2010-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Source Package Recipe vocabularies used in the lp/code modules."""4"""Source Package Recipe vocabularies used in the lp/code modules."""
@@ -22,8 +22,8 @@ from lp.services.webapp.vocabulary import (
22 IHugeVocabulary,22 IHugeVocabulary,
23 SQLObjectVocabularyBase,23 SQLObjectVocabularyBase,
24 )24 )
25from lp.soyuz.browser.archive import make_archive_vocabulary
26from lp.soyuz.interfaces.archive import IArchiveSet25from lp.soyuz.interfaces.archive import IArchiveSet
26from lp.soyuz.vocabularies import make_archive_vocabulary
2727
2828
29@implementer(IHugeVocabulary)29@implementer(IHugeVocabulary)
diff --git a/lib/lp/soyuz/browser/archive.py b/lib/lp/soyuz/browser/archive.py
index 6dd6cfe..3170ea0 100644
--- a/lib/lp/soyuz/browser/archive.py
+++ b/lib/lp/soyuz/browser/archive.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Browser views for archive."""4"""Browser views for archive."""
@@ -23,7 +23,6 @@ __all__ = [
23 'ArchiveView',23 'ArchiveView',
24 'ArchiveViewBase',24 'ArchiveViewBase',
25 'EnableProcessorsMixin',25 'EnableProcessorsMixin',
26 'make_archive_vocabulary',
27 'PackageCopyingMixin',26 'PackageCopyingMixin',
28 'traverse_named_ppa',27 'traverse_named_ppa',
29 ]28 ]
@@ -175,6 +174,7 @@ from lp.soyuz.model.archive import (
175 )174 )
176from lp.soyuz.model.publishing import SourcePackagePublishingHistory175from lp.soyuz.model.publishing import SourcePackagePublishingHistory
177from lp.soyuz.scripts.packagecopier import check_copy_permissions176from lp.soyuz.scripts.packagecopier import check_copy_permissions
177from lp.soyuz.vocabularies import make_archive_vocabulary
178178
179179
180class ArchiveBadges(HasBadgeBase):180class ArchiveBadges(HasBadgeBase):
@@ -1433,15 +1433,6 @@ class PackageCopyingMixin:
1433 return True1433 return True
14341434
14351435
1436def make_archive_vocabulary(archives):
1437 terms = []
1438 for archive in archives:
1439 label = '%s [%s]' % (archive.displayname, archive.reference)
1440 terms.append(SimpleTerm(archive, archive.reference, label))
1441 terms.sort(key=lambda x: x.value.reference)
1442 return SimpleVocabulary(terms)
1443
1444
1445class ArchivePackageCopyingView(ArchiveSourceSelectionFormView,1436class ArchivePackageCopyingView(ArchiveSourceSelectionFormView,
1446 PackageCopyingMixin):1437 PackageCopyingMixin):
1447 """Archive package copying view class.1438 """Archive package copying view class.
diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py
index e28f08b..2aae255 100644
--- a/lib/lp/soyuz/interfaces/archive.py
+++ b/lib/lp/soyuz/interfaces/archive.py
@@ -1522,7 +1522,7 @@ class IArchiveView(IHasBuildRecords):
1522 person, to_series=None, include_binaries=False,1522 person, to_series=None, include_binaries=False,
1523 sponsored=None, unembargo=False, auto_approve=False,1523 sponsored=None, unembargo=False, auto_approve=False,
1524 silent=False, from_pocket=None, from_series=None,1524 silent=False, from_pocket=None, from_series=None,
1525 phased_update_percentage=None):1525 phased_update_percentage=None, move=False):
1526 """Copy a single named source into this archive.1526 """Copy a single named source into this archive.
15271527
1528 Asynchronously copy a specific version of a named source to the1528 Asynchronously copy a specific version of a named source to the
@@ -1563,6 +1563,8 @@ class IArchiveView(IHasBuildRecords):
1563 omitted, copy from any series with a matching version.1563 omitted, copy from any series with a matching version.
1564 :param phased_update_percentage: the phased update percentage to1564 :param phased_update_percentage: the phased update percentage to
1565 apply to the copied publication.1565 apply to the copied publication.
1566 :param move: if True, delete the source publication after copying it
1567 to the destination.
15661568
1567 :raises NoSuchSourcePackageName: if the source name is invalid1569 :raises NoSuchSourcePackageName: if the source name is invalid
1568 :raises PocketNotFound: if the pocket name is invalid1570 :raises PocketNotFound: if the pocket name is invalid
diff --git a/lib/lp/soyuz/interfaces/packagecopyjob.py b/lib/lp/soyuz/interfaces/packagecopyjob.py
index 32ef2dc..043fff5 100644
--- a/lib/lp/soyuz/interfaces/packagecopyjob.py
+++ b/lib/lp/soyuz/interfaces/packagecopyjob.py
@@ -1,4 +1,4 @@
1# Copyright 2010-2013 Canonical Ltd. This software is licensed under the1# Copyright 2010-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -130,7 +130,7 @@ class IPlainPackageCopyJobSource(IJobSource):
130 copy_policy=PackageCopyPolicy.INSECURE, requester=None,130 copy_policy=PackageCopyPolicy.INSECURE, requester=None,
131 sponsored=None, unembargo=False, auto_approve=False,131 sponsored=None, unembargo=False, auto_approve=False,
132 silent=False, source_distroseries=None, source_pocket=None,132 silent=False, source_distroseries=None, source_pocket=None,
133 phased_update_percentage=None):133 phased_update_percentage=None, move=False):
134 """Create a new `IPlainPackageCopyJob`.134 """Create a new `IPlainPackageCopyJob`.
135135
136 :param package_name: The name of the source package to copy.136 :param package_name: The name of the source package to copy.
@@ -162,6 +162,8 @@ class IPlainPackageCopyJobSource(IJobSource):
162 from any pocket with a matching version.162 from any pocket with a matching version.
163 :param phased_update_percentage: The phased update percentage to163 :param phased_update_percentage: The phased update percentage to
164 apply to the copied publication.164 apply to the copied publication.
165 :param move: If True, delete the source publication after copying it
166 to the destination.
165 """167 """
166168
167 def createMultiple(target_distroseries, copy_tasks, requester,169 def createMultiple(target_distroseries, copy_tasks, requester,
@@ -254,6 +256,9 @@ class IPlainPackageCopyJob(IRunnableJob):
254 phased_update_percentage = Int(256 phased_update_percentage = Int(
255 title=_("Phased update percentage"), required=False, readonly=True)257 title=_("Phased update percentage"), required=False, readonly=True)
256258
259 move = Bool(
260 title=_("Delete source after copy"), required=False, readonly=True)
261
257 def addSourceOverride(override):262 def addSourceOverride(override):
258 """Add an `ISourceOverride` to the metadata."""263 """Add an `ISourceOverride` to the metadata."""
259264
diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py
index acf8541..cb3f089 100644
--- a/lib/lp/soyuz/model/archive.py
+++ b/lib/lp/soyuz/model/archive.py
@@ -1829,7 +1829,7 @@ class Archive(SQLBase):
1829 person, to_series=None, include_binaries=False,1829 person, to_series=None, include_binaries=False,
1830 sponsored=None, unembargo=False, auto_approve=False,1830 sponsored=None, unembargo=False, auto_approve=False,
1831 silent=False, from_pocket=None, from_series=None,1831 silent=False, from_pocket=None, from_series=None,
1832 phased_update_percentage=None):1832 phased_update_percentage=None, move=False):
1833 """See `IArchive`."""1833 """See `IArchive`."""
1834 # Asynchronously copy a package using the job system.1834 # Asynchronously copy a package using the job system.
1835 from lp.soyuz.scripts.packagecopier import check_copy_permissions1835 from lp.soyuz.scripts.packagecopier import check_copy_permissions
@@ -1854,7 +1854,8 @@ class Archive(SQLBase):
1854 from_pocket=from_pocket)1854 from_pocket=from_pocket)
1855 if series is None:1855 if series is None:
1856 series = source.distroseries1856 series = source.distroseries
1857 check_copy_permissions(person, self, series, pocket, [source])1857 check_copy_permissions(
1858 person, self, series, pocket, [source], move=move)
18581859
1859 job_source = getUtility(IPlainPackageCopyJobSource)1860 job_source = getUtility(IPlainPackageCopyJobSource)
1860 job_source.create(1861 job_source.create(
@@ -1866,12 +1867,12 @@ class Archive(SQLBase):
1866 sponsored=sponsored, unembargo=unembargo,1867 sponsored=sponsored, unembargo=unembargo,
1867 auto_approve=auto_approve, silent=silent,1868 auto_approve=auto_approve, silent=silent,
1868 source_distroseries=from_series, source_pocket=from_pocket,1869 source_distroseries=from_series, source_pocket=from_pocket,
1869 phased_update_percentage=phased_update_percentage)1870 phased_update_percentage=phased_update_percentage, move=move)
18701871
1871 def copyPackages(self, source_names, from_archive, to_pocket,1872 def copyPackages(self, source_names, from_archive, to_pocket,
1872 person, to_series=None, from_series=None,1873 person, to_series=None, from_series=None,
1873 include_binaries=None, sponsored=None, unembargo=False,1874 include_binaries=None, sponsored=None, unembargo=False,
1874 auto_approve=False, silent=False):1875 auto_approve=False, silent=False, move=False):
1875 """See `IArchive`."""1876 """See `IArchive`."""
1876 from lp.soyuz.scripts.packagecopier import check_copy_permissions1877 from lp.soyuz.scripts.packagecopier import check_copy_permissions
1877 sources = self._collectLatestPublishedSources(1878 sources = self._collectLatestPublishedSources(
@@ -1880,7 +1881,8 @@ class Archive(SQLBase):
1880 # Now do a mass check of permissions.1881 # Now do a mass check of permissions.
1881 pocket = self._text_to_pocket(to_pocket)1882 pocket = self._text_to_pocket(to_pocket)
1882 series = self._text_to_series(to_series)1883 series = self._text_to_series(to_series)
1883 check_copy_permissions(person, self, series, pocket, sources)1884 check_copy_permissions(
1885 person, self, series, pocket, sources, move=move)
18841886
1885 # If we get this far then we can create the PackageCopyJob.1887 # If we get this far then we can create the PackageCopyJob.
1886 copy_tasks = []1888 copy_tasks = []
@@ -1899,7 +1901,8 @@ class Archive(SQLBase):
1899 job_source.createMultiple(1901 job_source.createMultiple(
1900 copy_tasks, person, copy_policy=PackageCopyPolicy.MASS_SYNC,1902 copy_tasks, person, copy_policy=PackageCopyPolicy.MASS_SYNC,
1901 include_binaries=include_binaries, sponsored=sponsored,1903 include_binaries=include_binaries, sponsored=sponsored,
1902 unembargo=unembargo, auto_approve=auto_approve, silent=silent)1904 unembargo=unembargo, auto_approve=auto_approve, silent=silent,
1905 move=move)
19031906
1904 def _collectLatestPublishedSources(self, from_archive, from_series,1907 def _collectLatestPublishedSources(self, from_archive, from_series,
1905 source_names):1908 source_names):
diff --git a/lib/lp/soyuz/model/packagecopyjob.py b/lib/lp/soyuz/model/packagecopyjob.py
index b4d92b2..90389e9 100644
--- a/lib/lp/soyuz/model/packagecopyjob.py
+++ b/lib/lp/soyuz/model/packagecopyjob.py
@@ -1,4 +1,4 @@
1# Copyright 2010-2016 Canonical Ltd. This software is licensed under the1# Copyright 2010-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -275,7 +275,7 @@ class PlainPackageCopyJob(PackageCopyJobDerived):
275 include_binaries, sponsored=None, unembargo=False,275 include_binaries, sponsored=None, unembargo=False,
276 auto_approve=False, silent=False,276 auto_approve=False, silent=False,
277 source_distroseries=None, source_pocket=None,277 source_distroseries=None, source_pocket=None,
278 phased_update_percentage=None):278 phased_update_percentage=None, move=False):
279 """Produce a metadata dict for this job."""279 """Produce a metadata dict for this job."""
280 return {280 return {
281 'target_pocket': target_pocket.value,281 'target_pocket': target_pocket.value,
@@ -289,6 +289,7 @@ class PlainPackageCopyJob(PackageCopyJobDerived):
289 source_distroseries.name if source_distroseries else None,289 source_distroseries.name if source_distroseries else None,
290 'source_pocket': source_pocket.value if source_pocket else None,290 'source_pocket': source_pocket.value if source_pocket else None,
291 'phased_update_percentage': phased_update_percentage,291 'phased_update_percentage': phased_update_percentage,
292 'move': move,
292 }293 }
293294
294 @classmethod295 @classmethod
@@ -298,14 +299,14 @@ class PlainPackageCopyJob(PackageCopyJobDerived):
298 copy_policy=PackageCopyPolicy.INSECURE, requester=None,299 copy_policy=PackageCopyPolicy.INSECURE, requester=None,
299 sponsored=None, unembargo=False, auto_approve=False,300 sponsored=None, unembargo=False, auto_approve=False,
300 silent=False, source_distroseries=None, source_pocket=None,301 silent=False, source_distroseries=None, source_pocket=None,
301 phased_update_percentage=None):302 phased_update_percentage=None, move=False):
302 """See `IPlainPackageCopyJobSource`."""303 """See `IPlainPackageCopyJobSource`."""
303 assert package_version is not None, "No package version specified."304 assert package_version is not None, "No package version specified."
304 assert requester is not None, "No requester specified."305 assert requester is not None, "No requester specified."
305 metadata = cls._makeMetadata(306 metadata = cls._makeMetadata(
306 target_pocket, package_version, include_binaries, sponsored,307 target_pocket, package_version, include_binaries, sponsored,
307 unembargo, auto_approve, silent, source_distroseries,308 unembargo, auto_approve, silent, source_distroseries,
308 source_pocket, phased_update_percentage)309 source_pocket, phased_update_percentage, move)
309 job = PackageCopyJob(310 job = PackageCopyJob(
310 job_type=cls.class_job_type,311 job_type=cls.class_job_type,
311 source_archive=source_archive,312 source_archive=source_archive,
@@ -323,7 +324,7 @@ class PlainPackageCopyJob(PackageCopyJobDerived):
323 @classmethod324 @classmethod
324 def _composeJobInsertionTuple(cls, copy_policy, include_binaries, job_id,325 def _composeJobInsertionTuple(cls, copy_policy, include_binaries, job_id,
325 copy_task, sponsored, unembargo,326 copy_task, sponsored, unembargo,
326 auto_approve, silent):327 auto_approve, silent, move):
327 """Create an SQL fragment for inserting a job into the database.328 """Create an SQL fragment for inserting a job into the database.
328329
329 :return: A string representing an SQL tuple containing initializers330 :return: A string representing an SQL tuple containing initializers
@@ -340,7 +341,7 @@ class PlainPackageCopyJob(PackageCopyJobDerived):
340 ) = copy_task341 ) = copy_task
341 metadata = cls._makeMetadata(342 metadata = cls._makeMetadata(
342 target_pocket, package_version, include_binaries, sponsored,343 target_pocket, package_version, include_binaries, sponsored,
343 unembargo, auto_approve, silent)344 unembargo, auto_approve, silent, move=move)
344 data = (345 data = (
345 cls.class_job_type, target_distroseries, copy_policy,346 cls.class_job_type, target_distroseries, copy_policy,
346 source_archive, target_archive, package_name, job_id,347 source_archive, target_archive, package_name, job_id,
@@ -351,14 +352,15 @@ class PlainPackageCopyJob(PackageCopyJobDerived):
351 def createMultiple(cls, copy_tasks, requester,352 def createMultiple(cls, copy_tasks, requester,
352 copy_policy=PackageCopyPolicy.INSECURE,353 copy_policy=PackageCopyPolicy.INSECURE,
353 include_binaries=False, sponsored=None,354 include_binaries=False, sponsored=None,
354 unembargo=False, auto_approve=False, silent=False):355 unembargo=False, auto_approve=False, silent=False,
356 move=False):
355 """See `IPlainPackageCopyJobSource`."""357 """See `IPlainPackageCopyJobSource`."""
356 store = IMasterStore(Job)358 store = IMasterStore(Job)
357 job_ids = Job.createMultiple(store, len(copy_tasks), requester)359 job_ids = Job.createMultiple(store, len(copy_tasks), requester)
358 job_contents = [360 job_contents = [
359 cls._composeJobInsertionTuple(361 cls._composeJobInsertionTuple(
360 copy_policy, include_binaries, job_id, task, sponsored,362 copy_policy, include_binaries, job_id, task, sponsored,
361 unembargo, auto_approve, silent)363 unembargo, auto_approve, silent, move)
362 for job_id, task in zip(job_ids, copy_tasks)]364 for job_id, task in zip(job_ids, copy_tasks)]
363 return bulk.create(365 return bulk.create(
364 (PackageCopyJob.job_type, PackageCopyJob.target_distroseries,366 (PackageCopyJob.job_type, PackageCopyJob.target_distroseries,
@@ -467,6 +469,10 @@ class PlainPackageCopyJob(PackageCopyJobDerived):
467 return self.metadata.get('phased_update_percentage')469 return self.metadata.get('phased_update_percentage')
468470
469 @property471 @property
472 def move(self):
473 return self.metadata.get('move', False)
474
475 @property
470 def requester_can_admin_target(self):476 def requester_can_admin_target(self):
471 return self.target_archive.canAdministerQueue(477 return self.target_archive.canAdministerQueue(
472 self.requester, self.getSourceOverride().component,478 self.requester, self.getSourceOverride().component,
@@ -659,7 +665,7 @@ class PlainPackageCopyJob(PackageCopyJobDerived):
659 sponsored=self.sponsored, packageupload=pu,665 sponsored=self.sponsored, packageupload=pu,
660 unembargo=self.unembargo,666 unembargo=self.unembargo,
661 phased_update_percentage=self.phased_update_percentage,667 phased_update_percentage=self.phased_update_percentage,
662 logger=self.logger)668 move=self.move, logger=self.logger)
663669
664 # Add a PackageDiff for this new upload if it has ancestry.670 # Add a PackageDiff for this new upload if it has ancestry.
665 if copied_publications and not ancestry.is_empty():671 if copied_publications and not ancestry.is_empty():
diff --git a/lib/lp/soyuz/scripts/packagecopier.py b/lib/lp/soyuz/scripts/packagecopier.py
index 3891af5..2e4a767 100644
--- a/lib/lp/soyuz/scripts/packagecopier.py
+++ b/lib/lp/soyuz/scripts/packagecopier.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Package copying utilities."""4"""Package copying utilities."""
@@ -15,9 +15,15 @@ __all__ = [
1515
16import apt_pkg16import apt_pkg
17from lazr.delegates import delegate_to17from lazr.delegates import delegate_to
18from zope.component import getUtility18from zope.component import (
19 getAdapter,
20 getUtility,
21 )
19from zope.security.proxy import removeSecurityProxy22from zope.security.proxy import removeSecurityProxy
2023
24from lp.app.interfaces.security import IAuthorization
25from lp.registry.interfaces.role import IPersonRoles
26from lp.registry.model.person import Person
21from lp.services.database.bulk import load_related27from lp.services.database.bulk import load_related
22from lp.soyuz.adapters.overrides import SourceOverride28from lp.soyuz.adapters.overrides import SourceOverride
23from lp.soyuz.enums import SourcePackageFormat29from lp.soyuz.enums import SourcePackageFormat
@@ -141,7 +147,8 @@ class CheckedCopy:
141 return {'status': BuildSetStatus.NEEDSBUILD}147 return {'status': BuildSetStatus.NEEDSBUILD}
142148
143149
144def check_copy_permissions(person, archive, series, pocket, sources):150def check_copy_permissions(person, archive, series, pocket, sources,
151 move=False):
145 """Check that `person` has permission to copy a package.152 """Check that `person` has permission to copy a package.
146153
147 :param person: User attempting the upload.154 :param person: User attempting the upload.
@@ -150,9 +157,12 @@ def check_copy_permissions(person, archive, series, pocket, sources):
150 :param pocket: Destination `Pocket`.157 :param pocket: Destination `Pocket`.
151 :param sources: Sequence of `SourcePackagePublishingHistory`s for the158 :param sources: Sequence of `SourcePackagePublishingHistory`s for the
152 packages to be copied.159 packages to be copied.
160 :param move: If True, also check whether `person` has permission to
161 delete the sources.
153 :raises CannotCopy: If the copy is not allowed.162 :raises CannotCopy: If the copy is not allowed.
154 """163 """
155 # Circular import.164 # Circular import.
165 from lp.soyuz.model.archive import Archive
156 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease166 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
157167
158 if person is None:168 if person is None:
@@ -161,6 +171,12 @@ def check_copy_permissions(person, archive, series, pocket, sources):
161 if len(sources) > 1:171 if len(sources) > 1:
162 # Bulk-load the data we'll need from each source publication.172 # Bulk-load the data we'll need from each source publication.
163 load_related(SourcePackageRelease, sources, ["sourcepackagereleaseID"])173 load_related(SourcePackageRelease, sources, ["sourcepackagereleaseID"])
174 if move:
175 # Bulk-load at least some of the data we'll need for permission
176 # checks on each source archive. Not all of this is currently
177 # preloadable.
178 archives = load_related(Archive, sources, ["archiveID"])
179 load_related(Person, archives, ["ownerID"])
164180
165 # If there is a requester, check that they have upload permission into181 # If there is a requester, check that they have upload permission into
166 # the destination (archive, component, pocket). This check is done182 # the destination (archive, component, pocket). This check is done
@@ -191,6 +207,29 @@ def check_copy_permissions(person, archive, series, pocket, sources):
191 person, override.component, pocket, dest_series):207 person, override.component, pocket, dest_series):
192 raise CannotCopy(reason)208 raise CannotCopy(reason)
193209
210 if move:
211 roles = IPersonRoles(person)
212 for source_archive in set(source.archive for source in sources):
213 # XXX cjwatson 2019-10-09: Checking the archive rather than the
214 # SPPH duplicates security adapter logic, which is unfortunate;
215 # but too much of the logic required to use
216 # DelegatedAuthorization-based adapters such as the one used for
217 # launchpad.Edit on SPPH lives in
218 # lp.services.webapp.authorization and is hard to use without a
219 # full Zope interaction.
220 source_archive_authz = getAdapter(
221 source_archive, IAuthorization, "launchpad.Append")
222 if not source_archive_authz.checkAuthenticated(roles):
223 raise CannotCopy(
224 "%s is not permitted to delete publications from %s." %
225 (person.display_name, source_archive.displayname))
226 for source in sources:
227 if not source.archive.canModifySuite(
228 source.distroseries, source.pocket):
229 raise CannotCopy(
230 "Cannot delete publications from suite '%s'" %
231 source.distroseries.getSuite(source.pocket))
232
194233
195class CopyChecker:234class CopyChecker:
196 """Check copy candiates.235 """Check copy candiates.
@@ -388,7 +427,7 @@ class CopyChecker:
388 "different contents." % lf.libraryfile.filename)427 "different contents." % lf.libraryfile.filename)
389428
390 def checkCopy(self, source, series, pocket, person=None,429 def checkCopy(self, source, series, pocket, person=None,
391 check_permissions=True):430 check_permissions=True, move=False):
392 """Check if the source can be copied to the given location.431 """Check if the source can be copied to the given location.
393432
394 Check possible conflicting publications in the destination archive.433 Check possible conflicting publications in the destination archive.
@@ -407,13 +446,15 @@ class CopyChecker:
407 :param person: requester `IPerson`.446 :param person: requester `IPerson`.
408 :param check_permissions: boolean indicating whether or not the447 :param check_permissions: boolean indicating whether or not the
409 requester's permissions to copy should be checked.448 requester's permissions to copy should be checked.
449 :param move: if True, also check whether `person` has permission to
450 delete the source.
410451
411 :raise CannotCopy when a copy is not allowed to be performed452 :raise CannotCopy when a copy is not allowed to be performed
412 containing the reason of the error.453 containing the reason of the error.
413 """454 """
414 if check_permissions:455 if check_permissions:
415 check_copy_permissions(456 check_copy_permissions(
416 person, self.archive, series, pocket, [source])457 person, self.archive, series, pocket, [source], move=move)
417458
418 if series.distribution != self.archive.distribution:459 if series.distribution != self.archive.distribution:
419 raise CannotCopy(460 raise CannotCopy(
@@ -483,7 +524,7 @@ def do_copy(sources, archive, series, pocket, include_binaries=False,
483 send_email=False, strict_binaries=True, close_bugs=True,524 send_email=False, strict_binaries=True, close_bugs=True,
484 create_dsd_job=True, announce_from_person=None, sponsored=None,525 create_dsd_job=True, announce_from_person=None, sponsored=None,
485 packageupload=None, unembargo=False, phased_update_percentage=None,526 packageupload=None, unembargo=False, phased_update_percentage=None,
486 logger=None):527 move=False, logger=None):
487 """Perform the complete copy of the given sources incrementally.528 """Perform the complete copy of the given sources incrementally.
488529
489 Verifies if each copy can be performed using `CopyChecker` and530 Verifies if each copy can be performed using `CopyChecker` and
@@ -532,6 +573,8 @@ def do_copy(sources, archive, series, pocket, include_binaries=False,
532 doing so.573 doing so.
533 :param phased_update_percentage: The phased update percentage to apply574 :param phased_update_percentage: The phased update percentage to apply
534 to the copied publication.575 to the copied publication.
576 :param move: If True, delete the source publication after copying to the
577 destination.
535 :param logger: An optional logger.578 :param logger: An optional logger.
536579
537 :raise CannotCopy when one or more copies were not allowed. The error580 :raise CannotCopy when one or more copies were not allowed. The error
@@ -554,7 +597,8 @@ def do_copy(sources, archive, series, pocket, include_binaries=False,
554 destination_series = series597 destination_series = series
555 try:598 try:
556 copy_checker.checkCopy(599 copy_checker.checkCopy(
557 source, destination_series, pocket, person, check_permissions)600 source, destination_series, pocket, person, check_permissions,
601 move=move)
558 except CannotCopy as reason:602 except CannotCopy as reason:
559 errors.append("%s (%s)" % (source.displayname, reason))603 errors.append("%s (%s)" % (source.displayname, reason))
560 continue604 continue
@@ -610,7 +654,8 @@ def do_copy(sources, archive, series, pocket, include_binaries=False,
610 override, close_bugs=close_bugs, create_dsd_job=create_dsd_job,654 override, close_bugs=close_bugs, create_dsd_job=create_dsd_job,
611 close_bugs_since_version=old_version, creator=creator,655 close_bugs_since_version=old_version, creator=creator,
612 sponsor=sponsor, packageupload=packageupload,656 sponsor=sponsor, packageupload=packageupload,
613 phased_update_percentage=phased_update_percentage, logger=logger)657 phased_update_percentage=phased_update_percentage, move=move,
658 logger=logger)
614 if send_email and sub_copies:659 if send_email and sub_copies:
615 mailer = PackageUploadMailer.forAction(660 mailer = PackageUploadMailer.forAction(
616 'accepted', person, source.sourcepackagerelease, [], [],661 'accepted', person, source.sourcepackagerelease, [], [],
@@ -639,7 +684,7 @@ def _do_direct_copy(source, archive, series, pocket, include_binaries,
639 override=None, close_bugs=True, create_dsd_job=True,684 override=None, close_bugs=True, create_dsd_job=True,
640 close_bugs_since_version=None, creator=None,685 close_bugs_since_version=None, creator=None,
641 sponsor=None, packageupload=None,686 sponsor=None, packageupload=None,
642 phased_update_percentage=None, logger=None):687 phased_update_percentage=None, move=False, logger=None):
643 """Copy publishing records to another location.688 """Copy publishing records to another location.
644689
645 Copy each item of the given list of `SourcePackagePublishingHistory`690 Copy each item of the given list of `SourcePackagePublishingHistory`
@@ -671,6 +716,8 @@ def _do_direct_copy(source, archive, series, pocket, include_binaries,
671 to be created.716 to be created.
672 :param phased_update_percentage: The phased update percentage to apply717 :param phased_update_percentage: The phased update percentage to apply
673 to the copied publication.718 to the copied publication.
719 :param move: If True, delete the source publication after copying to the
720 destination.
674 :param logger: An optional logger.721 :param logger: An optional logger.
675722
676 :return: a list of `ISourcePackagePublishingHistory` and723 :return: a list of `ISourcePackagePublishingHistory` and
@@ -742,4 +789,11 @@ def _do_direct_copy(source, archive, series, pocket, include_binaries,
742 # XXX cjwatson 2012-06-22 bug=869308: Fails to honour P-a-s.789 # XXX cjwatson 2012-06-22 bug=869308: Fails to honour P-a-s.
743 source_copy.createMissingBuilds(logger=logger)790 source_copy.createMissingBuilds(logger=logger)
744791
792 if move:
793 removal_comment = "Moved to %s" % series.getSuite(pocket)
794 if archive != source.archive:
795 removal_comment += " in %s" % archive.reference
796 getUtility(IPublishingSet).requestDeletion(
797 [source], creator, removal_comment=removal_comment)
798
745 return copies799 return copies
diff --git a/lib/lp/soyuz/scripts/tests/test_copypackage.py b/lib/lp/soyuz/scripts/tests/test_copypackage.py
index 388fcf7..eee530a 100644
--- a/lib/lp/soyuz/scripts/tests/test_copypackage.py
+++ b/lib/lp/soyuz/scripts/tests/test_copypackage.py
@@ -1732,6 +1732,50 @@ class TestDoDirectCopy(BaseDoCopyTests, TestCaseWithFactory):
1732 self.assertIsInstance(copies[1], BinaryPackagePublishingHistory)1732 self.assertIsInstance(copies[1], BinaryPackagePublishingHistory)
1733 self.assertEqual("i386", copies[1].distroarchseries.architecturetag)1733 self.assertEqual("i386", copies[1].distroarchseries.architecturetag)
17341734
1735 def test_copy_without_move(self):
1736 # A copy with move=False (the default) leaves the source publication
1737 # intact.
1738 nobby, archive, source = self._setup_archive()
1739 target_archive = self.factory.makeArchive(
1740 distribution=self.test_publisher.ubuntutest)
1741 [copied_source] = do_copy(
1742 [source], target_archive, nobby, source.pocket,
1743 include_binaries=False, person=target_archive.owner,
1744 check_permissions=False, send_email=False)
1745 self.assertEqual(PackagePublishingStatus.PENDING, copied_source.status)
1746 self.assertEqual(PackagePublishingStatus.PENDING, source.status)
1747
1748 def test_copy_with_move(self):
1749 # A copy with move=True deletes the source publication.
1750 nobby, archive, source = self._setup_archive()
1751 target_archive = self.factory.makeArchive(
1752 distribution=self.test_publisher.ubuntutest)
1753 [copied_source] = do_copy(
1754 [source], target_archive, nobby, source.pocket,
1755 include_binaries=False, person=target_archive.owner,
1756 check_permissions=False, send_email=False, move=True)
1757 self.assertEqual(PackagePublishingStatus.PENDING, copied_source.status)
1758 self.assertEqual(PackagePublishingStatus.DELETED, source.status)
1759 self.assertEqual(
1760 "Moved to %s in %s" % (
1761 nobby.getSuite(source.pocket), target_archive.reference),
1762 source.removal_comment)
1763
1764 def test_copy_with_move_failure(self):
1765 # If a copy with move=True fails, then the source publication is
1766 # left intact.
1767 nobby, archive, source = self._setup_archive()
1768 self.test_publisher.getPubSource(
1769 sourcename=source.source_package_name,
1770 archive=nobby.main_archive, version="1.0-2",
1771 architecturehintlist="any")
1772 self.assertRaises(
1773 CannotCopy, do_copy,
1774 [source], archive, nobby, source.pocket,
1775 include_binaries=False, person=source.sourcepackagerelease.creator,
1776 check_permissions=False, send_email=False, move=True)
1777 self.assertEqual(PackagePublishingStatus.PENDING, source.status)
1778
17351779
1736class TestCopyBuildRecords(TestCaseWithFactory):1780class TestCopyBuildRecords(TestCaseWithFactory):
1737 """Test handling of binaries and their build records when copying."""1781 """Test handling of binaries and their build records when copying."""
diff --git a/lib/lp/soyuz/tests/test_archive.py b/lib/lp/soyuz/tests/test_archive.py
index 09f0141..b015953 100644
--- a/lib/lp/soyuz/tests/test_archive.py
+++ b/lib/lp/soyuz/tests/test_archive.py
@@ -2798,20 +2798,23 @@ class TestCopyPackage(TestCaseWithFactory):
27982798
2799 layer = DatabaseFunctionalLayer2799 layer = DatabaseFunctionalLayer
28002800
2801 def _setup_copy_data(self, source_distribution=None, source_private=False,2801 def _setup_copy_data(self, source_distribution=None, source_purpose=None,
2802 source_private=False, source_pocket=None,
2802 target_purpose=None,2803 target_purpose=None,
2803 target_status=SeriesStatus.DEVELOPMENT,2804 target_status=SeriesStatus.DEVELOPMENT,
2804 same_distribution=False):2805 same_distribution=False):
2805 if target_purpose is None:2806 if target_purpose is None:
2806 target_purpose = ArchivePurpose.PPA2807 target_purpose = ArchivePurpose.PPA
2807 source_archive = self.factory.makeArchive(2808 source_archive = self.factory.makeArchive(
2808 distribution=source_distribution, private=source_private)2809 distribution=source_distribution, purpose=source_purpose,
2810 private=source_private)
2809 target_distribution = (2811 target_distribution = (
2810 source_archive.distribution if same_distribution else None)2812 source_archive.distribution if same_distribution else None)
2811 target_archive = self.factory.makeArchive(2813 target_archive = self.factory.makeArchive(
2812 distribution=target_distribution, purpose=target_purpose)2814 distribution=target_distribution, purpose=target_purpose)
2813 source = self.factory.makeSourcePackagePublishingHistory(2815 source = self.factory.makeSourcePackagePublishingHistory(
2814 archive=source_archive, status=PackagePublishingStatus.PUBLISHED)2816 archive=source_archive, pocket=source_pocket,
2817 status=PackagePublishingStatus.PUBLISHED)
2815 with person_logged_in(source_archive.owner):2818 with person_logged_in(source_archive.owner):
2816 source_name = source.source_package_name2819 source_name = source.source_package_name
2817 version = source.source_package_version2820 version = source.source_package_version
@@ -2854,7 +2857,8 @@ class TestCopyPackage(TestCaseWithFactory):
2854 include_binaries=False,2857 include_binaries=False,
2855 sponsored=sponsored,2858 sponsored=sponsored,
2856 copy_policy=PackageCopyPolicy.INSECURE,2859 copy_policy=PackageCopyPolicy.INSECURE,
2857 phased_update_percentage=30))2860 phased_update_percentage=30,
2861 move=False))
28582862
2859 def test_copyPackage_disallows_non_primary_archive_uploaders(self):2863 def test_copyPackage_disallows_non_primary_archive_uploaders(self):
2860 # If copying to a primary archive and you're not an uploader for2864 # If copying to a primary archive and you're not an uploader for
@@ -3052,6 +3056,63 @@ class TestCopyPackage(TestCaseWithFactory):
3052 self.assertEqual(source.distroseries, copy_job.source_distroseries)3056 self.assertEqual(source.distroseries, copy_job.source_distroseries)
3053 self.assertEqual(source.pocket, copy_job.source_pocket)3057 self.assertEqual(source.pocket, copy_job.source_pocket)
30543058
3059 def test_copyPackage_move(self):
3060 # Passing move=True causes copyPackage to create a copy job that
3061 # will delete the source publication after copying.
3062 (source, source_archive, source_name, target_archive, to_pocket,
3063 to_series, version) = self._setup_copy_data(
3064 source_distribution=self.factory.makeDistribution())
3065 with person_logged_in(target_archive.owner):
3066 target_archive.newComponentUploader(source_archive.owner, "main")
3067 with person_logged_in(source_archive.owner):
3068 target_archive.copyPackage(
3069 source_name, version, source_archive, to_pocket.name,
3070 to_series=to_series.name, include_binaries=True,
3071 person=source_archive.owner, move=True)
3072
3073 # There should be one copy job, with move=True set.
3074 job_source = getUtility(IPlainPackageCopyJobSource)
3075 copy_job = job_source.getActiveJobs(target_archive).one()
3076 self.assertTrue(copy_job.move)
3077
3078 def test_copyPackage_move_without_permission(self):
3079 # Passing move=True checks that the user is permitted to delete the
3080 # source publication.
3081 (source, source_archive, source_name, target_archive, to_pocket,
3082 to_series, version) = self._setup_copy_data(
3083 source_distribution=self.factory.makeDistribution())
3084 with person_logged_in(target_archive.owner):
3085 expected_error = (
3086 "%s is not permitted to delete publications from %s." % (
3087 target_archive.owner.display_name,
3088 source_archive.displayname))
3089 self.assertRaisesWithContent(
3090 CannotCopy, expected_error, target_archive.copyPackage,
3091 source_name, version, source_archive, to_pocket.name,
3092 to_series=to_series.name, include_binaries=True,
3093 person=target_archive.owner, move=True)
3094
3095 def test_copyPackage_move_from_immutable_suite(self):
3096 # Passing move=True checks that the source suite can be modified.
3097 (source, source_archive, source_name, target_archive, to_pocket,
3098 to_series, version) = self._setup_copy_data(
3099 source_distribution=self.factory.makeDistribution(),
3100 source_purpose=ArchivePurpose.PRIMARY,
3101 source_pocket=PackagePublishingPocket.RELEASE)
3102 with person_logged_in(target_archive.owner):
3103 target_archive.newComponentUploader(source_archive.owner, "main")
3104 removeSecurityProxy(source.distroseries).status = (
3105 SeriesStatus.SUPPORTED)
3106 with person_logged_in(source_archive.owner):
3107 expected_error = (
3108 "Cannot delete publications from suite '%s'" % (
3109 source.distroseries.getSuite(source.pocket)))
3110 self.assertRaisesWithContent(
3111 CannotCopy, expected_error, target_archive.copyPackage,
3112 source_name, version, source_archive, to_pocket.name,
3113 to_series=to_series.name, include_binaries=True,
3114 person=source_archive.owner, move=True)
3115
3055 def test_copyPackages_with_single_package(self):3116 def test_copyPackages_with_single_package(self):
3056 (source, source_archive, source_name, target_archive, to_pocket,3117 (source, source_archive, source_name, target_archive, to_pocket,
3057 to_series, version) = self._setup_copy_data()3118 to_series, version) = self._setup_copy_data()
@@ -3080,7 +3141,8 @@ class TestCopyPackage(TestCaseWithFactory):
3080 target_pocket=to_pocket,3141 target_pocket=to_pocket,
3081 include_binaries=False,3142 include_binaries=False,
3082 sponsored=sponsored,3143 sponsored=sponsored,
3083 copy_policy=PackageCopyPolicy.MASS_SYNC))3144 copy_policy=PackageCopyPolicy.MASS_SYNC,
3145 move=False))
30843146
3085 def test_copyPackages_with_multiple_packages(self):3147 def test_copyPackages_with_multiple_packages(self):
3086 # PENDING and PUBLISHED packages should both be copied.3148 # PENDING and PUBLISHED packages should both be copied.
@@ -3297,6 +3359,63 @@ class TestCopyPackage(TestCaseWithFactory):
3297 copy_job = job_source.getActiveJobs(target_archive).one()3359 copy_job = job_source.getActiveJobs(target_archive).one()
3298 self.assertEqual(to_pocket, copy_job.target_pocket)3360 self.assertEqual(to_pocket, copy_job.target_pocket)
32993361
3362 def test_copyPackages_move(self):
3363 # Passing move=True causes copyPackages to create copy jobs that
3364 # will delete the source publication after copying.
3365 (source, source_archive, source_name, target_archive, to_pocket,
3366 to_series, version) = self._setup_copy_data(
3367 source_distribution=self.factory.makeDistribution())
3368 with person_logged_in(target_archive.owner):
3369 target_archive.newComponentUploader(source_archive.owner, "main")
3370 with person_logged_in(source_archive.owner):
3371 target_archive.copyPackages(
3372 [source_name], source_archive, to_pocket.name,
3373 to_series=to_series.name, include_binaries=True,
3374 person=source_archive.owner, move=True)
3375
3376 # There should be one copy job, with move=True set.
3377 job_source = getUtility(IPlainPackageCopyJobSource)
3378 copy_job = job_source.getActiveJobs(target_archive).one()
3379 self.assertTrue(copy_job.move)
3380
3381 def test_copyPackages_move_without_permission(self):
3382 # Passing move=True checks that the user is permitted to delete the
3383 # source publication.
3384 (source, source_archive, source_name, target_archive, to_pocket,
3385 to_series, version) = self._setup_copy_data(
3386 source_distribution=self.factory.makeDistribution())
3387 with person_logged_in(target_archive.owner):
3388 expected_error = (
3389 "%s is not permitted to delete publications from %s." % (
3390 target_archive.owner.display_name,
3391 source_archive.displayname))
3392 self.assertRaisesWithContent(
3393 CannotCopy, expected_error, target_archive.copyPackages,
3394 [source_name], source_archive, to_pocket.name,
3395 to_series=to_series.name, include_binaries=True,
3396 person=target_archive.owner, move=True)
3397
3398 def test_copyPackages_move_from_immutable_suite(self):
3399 # Passing move=True checks that the source suite can be modified.
3400 (source, source_archive, source_name, target_archive, to_pocket,
3401 to_series, version) = self._setup_copy_data(
3402 source_distribution=self.factory.makeDistribution(),
3403 source_purpose=ArchivePurpose.PRIMARY,
3404 source_pocket=PackagePublishingPocket.RELEASE)
3405 with person_logged_in(target_archive.owner):
3406 target_archive.newComponentUploader(source_archive.owner, "main")
3407 removeSecurityProxy(source.distroseries).status = (
3408 SeriesStatus.SUPPORTED)
3409 with person_logged_in(source_archive.owner):
3410 expected_error = (
3411 "Cannot delete publications from suite '%s'" % (
3412 source.distroseries.getSuite(source.pocket)))
3413 self.assertRaisesWithContent(
3414 CannotCopy, expected_error, target_archive.copyPackages,
3415 [source_name], source_archive, to_pocket.name,
3416 to_series=to_series.name, include_binaries=True,
3417 person=source_archive.owner, move=True)
3418
33003419
3301class TestgetAllPublishedBinaries(TestCaseWithFactory):3420class TestgetAllPublishedBinaries(TestCaseWithFactory):
33023421
diff --git a/lib/lp/soyuz/tests/test_packagecopyjob.py b/lib/lp/soyuz/tests/test_packagecopyjob.py
index 68c1f95..6817c10 100644
--- a/lib/lp/soyuz/tests/test_packagecopyjob.py
+++ b/lib/lp/soyuz/tests/test_packagecopyjob.py
@@ -1,4 +1,4 @@
1# Copyright 2010-2018 Canonical Ltd. This software is licensed under the1# Copyright 2010-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for sync package jobs."""4"""Tests for sync package jobs."""
@@ -220,7 +220,7 @@ class PlainPackageCopyJobTests(TestCaseWithFactory, LocalTestHelper):
220 package_version="1.0-1", include_binaries=False,220 package_version="1.0-1", include_binaries=False,
221 copy_policy=PackageCopyPolicy.MASS_SYNC,221 copy_policy=PackageCopyPolicy.MASS_SYNC,
222 requester=requester, sponsored=sponsored,222 requester=requester, sponsored=sponsored,
223 phased_update_percentage=20)223 phased_update_percentage=20, move=True)
224 self.assertProvides(job, IPackageCopyJob)224 self.assertProvides(job, IPackageCopyJob)
225 self.assertEqual(archive1.id, job.source_archive_id)225 self.assertEqual(archive1.id, job.source_archive_id)
226 self.assertEqual(archive1, job.source_archive)226 self.assertEqual(archive1, job.source_archive)
@@ -230,11 +230,12 @@ class PlainPackageCopyJobTests(TestCaseWithFactory, LocalTestHelper):
230 self.assertEqual(PackagePublishingPocket.RELEASE, job.target_pocket)230 self.assertEqual(PackagePublishingPocket.RELEASE, job.target_pocket)
231 self.assertEqual("foo", job.package_name)231 self.assertEqual("foo", job.package_name)
232 self.assertEqual("1.0-1", job.package_version)232 self.assertEqual("1.0-1", job.package_version)
233 self.assertEqual(False, job.include_binaries)233 self.assertFalse(job.include_binaries)
234 self.assertEqual(PackageCopyPolicy.MASS_SYNC, job.copy_policy)234 self.assertEqual(PackageCopyPolicy.MASS_SYNC, job.copy_policy)
235 self.assertEqual(requester, job.requester)235 self.assertEqual(requester, job.requester)
236 self.assertEqual(sponsored, job.sponsored)236 self.assertEqual(sponsored, job.sponsored)
237 self.assertEqual(20, job.phased_update_percentage)237 self.assertEqual(20, job.phased_update_percentage)
238 self.assertTrue(job.move)
238239
239 def test_createMultiple_creates_one_job_per_copy(self):240 def test_createMultiple_creates_one_job_per_copy(self):
240 mother = self.factory.makeDistroSeriesParent()241 mother = self.factory.makeDistroSeriesParent()
@@ -1726,6 +1727,30 @@ class PlainPackageCopyJobTests(TestCaseWithFactory, LocalTestHelper):
1726 1, archive.getPublishedOnDiskBinaries(1727 1, archive.getPublishedOnDiskBinaries(
1727 status=PackagePublishingStatus.PENDING).count())1728 status=PackagePublishingStatus.PENDING).count())
17281729
1730 def test_move(self):
1731 # A job with move=True deletes the old publication after copying it.
1732 source_archive = self.factory.makeArchive(
1733 self.distroseries.distribution)
1734 target_archive = self.factory.makeArchive(
1735 self.distroseries.distribution)
1736 spph = self.publisher.getPubSource(
1737 distroseries=self.distroseries, sourcename="moveme",
1738 archive=source_archive)
1739 with person_logged_in(target_archive.owner):
1740 target_archive.newComponentUploader(source_archive.owner, "main")
1741 job = self.createCopyJobForSPPH(
1742 spph, source_archive, target_archive,
1743 requester=source_archive.owner, move=True)
1744 self.runJob(job)
1745 self.assertEqual(JobStatus.COMPLETED, job.status)
1746 new_spph = target_archive.getPublishedSources(name="moveme").one()
1747 self.assertEqual(PackagePublishingStatus.PENDING, new_spph.status)
1748 self.assertEqual(PackagePublishingStatus.DELETED, spph.status)
1749 self.assertEqual(
1750 "Moved to %s in %s" % (
1751 self.distroseries.name, target_archive.reference),
1752 spph.removal_comment)
1753
17291754
1730class TestViaCelery(TestCaseWithFactory):1755class TestViaCelery(TestCaseWithFactory):
1731 """PackageCopyJob runs under Celery."""1756 """PackageCopyJob runs under Celery."""
diff --git a/lib/lp/soyuz/vocabularies.py b/lib/lp/soyuz/vocabularies.py
index b4f40b5..bcaf16e 100644
--- a/lib/lp/soyuz/vocabularies.py
+++ b/lib/lp/soyuz/vocabularies.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the GNU1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the GNU
2# Affero General Public License version 3 (see the file LICENSE).2# Affero General Public License version 3 (see the file LICENSE).
33
4"""Soyuz vocabularies."""4"""Soyuz vocabularies."""
@@ -8,6 +8,7 @@ __metaclass__ = type
8__all__ = [8__all__ = [
9 'ComponentVocabulary',9 'ComponentVocabulary',
10 'FilteredDistroArchSeriesVocabulary',10 'FilteredDistroArchSeriesVocabulary',
11 'make_archive_vocabulary',
11 'PackageReleaseVocabulary',12 'PackageReleaseVocabulary',
12 'PPAVocabulary',13 'PPAVocabulary',
13 ]14 ]
@@ -18,7 +19,10 @@ from storm.locals import (
18 )19 )
19from zope.component import getUtility20from zope.component import getUtility
20from zope.interface import implementer21from zope.interface import implementer
21from zope.schema.vocabulary import SimpleTerm22from zope.schema.vocabulary import (
23 SimpleTerm,
24 SimpleVocabulary,
25 )
22from zope.security.interfaces import Unauthorized26from zope.security.interfaces import Unauthorized
2327
24from lp.registry.model.distroseries import DistroSeries28from lp.registry.model.distroseries import DistroSeries
@@ -150,3 +154,12 @@ class PPAVocabulary(SQLObjectVocabularyBase):
150 search_clause)154 search_clause)
151 return self._table.select(155 return self._table.select(
152 clause, orderBy=self._orderBy, clauseTables=self._clauseTables)156 clause, orderBy=self._orderBy, clauseTables=self._clauseTables)
157
158
159def make_archive_vocabulary(archives):
160 terms = []
161 for archive in archives:
162 label = '%s [%s]' % (archive.displayname, archive.reference)
163 terms.append(SimpleTerm(archive, archive.reference, label))
164 terms.sort(key=lambda x: x.value.reference)
165 return SimpleVocabulary(terms)

Subscribers

People subscribed via source and target branches

to status/vote changes: