Merge lp:~julian-edwards/launchpad/async-copying-part-2 into lp:launchpad

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 13459
Proposed branch: lp:~julian-edwards/launchpad/async-copying-part-2
Merge into: lp:launchpad
Prerequisite: lp:~julian-edwards/launchpad/async-copying-bug-809805
Diff against target: 979 lines (+359/-45)
16 files modified
database/schema/security.cfg (+1/-0)
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+2/-0)
lib/lp/registry/browser/distroseries.py (+1/-1)
lib/lp/services/job/interfaces/job.py (+7/-0)
lib/lp/services/job/model/job.py (+13/-3)
lib/lp/services/job/tests/test_job.py (+24/-4)
lib/lp/soyuz/browser/archive.py (+2/-1)
lib/lp/soyuz/browser/tests/test_archive_webservice.py (+31/-5)
lib/lp/soyuz/browser/tests/test_package_copying_mixin.py (+3/-3)
lib/lp/soyuz/interfaces/archive.py (+45/-0)
lib/lp/soyuz/interfaces/packagecopyjob.py (+4/-2)
lib/lp/soyuz/model/archive.py (+45/-0)
lib/lp/soyuz/model/packagecopyjob.py (+13/-8)
lib/lp/soyuz/tests/test_archive.py (+88/-1)
lib/lp/soyuz/tests/test_packagecopyjob.py (+75/-15)
lib/lp/testing/factory.py (+5/-2)
To merge this branch: bzr merge lp:~julian-edwards/launchpad/async-copying-part-2
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+67989@code.launchpad.net

Commit message

Expose a simple webservice API to copy packages asynchronously

Description of the change

This is the second part of my copyPackage(s) change for asynchronous package copying. This part adds copyPackages() which does multiple packages in one go similar to syncSources().

See the pre-requisite at https://code.launchpad.net/~julian-edwards/launchpad/async-copying-bug-809805/+merge/67941

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

Looks good to me.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2011-07-14 10:05:11 +0000
3+++ database/schema/security.cfg 2011-07-18 08:59:28 +0000
4@@ -1058,6 +1058,7 @@
5 public.sourcepackagepublishinghistory = SELECT, INSERT
6 public.sourcepackagerelease = SELECT
7 public.sourcepackagereleasefile = SELECT, INSERT, UPDATE
8+public.teamparticipation = SELECT
9 type=user
10
11 [distroseriesdifferencejob]
12
13=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
14--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-07-18 08:59:27 +0000
15+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-07-18 08:59:28 +0000
16@@ -405,6 +405,8 @@
17 patch_plain_parameter_type(IArchive, 'syncSources', 'from_archive', IArchive)
18 patch_plain_parameter_type(IArchive, 'syncSource', 'from_archive', IArchive)
19 patch_plain_parameter_type(IArchive, 'copyPackage', 'from_archive', IArchive)
20+patch_plain_parameter_type(
21+ IArchive, 'copyPackages', 'from_archive', IArchive)
22 patch_entry_return_type(IArchive, 'newSubscription', IArchiveSubscriber)
23 patch_plain_parameter_type(
24 IArchive, 'getArchiveDependency', 'dependency', IArchive)
25
26=== modified file 'lib/lp/registry/browser/distroseries.py'
27--- lib/lp/registry/browser/distroseries.py 2011-07-08 09:06:24 +0000
28+++ lib/lp/registry/browser/distroseries.py 2011-07-18 08:59:28 +0000
29@@ -1161,7 +1161,7 @@
30 )
31 for dsd in self.getUpgrades()]
32 getUtility(IPlainPackageCopyJobSource).createMultiple(
33- target_distroseries, copies,
34+ target_distroseries, copies, self.user,
35 copy_policy=PackageCopyPolicy.MASS_SYNC)
36
37 self.request.response.addInfoNotification(
38
39=== modified file 'lib/lp/services/job/interfaces/job.py'
40--- lib/lp/services/job/interfaces/job.py 2011-07-06 14:02:54 +0000
41+++ lib/lp/services/job/interfaces/job.py 2011-07-18 08:59:28 +0000
42@@ -22,6 +22,7 @@
43 DBEnumeratedType,
44 DBItem,
45 )
46+from lazr.restful.fields import Reference
47 from zope.interface import (
48 Attribute,
49 Interface,
50@@ -35,6 +36,7 @@
51 )
52
53 from canonical.launchpad import _
54+from lp.registry.interfaces.person import IPerson
55
56
57 class SuspendJobException(Exception):
58@@ -109,6 +111,11 @@
59 max_retries = Int(title=_(
60 'The number of retries permitted before this job permanently fails.'))
61
62+ requester = Reference(
63+ IPerson, title=_("The person who requested the job"),
64+ required=False, readonly=True
65+ )
66+
67 is_pending = Bool(
68 title=_("Whether or not this job's status is such that it "
69 "could eventually complete."))
70
71=== modified file 'lib/lp/services/job/model/job.py'
72--- lib/lp/services/job/model/job.py 2011-07-06 14:02:54 +0000
73+++ lib/lp/services/job/model/job.py 2011-07-18 08:59:28 +0000
74@@ -21,6 +21,10 @@
75 Or,
76 Select,
77 )
78+from storm.locals import (
79+ Int,
80+ Reference,
81+ )
82 from zope.interface import implements
83
84 from canonical.database.constants import UTC_NOW
85@@ -73,6 +77,9 @@
86
87 max_retries = IntCol(default=0)
88
89+ requester_id = Int(name='requester', allow_none=True)
90+ requester = Reference(requester_id, 'Person.id')
91+
92 # Mapping of valid target states from a given state.
93 _valid_transitions = {
94 JobStatus.WAITING:
95@@ -108,16 +115,19 @@
96 return self.status in self.PENDING_STATUSES
97
98 @classmethod
99- def createMultiple(self, store, num_jobs):
100+ def createMultiple(self, store, num_jobs, requester=None):
101 """Create multiple `Job`s at once.
102
103 :param store: `Store` to ceate the jobs in.
104 :param num_jobs: Number of `Job`s to create.
105+ :param request: The `IPerson` requesting the jobs.
106 :return: An iterable of `Job.id` values for the new jobs.
107 """
108- job_contents = ["(%s)" % quote(JobStatus.WAITING)] * num_jobs
109+ job_contents = [
110+ "(%s, %s)" % (
111+ quote(JobStatus.WAITING), quote(requester))] * num_jobs
112 result = store.execute("""
113- INSERT INTO Job (status)
114+ INSERT INTO Job (status, requester)
115 VALUES %s
116 RETURNING id
117 """ % ", ".join(job_contents))
118
119=== modified file 'lib/lp/services/job/tests/test_job.py'
120--- lib/lp/services/job/tests/test_job.py 2011-07-06 14:02:54 +0000
121+++ lib/lp/services/job/tests/test_job.py 2011-07-18 08:59:28 +0000
122@@ -22,10 +22,13 @@
123 Job,
124 LeaseHeld,
125 )
126-from lp.testing import TestCase
127-
128-
129-class TestJob(TestCase):
130+from lp.testing import (
131+ TestCase,
132+ TestCaseWithFactory,
133+ )
134+
135+
136+class TestJob(TestCaseWithFactory):
137 """Ensure Job behaves as intended."""
138
139 layer = ZopelessDatabaseLayer
140@@ -39,6 +42,12 @@
141 job = Job()
142 self.assertEqual(job.status, JobStatus.WAITING)
143
144+ def test_stores_requester(self):
145+ job = Job()
146+ random_joe = self.factory.makePerson()
147+ job.requester = random_joe
148+ self.assertEqual(random_joe, job.requester)
149+
150 def test_createMultiple_creates_requested_number_of_jobs(self):
151 job_ids = list(Job.createMultiple(IStore(Job), 3))
152 self.assertEqual(3, len(job_ids))
153@@ -55,6 +64,17 @@
154 job = store.get(Job, Job.createMultiple(store, 1)[0])
155 self.assertEqual(JobStatus.WAITING, job.status)
156
157+ def test_createMultiple_sets_requester(self):
158+ store = IStore(Job)
159+ requester = self.factory.makePerson()
160+ job = store.get(Job, Job.createMultiple(store, 1, requester)[0])
161+ self.assertEqual(requester, job.requester)
162+
163+ def test_createMultiple_defaults_requester_to_None(self):
164+ store = IStore(Job)
165+ job = store.get(Job, Job.createMultiple(store, 1)[0])
166+ self.assertEqual(None, job.requester)
167+
168 def test_start(self):
169 """Job.start should update the object appropriately.
170
171
172=== modified file 'lib/lp/soyuz/browser/archive.py'
173--- lib/lp/soyuz/browser/archive.py 2011-06-29 21:49:36 +0000
174+++ lib/lp/soyuz/browser/archive.py 2011-07-18 08:59:28 +0000
175@@ -1284,7 +1284,8 @@
176 spph.source_package_name, spph.archive, dest_archive, dest_series,
177 dest_pocket, include_binaries=include_binaries,
178 package_version=spph.sourcepackagerelease.version,
179- copy_policy=PackageCopyPolicy.INSECURE)
180+ copy_policy=PackageCopyPolicy.INSECURE,
181+ requester=person)
182
183 return structured("""
184 <p>Requested sync of %s packages.</p>
185
186=== modified file 'lib/lp/soyuz/browser/tests/test_archive_webservice.py'
187--- lib/lp/soyuz/browser/tests/test_archive_webservice.py 2011-07-18 08:59:27 +0000
188+++ lib/lp/soyuz/browser/tests/test_archive_webservice.py 2011-07-18 08:59:28 +0000
189@@ -19,7 +19,10 @@
190 from canonical.testing.layers import DatabaseFunctionalLayer
191 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
192 from lp.registry.interfaces.pocket import PackagePublishingPocket
193-from lp.soyuz.enums import ArchivePurpose
194+from lp.soyuz.enums import (
195+ ArchivePurpose,
196+ PackagePublishingStatus,
197+ )
198 from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
199 from lp.soyuz.interfaces.processor import IProcessorFamilySet
200 from lp.testing import (
201@@ -311,17 +314,16 @@
202
203
204 class TestCopyPackage(WebServiceTestCase):
205- """Webservice test cases for the copyPackage method"""
206+ """Webservice test cases for the copyPackage/copyPackages methods"""
207
208- def test_copyPackage(self):
209- """Basic smoke test"""
210+ def setup_data(self):
211 self.ws_version = "devel"
212 uploader_dude = self.factory.makePerson()
213 source_archive = self.factory.makeArchive()
214 target_archive = self.factory.makeArchive(
215 purpose=ArchivePurpose.PRIMARY)
216 source = self.factory.makeSourcePackagePublishingHistory(
217- archive=source_archive)
218+ archive=source_archive, status=PackagePublishingStatus.PUBLISHED)
219 source_name = source.source_package_name
220 version = source.source_package_version
221 to_pocket = PackagePublishingPocket.RELEASE
222@@ -330,6 +332,13 @@
223 with person_logged_in(target_archive.owner):
224 target_archive.newComponentUploader(uploader_dude, "universe")
225 transaction.commit()
226+ return (source_archive, source_name, target_archive, to_pocket,
227+ to_series, uploader_dude, version)
228+
229+ def test_copyPackage(self):
230+ """Basic smoke test"""
231+ (source_archive, source_name, target_archive, to_pocket, to_series,
232+ uploader_dude, version) = self.setup_data()
233
234 ws_target_archive = self.wsObject(target_archive, user=uploader_dude)
235 ws_source_archive = self.wsObject(source_archive)
236@@ -344,6 +353,23 @@
237 copy_job = job_source.getActiveJobs(target_archive).one()
238 self.assertEqual(target_archive, copy_job.target_archive)
239
240+ def test_copyPackages(self):
241+ """Basic smoke test"""
242+ (source_archive, source_name, target_archive, to_pocket, to_series,
243+ uploader_dude, version) = self.setup_data()
244+
245+ ws_target_archive = self.wsObject(target_archive, user=uploader_dude)
246+ ws_source_archive = self.wsObject(source_archive)
247+
248+ ws_target_archive.copyPackages(
249+ source_names=[source_name], from_archive=ws_source_archive,
250+ to_pocket=to_pocket.name, to_series=to_series.name,
251+ include_binaries=False)
252+ transaction.commit()
253+
254+ job_source = getUtility(IPlainPackageCopyJobSource)
255+ copy_job = job_source.getActiveJobs(target_archive).one()
256+ self.assertEqual(target_archive, copy_job.target_archive)
257
258 def test_suite():
259 return unittest.TestLoader().loadTestsFromName(__name__)
260
261=== modified file 'lib/lp/soyuz/browser/tests/test_package_copying_mixin.py'
262--- lib/lp/soyuz/browser/tests/test_package_copying_mixin.py 2011-06-02 08:16:03 +0000
263+++ lib/lp/soyuz/browser/tests/test_package_copying_mixin.py 2011-07-18 08:59:28 +0000
264@@ -219,7 +219,7 @@
265 pocket = self.factory.getAnyPocket()
266 copy_asynchronously(
267 [spph], archive, dest_series, pocket, include_binaries=False,
268- check_permissions=False)
269+ check_permissions=False, person=self.factory.makePerson())
270 self.assertEqual(None, find_spph_copy(archive, spph))
271
272 def test_copy_synchronously_lists_packages(self):
273@@ -243,7 +243,7 @@
274 archive = dest_series.distribution.main_archive
275 copy_asynchronously(
276 [spph], archive, dest_series, pocket, include_binaries=False,
277- check_permissions=False)
278+ check_permissions=False, person=self.factory.makePerson())
279 jobs = list(getUtility(IPlainPackageCopyJobSource).getActiveJobs(
280 archive))
281 self.assertEqual(1, len(jobs))
282@@ -263,7 +263,7 @@
283 view.canCopySynchronously = FakeMethod(result=False)
284 view.do_copy(
285 'selected_differences', [spph], archive, dest_series, pocket,
286- False, check_permissions=False)
287+ False, check_permissions=False, person=self.factory.makePerson())
288 jobs = list(getUtility(IPlainPackageCopyJobSource).getActiveJobs(
289 archive))
290 self.assertNotEqual([], jobs)
291
292=== modified file 'lib/lp/soyuz/interfaces/archive.py'
293--- lib/lp/soyuz/interfaces/archive.py 2011-07-18 08:59:27 +0000
294+++ lib/lp/soyuz/interfaces/archive.py 2011-07-18 08:59:28 +0000
295@@ -1258,6 +1258,51 @@
296 :raises CannotCopy: if there is a problem copying.
297 """
298
299+ @call_with(person=REQUEST_USER)
300+ @operation_parameters(
301+ source_names=List(
302+ title=_("Source package names"),
303+ value_type=TextLine()),
304+ from_archive=Reference(schema=Interface),
305+ #Really IArchive, see below
306+ to_pocket=TextLine(title=_("Pocket name")),
307+ to_series=TextLine(title=_("Distroseries name"), required=False),
308+ include_binaries=Bool(
309+ title=_("Include Binaries"),
310+ description=_("Whether or not to copy binaries already built for"
311+ " this source"),
312+ required=False))
313+ @export_write_operation()
314+ @operation_for_version('devel')
315+ def copyPackages(source_names, from_archive, to_pocket, person,
316+ to_series=None, include_binaries=False):
317+ """Atomically copy multiple named sources into this archive from another.
318+
319+ Asynchronously copy the most recent PUBLISHED versions of the named
320+ sources to the destination archive if necessary. Calls to this
321+ method will return immediately if the copy passes basic security
322+ checks and the copy will happen sometime later with full checking.
323+
324+ This operation will only succeed when all requested packages
325+ are synchronised between the archives. If any of the requested
326+ copies cannot be performed, the whole operation will fail. There
327+ will be no partial changes of the destination archive.
328+
329+ :param source_names: a list of string names of packages to copy.
330+ :param from_archive: the source archive from which to copy.
331+ :param to_pocket: the target pocket (as a string).
332+ :param to_series: the target distroseries (as a string).
333+ :param include_binaries: optional boolean, controls whether or not
334+ the published binaries for each given source should also be
335+ copied along with the source.
336+ :param person: the `IPerson` who requests the sync.
337+
338+ :raises NoSuchSourcePackageName: if the source name is invalid
339+ :raises PocketNotFound: if the pocket name is invalid
340+ :raises NoSuchDistroSeries: if the distro series name is invalid
341+ :raises CannotCopy: if there is a problem copying.
342+ """
343+
344
345 class IArchiveAppend(Interface):
346 """Archive interface for operations restricted by append privilege."""
347
348=== modified file 'lib/lp/soyuz/interfaces/packagecopyjob.py'
349--- lib/lp/soyuz/interfaces/packagecopyjob.py 2011-07-12 14:23:40 +0000
350+++ lib/lp/soyuz/interfaces/packagecopyjob.py 2011-07-18 08:59:28 +0000
351@@ -126,7 +126,7 @@
352 def create(package_name, source_archive,
353 target_archive, target_distroseries, target_pocket,
354 include_binaries=False, package_version=None,
355- copy_policy=PackageCopyPolicy.INSECURE):
356+ copy_policy=PackageCopyPolicy.INSECURE, requester=None):
357 """Create a new `IPlainPackageCopyJob`.
358
359 :param package_name: The name of the source package to copy.
360@@ -141,9 +141,10 @@
361 :param package_version: The version string for the package version
362 that is to be copied.
363 :param copy_policy: Applicable `PackageCopyPolicy`.
364+ :param requester: The user requesting the copy.
365 """
366
367- def createMultiple(target_distroseries, copy_tasks,
368+ def createMultiple(target_distroseries, copy_tasks, requester,
369 copy_policy=PackageCopyPolicy.INSECURE,
370 include_binaries=False):
371 """Create multiple new `IPlainPackageCopyJob`s at once.
372@@ -153,6 +154,7 @@
373 :param copy_tasks: A list of tuples describing the copies to be
374 performed: (package name, package version, source archive,
375 target archive, target pocket).
376+ :param requester: The user requesting the copy.
377 :param copy_policy: Applicable `PackageCopyPolicy`.
378 :param include_binaries: As in `do_copy`.
379 :return: An iterable of `PackageCopyJob` ids.
380
381=== modified file 'lib/lp/soyuz/model/archive.py'
382--- lib/lp/soyuz/model/archive.py 2011-07-18 08:59:27 +0000
383+++ lib/lp/soyuz/model/archive.py 2011-07-18 08:59:28 +0000
384@@ -102,6 +102,7 @@
385 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
386 from lp.registry.model.sourcepackagename import SourcePackageName
387 from lp.registry.model.teammembership import TeamParticipation
388+from lp.services.database.bulk import load_related
389 from lp.services.job.interfaces.job import JobStatus
390 from lp.services.propertycache import (
391 cachedproperty,
392@@ -1550,6 +1551,47 @@
393 package_version=version, include_binaries=include_binaries,
394 copy_policy=PackageCopyPolicy.INSECURE)
395
396+ def copyPackages(self, source_names, from_archive, to_pocket,
397+ person, to_series=None, include_binaries=None):
398+ """See `IArchive`."""
399+ sources = self._collectLatestPublishedSources(
400+ from_archive, source_names)
401+ if not sources:
402+ raise CannotCopy(
403+ "None of the supplied package names are published")
404+
405+ # Bulk-load the sourcepackagereleases so that the list
406+ # comprehension doesn't generate additional queries. The
407+ # sourcepackagenames themselves will already have been loaded when
408+ # generating the list of source publications in "sources".
409+ load_related(
410+ SourcePackageRelease, sources, ["sourcepackagereleaseID"])
411+ sourcepackagenames = [source.sourcepackagerelease.sourcepackagename
412+ for source in sources]
413+
414+ # Now do a mass check of permissions.
415+ pocket = self._text_to_pocket(to_pocket)
416+ series = self._text_to_series(to_series)
417+ check_copy_permissions(
418+ person, self, series, pocket, sourcepackagenames)
419+
420+ # If we get this far then we can create the PackageCopyJob.
421+ copy_tasks = []
422+ for source in sources:
423+ task = (
424+ source.sourcepackagerelease.sourcepackagename,
425+ source.sourcepackagerelease.version,
426+ from_archive,
427+ self,
428+ PackagePublishingPocket.RELEASE
429+ )
430+ copy_tasks.append(task)
431+
432+ job_source = getUtility(IPlainPackageCopyJobSource)
433+ job_source.createMultiple(
434+ series, copy_tasks, copy_policy=PackageCopyPolicy.INSECURE,
435+ include_binaries=include_binaries)
436+
437 def _collectLatestPublishedSources(self, from_archive, source_names):
438 """Private helper to collect the latest published sources for an
439 archive.
440@@ -1557,6 +1599,9 @@
441 :raises NoSuchSourcePackageName: If any of the source_names do not
442 exist.
443 """
444+ # XXX bigjools bug=810421
445+ # This code is inefficient. It should try to bulk load all the
446+ # sourcepackagenames and publications instead of iterating.
447 sources = []
448 for name in source_names:
449 # Check to see if the source package exists. This will raise
450
451=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
452--- lib/lp/soyuz/model/packagecopyjob.py 2011-07-08 09:06:24 +0000
453+++ lib/lp/soyuz/model/packagecopyjob.py 2011-07-18 08:59:28 +0000
454@@ -131,9 +131,11 @@
455 return cls.wrap(IStore(PackageCopyJob).get(PackageCopyJob, pcj_id))
456
457 def __init__(self, source_archive, target_archive, target_distroseries,
458- job_type, metadata, package_name=None, copy_policy=None):
459+ job_type, metadata, requester, package_name=None,
460+ copy_policy=None):
461 super(PackageCopyJob, self).__init__()
462 self.job = Job()
463+ self.job.requester = requester
464 self.job_type = job_type
465 self.source_archive = source_archive
466 self.target_archive = target_archive
467@@ -250,9 +252,10 @@
468 def create(cls, package_name, source_archive,
469 target_archive, target_distroseries, target_pocket,
470 include_binaries=False, package_version=None,
471- copy_policy=PackageCopyPolicy.INSECURE):
472+ copy_policy=PackageCopyPolicy.INSECURE, requester=None):
473 """See `IPlainPackageCopyJobSource`."""
474 assert package_version is not None, "No package version specified."
475+ assert requester is not None, "No requester specified."
476 metadata = cls._makeMetadata(
477 target_pocket, package_version, include_binaries)
478 job = PackageCopyJob(
479@@ -262,7 +265,8 @@
480 target_distroseries=target_distroseries,
481 package_name=package_name,
482 copy_policy=copy_policy,
483- metadata=metadata)
484+ metadata=metadata,
485+ requester=requester)
486 IMasterStore(PackageCopyJob).add(job)
487 return cls(job)
488
489@@ -292,12 +296,12 @@
490 return format_string % sqlvalues(*data)
491
492 @classmethod
493- def createMultiple(cls, target_distroseries, copy_tasks,
494+ def createMultiple(cls, target_distroseries, copy_tasks, requester,
495 copy_policy=PackageCopyPolicy.INSECURE,
496 include_binaries=False):
497 """See `IPlainPackageCopyJobSource`."""
498 store = IMasterStore(Job)
499- job_ids = Job.createMultiple(store, len(copy_tasks))
500+ job_ids = Job.createMultiple(store, len(copy_tasks), requester)
501 job_contents = [
502 cls._composeJobInsertionTuple(
503 target_distroseries, copy_policy, include_binaries, job_id,
504@@ -478,13 +482,14 @@
505 do_copy(
506 sources=[source_package], archive=self.target_archive,
507 series=self.target_distroseries, pocket=self.target_pocket,
508- include_binaries=self.include_binaries, check_permissions=False,
509- overrides=[override], send_email=send_email)
510+ include_binaries=self.include_binaries, check_permissions=True,
511+ person=self.requester, overrides=[override],
512+ send_email=send_email)
513
514 if pu is not None:
515 # A PackageUpload will only exist if the copy job had to be
516 # held in the queue because of policy/ancestry checks. If one
517- # does exist we need to make sure
518+ # does exist we need to make sure it gets moved to DONE.
519 pu.setDone()
520
521 def abort(self):
522
523=== modified file 'lib/lp/soyuz/tests/test_archive.py'
524--- lib/lp/soyuz/tests/test_archive.py 2011-07-18 08:59:27 +0000
525+++ lib/lp/soyuz/tests/test_archive.py 2011-07-18 08:59:28 +0000
526@@ -1975,7 +1975,7 @@
527 source_archive = self.factory.makeArchive()
528 target_archive = self.factory.makeArchive(purpose=target_purpose)
529 source = self.factory.makeSourcePackagePublishingHistory(
530- archive=source_archive)
531+ archive=source_archive, status=PackagePublishingStatus.PUBLISHED)
532 source_name = source.source_package_name
533 version = source.source_package_version
534 to_pocket = PackagePublishingPocket.RELEASE
535@@ -2068,3 +2068,90 @@
536 to_pocket.name, to_series=to_series.name, include_binaries=False,
537 person=target_archive.owner)
538
539+ def test_copyPackages_with_single_package(self):
540+ (source, source_archive, source_name, target_archive, to_pocket,
541+ to_series, version) = self._setup_copy_data()
542+
543+ with person_logged_in(target_archive.owner):
544+ target_archive.copyPackages(
545+ [source_name], source_archive, to_pocket.name,
546+ to_series=to_series.name, include_binaries=False,
547+ person=target_archive.owner)
548+
549+ # The source should not be published yet in the target_archive.
550+ published = target_archive.getPublishedSources(
551+ name=source.source_package_name).any()
552+ self.assertIs(None, published)
553+
554+ # There should be one copy job.
555+ job_source = getUtility(IPlainPackageCopyJobSource)
556+ copy_job = job_source.getActiveJobs(target_archive).one()
557+ self.assertEqual(target_archive, copy_job.target_archive)
558+
559+ def test_copyPackages_with_multiple_packages(self):
560+ (source, source_archive, source_name, target_archive, to_pocket,
561+ to_series, version) = self._setup_copy_data()
562+ sources = [source]
563+ sources.append(self.factory.makeSourcePackagePublishingHistory(
564+ archive=source_archive,
565+ status=PackagePublishingStatus.PUBLISHED))
566+ sources.append(self.factory.makeSourcePackagePublishingHistory(
567+ archive=source_archive,
568+ status=PackagePublishingStatus.PUBLISHED))
569+ names = [source.sourcepackagerelease.sourcepackagename.name
570+ for source in sources]
571+
572+ with person_logged_in(target_archive.owner):
573+ target_archive.copyPackages(
574+ names, source_archive, to_pocket.name,
575+ to_series=to_series.name, include_binaries=False,
576+ person=target_archive.owner)
577+
578+ # Make sure three copy jobs exist.
579+ job_source = getUtility(IPlainPackageCopyJobSource)
580+ copy_jobs = job_source.getActiveJobs(target_archive)
581+ self.assertEqual(3, copy_jobs.count())
582+
583+ def test_copyPackages_disallows_non_primary_archive_uploaders(self):
584+ # If copying to a primary archive and you're not an uploader for
585+ # the package then you can't copy.
586+ (source, source_archive, source_name, target_archive, to_pocket,
587+ to_series, version) = self._setup_copy_data(
588+ target_purpose=ArchivePurpose.PRIMARY)
589+ person = self.factory.makePerson()
590+ self.assertRaises(
591+ CannotCopy,
592+ target_archive.copyPackages, [source_name], source_archive,
593+ to_pocket.name, to_series=to_series.name, include_binaries=False,
594+ person=person)
595+
596+ def test_copyPackages_allows_primary_archive_uploaders(self):
597+ # Copying to a primary archive if you're already an uploader is OK.
598+ (source, source_archive, source_name, target_archive, to_pocket,
599+ to_series, version) = self._setup_copy_data(
600+ target_purpose=ArchivePurpose.PRIMARY)
601+ person = self.factory.makePerson()
602+ with person_logged_in(target_archive.owner):
603+ target_archive.newComponentUploader(person, "universe")
604+ target_archive.copyPackages(
605+ [source_name], source_archive, to_pocket.name,
606+ to_series=to_series.name, include_binaries=False,
607+ person=person)
608+
609+ # There should be one copy job.
610+ job_source = getUtility(IPlainPackageCopyJobSource)
611+ copy_job = job_source.getActiveJobs(target_archive).one()
612+ self.assertEqual(target_archive, copy_job.target_archive)
613+
614+ def test_copyPackages_disallows_non_PPA_owners(self):
615+ # Only people with launchpad.Append are allowed to call copyPackage.
616+ (source, source_archive, source_name, target_archive, to_pocket,
617+ to_series, version) = self._setup_copy_data()
618+ person = self.factory.makePerson()
619+ self.assertTrue(target_archive.is_ppa)
620+ self.assertRaises(
621+ CannotCopy,
622+ target_archive.copyPackages, [source_name], source_archive,
623+ to_pocket.name, to_series=to_series.name, include_binaries=False,
624+ person=person)
625+
626
627=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
628--- lib/lp/soyuz/tests/test_packagecopyjob.py 2011-07-12 14:52:52 +0000
629+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2011-07-18 08:59:28 +0000
630@@ -3,6 +3,7 @@
631
632 """Tests for sync package jobs."""
633
634+import operator
635 from testtools.content import text_content
636 from testtools.matchers import (
637 Equals,
638@@ -92,9 +93,10 @@
639 target_archive = dsd.derived_series.main_archive
640 target_distroseries = dsd.derived_series
641 target_pocket = self.factory.getAnyPocket()
642+ requester = self.factory.makePerson()
643 return getUtility(IPlainPackageCopyJobSource).create(
644 dsd.source_package_name.name, source_archive, target_archive,
645- target_distroseries, target_pocket,
646+ target_distroseries, target_pocket, requester=requester,
647 package_version=dsd.parent_source_version, **kwargs)
648
649 def runJob(self, job):
650@@ -122,13 +124,15 @@
651 distroseries = self.factory.makeDistroSeries()
652 archive1 = self.factory.makeArchive(distroseries.distribution)
653 archive2 = self.factory.makeArchive(distroseries.distribution)
654+ requester = self.factory.makePerson()
655 source = getUtility(IPlainPackageCopyJobSource)
656 job = source.create(
657 package_name="foo", source_archive=archive1,
658 target_archive=archive2, target_distroseries=distroseries,
659 target_pocket=PackagePublishingPocket.RELEASE,
660 package_version="1.0-1", include_binaries=False,
661- copy_policy=PackageCopyPolicy.MASS_SYNC)
662+ copy_policy=PackageCopyPolicy.MASS_SYNC,
663+ requester=requester)
664 self.assertProvides(job, IPackageCopyJob)
665 self.assertEquals(archive1.id, job.source_archive_id)
666 self.assertEquals(archive1, job.source_archive)
667@@ -140,6 +144,7 @@
668 self.assertEqual("1.0-1", job.package_version)
669 self.assertEquals(False, job.include_binaries)
670 self.assertEquals(PackageCopyPolicy.MASS_SYNC, job.copy_policy)
671+ self.assertEqual(requester, job.requester)
672
673 def test_createMultiple_creates_one_job_per_copy(self):
674 mother = self.factory.makeDistroSeriesParent()
675@@ -148,6 +153,7 @@
676 derived_series=derived_series)
677 mother_package = self.factory.makeSourcePackageName()
678 father_package = self.factory.makeSourcePackageName()
679+ requester = self.factory.makePerson()
680 job_source = getUtility(IPlainPackageCopyJobSource)
681 copy_tasks = [
682 (
683@@ -166,7 +172,8 @@
684 ),
685 ]
686 job_ids = list(
687- job_source.createMultiple(mother.derived_series, copy_tasks))
688+ job_source.createMultiple(mother.derived_series, copy_tasks,
689+ requester))
690 jobs = list(job_source.getActiveJobs(derived_series.main_archive))
691 self.assertContentEqual(job_ids, [job.id for job in jobs])
692 self.assertEqual(len(copy_tasks), len(set([job.job for job in jobs])))
693@@ -185,17 +192,24 @@
694 for job in jobs]
695 self.assertEqual(copy_tasks, requested_copies)
696
697+ # The passed requester should be the same on all jobs.
698+ actual_requester = set(job.requester for job in jobs)
699+ self.assertEqual(1, len(actual_requester))
700+ self.assertEqual(requester, jobs[0].requester)
701+
702 def test_getActiveJobs(self):
703 # getActiveJobs() can retrieve all active jobs for an archive.
704 distroseries = self.factory.makeDistroSeries()
705 archive1 = self.factory.makeArchive(distroseries.distribution)
706 archive2 = self.factory.makeArchive(distroseries.distribution)
707 source = getUtility(IPlainPackageCopyJobSource)
708+ requester = self.factory.makePerson()
709 job = source.create(
710 package_name="foo", source_archive=archive1,
711 target_archive=archive2, target_distroseries=distroseries,
712 target_pocket=PackagePublishingPocket.RELEASE,
713- package_version="1.0-1", include_binaries=False)
714+ package_version="1.0-1", include_binaries=False,
715+ requester=requester)
716 self.assertContentEqual([job], source.getActiveJobs(archive2))
717
718 def test_getActiveJobs_gets_oldest_first(self):
719@@ -249,12 +263,14 @@
720 distroseries = self.factory.makeDistroSeries()
721 archive1 = self.factory.makeArchive(distroseries.distribution)
722 archive2 = self.factory.makeArchive(distroseries.distribution)
723+ requester = self.factory.makePerson()
724 job_source = getUtility(IPlainPackageCopyJobSource)
725 job = job_source.create(
726 package_name="foo", source_archive=archive1,
727 target_archive=archive2, target_distroseries=distroseries,
728 target_pocket=PackagePublishingPocket.RELEASE,
729- package_version="1.0-1", include_binaries=False)
730+ package_version="1.0-1", include_binaries=False,
731+ requester=requester)
732 naked_job = removeSecurityProxy(job)
733 naked_job.reportFailure = FakeMethod()
734
735@@ -268,12 +284,14 @@
736 package = self.factory.makeSourcePackageName()
737 archive1 = self.factory.makeArchive(distroseries.distribution)
738 archive2 = self.factory.makeArchive(distroseries.distribution)
739+ requester = self.factory.makePerson()
740 source = getUtility(IPlainPackageCopyJobSource)
741 job = source.create(
742 package_name=package.name, source_archive=archive1,
743 target_archive=archive2, target_distroseries=distroseries,
744 target_pocket=PackagePublishingPocket.UPDATES,
745- include_binaries=False, package_version='1.0')
746+ include_binaries=False, package_version='1.0',
747+ requester=requester)
748
749 naked_job = removeSecurityProxy(job)
750 naked_job.reportFailure = FakeMethod()
751@@ -315,12 +333,16 @@
752 archive=target_archive)
753
754 source = getUtility(IPlainPackageCopyJobSource)
755+ requester = self.factory.makePerson()
756+ with person_logged_in(target_archive.owner):
757+ target_archive.newComponentUploader(requester, "main")
758 job = source.create(
759 package_name="libc",
760 source_archive=breezy_archive, target_archive=target_archive,
761 target_distroseries=target_series,
762 target_pocket=PackagePublishingPocket.RELEASE,
763- package_version="2.8-1", include_binaries=False)
764+ package_version="2.8-1", include_binaries=False,
765+ requester=requester)
766 self.assertEqual("libc", job.package_name)
767 self.assertEqual("2.8-1", job.package_version)
768
769@@ -348,12 +370,14 @@
770 distroseries = self.factory.makeDistroSeries()
771 archive1 = self.factory.makeArchive(distroseries.distribution)
772 archive2 = self.factory.makeArchive(distroseries.distribution)
773+ requester = self.factory.makePerson()
774 source = getUtility(IPlainPackageCopyJobSource)
775 job = source.create(
776 package_name="foo", source_archive=archive1,
777 target_archive=archive2, target_distroseries=distroseries,
778 target_pocket=PackagePublishingPocket.RELEASE,
779- package_version="1.0-1", include_binaries=False)
780+ package_version="1.0-1", include_binaries=False,
781+ requester=requester)
782 oops_vars = job.getOopsVars()
783 naked_job = removeSecurityProxy(job)
784 self.assertIn(
785@@ -374,6 +398,7 @@
786 distroseries = publisher.breezy_autotest
787 archive1 = self.factory.makeArchive(distroseries.distribution)
788 archive2 = self.factory.makeArchive(distroseries.distribution)
789+ requester = self.factory.makePerson()
790 publisher.getPubSource(
791 distroseries=distroseries, sourcename="libc",
792 version="2.8-1", status=PackagePublishingStatus.PUBLISHED,
793@@ -382,7 +407,10 @@
794 package_name="libc", source_archive=archive1,
795 target_archive=archive2, target_distroseries=distroseries,
796 target_pocket=PackagePublishingPocket.RELEASE,
797- package_version="2.8-1", include_binaries=False)
798+ package_version="2.8-1", include_binaries=False,
799+ requester=requester)
800+ with person_logged_in(archive2.owner):
801+ archive2.newComponentUploader(requester, "main")
802 transaction.commit()
803
804 out, err, exit_code = run_script(
805@@ -401,12 +429,14 @@
806 distroseries = self.factory.makeDistroSeries()
807 archive1 = self.factory.makeArchive(distroseries.distribution)
808 archive2 = self.factory.makeArchive(distroseries.distribution)
809+ requester = self.factory.makePerson()
810 source = getUtility(IPlainPackageCopyJobSource)
811 job = source.create(
812 package_name="foo", source_archive=archive1,
813 target_archive=archive2, target_distroseries=distroseries,
814 target_pocket=PackagePublishingPocket.RELEASE,
815- package_version="1.0-1", include_binaries=True)
816+ package_version="1.0-1", include_binaries=True,
817+ requester=requester)
818 self.assertEqual(
819 ("<PlainPackageCopyJob to copy package foo from "
820 "{distroseries.distribution.name}/{archive1.name} to "
821@@ -519,6 +549,9 @@
822 # target_archive.
823
824 source = getUtility(IPlainPackageCopyJobSource)
825+ requester = self.factory.makePerson()
826+ with person_logged_in(target_archive.owner):
827+ target_archive.newComponentUploader(requester, "restricted")
828 job = source.create(
829 package_name="libc",
830 package_version="2.8-1",
831@@ -526,7 +559,8 @@
832 target_archive=target_archive,
833 target_distroseries=distroseries,
834 target_pocket=PackagePublishingPocket.RELEASE,
835- include_binaries=False)
836+ include_binaries=False,
837+ requester=requester)
838
839 self.runJob(job)
840
841@@ -556,6 +590,9 @@
842 # Now, run the copy job, which should raise an error because
843 # there's no ancestry.
844 source = getUtility(IPlainPackageCopyJobSource)
845+ requester = self.factory.makePerson()
846+ with person_logged_in(target_archive.owner):
847+ target_archive.newComponentUploader(requester, "main")
848 job = source.create(
849 package_name="copyme",
850 package_version="2.8-1",
851@@ -563,7 +600,8 @@
852 target_archive=target_archive,
853 target_distroseries=distroseries,
854 target_pocket=PackagePublishingPocket.RELEASE,
855- include_binaries=False)
856+ include_binaries=False,
857+ requester=requester)
858
859 self.assertRaises(SuspendJobException, self.runJob, job)
860 # Simulate the job runner suspending after getting a
861@@ -619,6 +657,7 @@
862 # Now, run the copy job.
863
864 source = getUtility(IPlainPackageCopyJobSource)
865+ requester = self.factory.makePerson()
866 job = source.create(
867 package_name="copyme",
868 package_version="2.8-1",
869@@ -626,7 +665,8 @@
870 target_archive=target_archive,
871 target_distroseries=distroseries,
872 target_pocket=PackagePublishingPocket.RELEASE,
873- include_binaries=False)
874+ include_binaries=False,
875+ requester=requester)
876
877 # The job should be suspended and there's a PackageUpload with
878 # its package_copy_job set.
879@@ -672,6 +712,7 @@
880
881 # Now, run the copy job.
882 source = getUtility(IPlainPackageCopyJobSource)
883+ requester = self.factory.makePerson()
884 job = source.create(
885 package_name="copyme",
886 package_version="2.8-1",
887@@ -679,7 +720,8 @@
888 target_archive=target_archive,
889 target_distroseries=distroseries,
890 target_pocket=PackagePublishingPocket.RELEASE,
891- include_binaries=False)
892+ include_binaries=False,
893+ requester=requester)
894
895 # The job should be suspended and there's a PackageUpload with
896 # its package_copy_job set in the UNAPPROVED queue.
897@@ -703,6 +745,7 @@
898 publisher = SoyuzTestPublisher()
899 publisher.prepareBreezyAutotest()
900 distroseries = publisher.breezy_autotest
901+ distroseries.changeslist = "changes@example.com"
902
903 target_archive = self.factory.makeArchive(
904 distroseries.distribution, purpose=ArchivePurpose.PRIMARY)
905@@ -715,6 +758,9 @@
906 archive=source_archive)
907
908 source = getUtility(IPlainPackageCopyJobSource)
909+ requester = self.factory.makePerson(email="requester@example.com")
910+ with person_logged_in(target_archive.owner):
911+ target_archive.newComponentUploader(requester, "main")
912 job = source.create(
913 package_name="copyme",
914 package_version="2.8-1",
915@@ -722,7 +768,8 @@
916 target_archive=target_archive,
917 target_distroseries=distroseries,
918 target_pocket=PackagePublishingPocket.RELEASE,
919- include_binaries=False)
920+ include_binaries=False,
921+ requester=requester)
922
923 # Run the job so it gains a PackageUpload.
924 self.assertRaises(SuspendJobException, self.runJob, job)
925@@ -736,6 +783,9 @@
926 pu = getUtility(IPackageUploadSet).getByPackageCopyJobIDs(
927 [removeSecurityProxy(job).context.id]).one()
928 pu.acceptFromQueue()
929+ # Clear existing emails so we can see only the ones the job
930+ # generates later.
931+ pop_notifications()
932 self.runJob(job)
933
934 # The job should have set the PU status to DONE:
935@@ -745,6 +795,16 @@
936 existing_sources = target_archive.getPublishedSources(name='copyme')
937 self.assertIsNot(None, existing_sources.any())
938
939+ # It would be nice to test emails in a separate test but it would
940+ # require all of the same setup as above again so we might as well
941+ # do it here.
942+ emails = pop_notifications(sort_key=operator.itemgetter('To'))
943+
944+ # We expect an uploader email and an announcement to the changeslist.
945+ self.assertEquals(2, len(emails))
946+ self.assertIn("requester@example.com", emails[0]['To'])
947+ self.assertIn("changes@example.com", emails[1]['To'])
948+
949 def test_findMatchingDSDs_matches_all_DSDs_for_job(self):
950 # findMatchingDSDs finds matching DSDs for any of the packages
951 # in the job.
952
953=== modified file 'lib/lp/testing/factory.py'
954--- lib/lp/testing/factory.py 2011-07-13 20:55:34 +0000
955+++ lib/lp/testing/factory.py 2011-07-18 08:59:28 +0000
956@@ -4165,7 +4165,8 @@
957
958 def makePlainPackageCopyJob(
959 self, package_name=None, package_version=None, source_archive=None,
960- target_archive=None, target_distroseries=None, target_pocket=None):
961+ target_archive=None, target_distroseries=None, target_pocket=None,
962+ requester=None):
963 """Create a new `PlainPackageCopyJob`."""
964 if package_name is None and package_version is None:
965 package_name = self.makeSourcePackageName().name
966@@ -4178,10 +4179,12 @@
967 target_distroseries = self.makeDistroSeries()
968 if target_pocket is None:
969 target_pocket = self.getAnyPocket()
970+ if requester is None:
971+ requester = self.makePerson()
972 return getUtility(IPlainPackageCopyJobSource).create(
973 package_name, source_archive, target_archive,
974 target_distroseries, target_pocket,
975- package_version=package_version)
976+ package_version=package_version, requester=requester)
977
978
979 # Some factory methods return simple Python types. We don't add