Merge lp:~jml/launchpad/upload-permission-joy into lp:launchpad

Proposed by Jonathan Lange
Status: Merged
Approved by: Jonathan Lange
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~jml/launchpad/upload-permission-joy
Merge into: lp:launchpad
Diff against target: 698 lines (+332/-81)
10 files modified
lib/canonical/launchpad/security.py (+4/-36)
lib/lp/archiveuploader/nascentupload.py (+10/-5)
lib/lp/archiveuploader/permission.py (+86/-1)
lib/lp/archiveuploader/uploadpolicy.py (+0/-23)
lib/lp/registry/interfaces/distribution.py (+10/-0)
lib/lp/registry/interfaces/sourcepackage.py (+14/-0)
lib/lp/registry/model/sourcepackage.py (+18/-1)
lib/lp/registry/tests/test_sourcepackage.py (+57/-0)
lib/lp/soyuz/interfaces/component.py (+2/-2)
lib/lp/testing/factory.py (+131/-13)
To merge this branch: bzr merge lp:~jml/launchpad/upload-permission-joy
Reviewer Review Type Date Requested Status
Tim Penhey (community) Approve
Abel Deuring Pending
Review via email: mp+14529@code.launchpad.net

Commit message

Extract a function that gives a clear answer to whether you can upload to a package. Use it for branch edit checks.

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

This branch extracts out a function that says whether you can upload a package or not, and makes sure that this function gets used to get branch edit permissions and also for uploads, thus reducing duplication.

Revision history for this message
Abel Deuring (adeuring) wrote :

returning the review to the team -- got interrupted by an, let's say, serious OOPS...

Revision history for this message
Tim Penhey (thumper) wrote :

As discussed on IRC, some small changes, but otherwise good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2009-11-17 09:51:40 +0000
3+++ lib/canonical/launchpad/security.py 2009-11-19 15:18:17 +0000
4@@ -10,8 +10,8 @@
5 from zope.component import getUtility
6
7 from canonical.launchpad.interfaces.account import IAccount
8+from lp.archiveuploader.permission import can_upload_to_archive
9 from canonical.launchpad.interfaces.emailaddress import IEmailAddress
10-from lp.archiveuploader.permission import verify_upload
11 from lp.registry.interfaces.announcement import IAnnouncement
12 from lp.soyuz.interfaces.archive import IArchive
13 from lp.soyuz.interfaces.archivepermission import (
14@@ -1578,17 +1578,6 @@
15
16 def can_upload_linked_package(person, branch):
17 """True if person may upload the package linked to `branch`."""
18-
19- def get_current_release(ssp):
20- """Get current release for the source package linked to branch.
21-
22- This function uses the `ISuiteSourcePackage` instance supplied.
23- """
24- package = ssp.sourcepackage
25- releases = ssp.distroseries.getCurrentSourceReleases(
26- [package.sourcepackagename])
27- return releases.get(package, None)
28-
29 # No associated `ISuiteSourcePackage` data -> not an official branch.
30 # Abort.
31 ssp_list = branch.associatedSuiteSourcePackages()
32@@ -1601,31 +1590,10 @@
33 # around this by assuming that things are fine as long as we find at least
34 # one combination that allows us to upload the corresponding source
35 # package.
36-
37- # Go through the associated `ISuiteSourcePackage` instances and see
38- # whether we can upload to any of the distro series/pocket combinations.
39- ssp = None
40 for ssp in ssp_list:
41- # Can we upload to the respective pocket?
42- if ssp.distroseries.canUploadToPocket(ssp.pocket):
43- break
44- else:
45- # Loop terminated normally i.e. we could not upload to any of the
46- # (distroseries, pocket) combinations found.
47- return False
48-
49- archive = ssp.distroseries.distribution.main_archive
50- # Find the component the linked source package was published in.
51- current_release = get_current_release(ssp)
52- component = getattr(current_release, 'component', None)
53-
54- # Is person authorised to upload the source package this branch
55- # is targeting?
56- result = verify_upload(
57- person, ssp.sourcepackagename, archive, component, ssp.distroseries)
58- # verify_upload() indicates that person *is* allowed to upload by
59- # returning None.
60- return result is None
61+ if can_upload_to_archive(person, ssp):
62+ return True
63+ return False
64
65
66 class AdminBranch(AuthorizationBase):
67
68=== modified file 'lib/lp/archiveuploader/nascentupload.py'
69--- lib/lp/archiveuploader/nascentupload.py 2009-11-09 17:54:02 +0000
70+++ lib/lp/archiveuploader/nascentupload.py 2009-11-19 15:18:17 +0000
71@@ -28,7 +28,7 @@
72 from lp.archiveuploader.nascentuploadfile import (
73 UploadError, UploadWarning, CustomUploadFile, SourceUploadFile,
74 BaseBinaryUploadFile)
75-from lp.archiveuploader.permission import verify_upload
76+from lp.archiveuploader.permission import check_upload_to_archive
77 from lp.registry.interfaces.pocket import PackagePublishingPocket
78 from lp.soyuz.interfaces.archive import ArchivePurpose, MAIN_ARCHIVE_PURPOSES
79 from canonical.launchpad.interfaces import (
80@@ -504,9 +504,10 @@
81 source_name = getUtility(
82 ISourcePackageNameSet).queryByName(self.changes.dsc.package)
83
84- rejection_reason = verify_upload(
85- signer, source_name, archive, self.changes.dsc.component,
86- self.policy.distroseries, not self.is_new)
87+ rejection_reason = check_upload_to_archive(
88+ signer, self.policy.distroseries, source_name, archive,
89+ self.changes.dsc.component, self.policy.pocket, not self.is_new)
90+
91 if rejection_reason is not None:
92 self.reject(str(rejection_reason))
93
94@@ -1012,6 +1013,11 @@
95 if self.is_ppa:
96 return
97
98+ # XXX: JonathanLange 2009-09-16: It would be nice to use
99+ # ISourcePackage.get_default_archive here, since it has the same
100+ # logic. However, I'm not sure whether we can get a SourcePackage
101+ # object.
102+
103 # See if there is an archive to override with.
104 distribution = self.policy.distroseries.distribution
105 archive = distribution.getArchiveByComponent(
106@@ -1027,4 +1033,3 @@
107 else:
108 # Reset the archive in the policy to the partner archive.
109 self.policy.archive = archive
110-
111
112=== modified file 'lib/lp/archiveuploader/permission.py'
113--- lib/lp/archiveuploader/permission.py 2009-11-09 17:50:23 +0000
114+++ lib/lp/archiveuploader/permission.py 2009-11-19 15:18:17 +0000
115@@ -7,14 +7,17 @@
116 __all__ = [
117 'CannotUploadToArchive',
118 'CannotUploadToPPA',
119+ 'can_upload_to_archive',
120+ 'check_upload_to_archive',
121 'components_valid_for',
122 'verify_upload',
123 ]
124
125 from zope.component import getUtility
126
127-from lp.registry.interfaces.distribution import IDistributionSet
128+from lp.registry.interfaces.pocket import PackagePublishingPocket
129 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
130+from lp.soyuz.interfaces.archive import ArchivePurpose
131
132
133 class CannotUploadToArchive:
134@@ -30,6 +33,15 @@
135 return self._message
136
137
138+class CannotUploadToPocket:
139+ """Returned when a pocket is closed for uploads."""
140+
141+ def __init__(self, distroseries, pocket):
142+ super(CannotUploadToPocket, self).__init__(
143+ "Not permitted to upload to the %s pocket in a series in the "
144+ "'%s' state." % (pocket.name, distroseries.status.name))
145+
146+
147 class CannotUploadToPPA(CannotUploadToArchive):
148 """Raised when a person cannot upload to a PPA."""
149
150@@ -63,6 +75,18 @@
151 super(NoRightsForComponent, self).__init__(component=component.name)
152
153
154+class InvalidPocketForPPA(CannotUploadToArchive):
155+ """PPAs only support some pockets."""
156+
157+ _fmt = "PPA uploads must be for the RELEASE pocket."
158+
159+
160+class InvalidPocketForPartnerArchive(CannotUploadToArchive):
161+ """Partner archives only support some pockets."""
162+
163+ _fmt = "Partner uploads must be for the RELEASE or PROPOSED pocket."
164+
165+
166 def components_valid_for(archive, person):
167 """Return the components that 'person' can upload to 'archive'.
168
169@@ -75,6 +99,67 @@
170 return set(permission.component for permission in permissions)
171
172
173+def can_upload_to_archive(person, suitesourcepackage, archive=None):
174+ """Check if 'person' upload 'suitesourcepackage' to 'archive'.
175+
176+ :param person: An `IPerson` who might be uploading.
177+ :param suitesourcepackage: An `ISuiteSourcePackage` to be uploaded.
178+ :param archive: The `IArchive` to upload to. If not provided, defaults
179+ to the default archive for the source package. (See
180+ `ISourcePackage.get_default_archive`).
181+ :return: True if they can, False if they cannot.
182+ """
183+ sourcepackage = suitesourcepackage.sourcepackage
184+ if archive is None:
185+ archive = sourcepackage.get_default_archive()
186+ pocket = suitesourcepackage.pocket
187+ distroseries = sourcepackage.distroseries
188+ sourcepackagename = sourcepackage.sourcepackagename
189+ component = sourcepackage.latest_published_component
190+ # strict_component is True because the source package already exists
191+ # (otherwise we couldn't have a suitesourcepackage object) and
192+ # nascentupload passes True as a matter of policy when the package exists.
193+ reason = check_upload_to_archive(
194+ person, distroseries, sourcepackagename, archive, component, pocket,
195+ strict_component=True)
196+ return reason is None
197+
198+
199+def check_upload_to_archive(person, distroseries, sourcepackagename, archive,
200+ component, pocket, strict_component=True):
201+ """Check if 'person' upload 'suitesourcepackage' to 'archive'.
202+
203+ :param person: An `IPerson` who might be uploading.
204+ :param distroseries: The `IDistroSeries` being uploaded to.
205+ :param sourcepackagename: The `ISourcePackageName` being uploaded.
206+ :param archive: The `IArchive` to upload to. If not provided, defaults
207+ to the default archive for the source package. (See
208+ `ISourcePackage.get_default_archive`).
209+ :param component: The `Component` being uploaded to.
210+ :param pocket: The `PackagePublishingPocket` of 'distroseries' being
211+ uploaded to.
212+ :return: The reason for not being able to upload, None otherwise.
213+ """
214+ if archive.purpose == ArchivePurpose.PARTNER:
215+ if pocket not in (
216+ PackagePublishingPocket.RELEASE,
217+ PackagePublishingPocket.PROPOSED):
218+ return InvalidPocketForPartnerArchive()
219+ else:
220+ # Uploads to the partner archive are allowed in any distroseries
221+ # state.
222+ # XXX julian 2005-05-29 bug=117557:
223+ # This is a greasy hack until bug #117557 is fixed.
224+ if not distroseries.canUploadToPocket(pocket):
225+ return CannotUploadToPocket(distroseries, pocket)
226+
227+ if archive.is_ppa and pocket != PackagePublishingPocket.RELEASE:
228+ return InvalidPocketForPPA()
229+ return verify_upload(
230+ person, sourcepackagename, archive, component, distroseries,
231+ strict_component)
232+
233+
234 def packagesets_valid_for(archive, person):
235 """Return the package sets that 'person' can upload to 'archive'.
236
237
238=== modified file 'lib/lp/archiveuploader/uploadpolicy.py'
239--- lib/lp/archiveuploader/uploadpolicy.py 2009-08-28 06:39:38 +0000
240+++ lib/lp/archiveuploader/uploadpolicy.py 2009-11-19 15:18:17 +0000
241@@ -130,29 +130,6 @@
242 # Buildd binary uploads (resulting from successful builds)
243 # to copy archives may go into *any* pocket.
244 return
245- if upload.is_ppa:
246- if self.pocket != PackagePublishingPocket.RELEASE:
247- upload.reject(
248- "PPA uploads must be for the RELEASE pocket.")
249- elif (self.archive.purpose == ArchivePurpose.PARTNER and
250- self.pocket != PackagePublishingPocket.RELEASE and
251- self.pocket != PackagePublishingPocket.PROPOSED):
252- # Partner uploads can only go to the release or proposed
253- # pockets.
254- upload.reject(
255- "Partner uploads must be for the RELEASE or PROPOSED pocket.")
256- else:
257- # Uploads to the partner archive are allowed in any distroseries
258- # state.
259- # XXX julian 2005-05-29 bug=117557:
260- # This is a greasy hack until bug #117557 is fixed.
261- if (self.distroseries and
262- self.archive.purpose != ArchivePurpose.PARTNER and
263- not self.distroseries.canUploadToPocket(self.pocket)):
264- upload.reject(
265- "Not permitted to upload to the %s pocket in a "
266- "series in the '%s' state." % (
267- self.pocket.name, self.distroseries.status.name))
268
269 # reject PPA uploads by default
270 self.rejectPPAUploads(upload)
271
272=== modified file 'lib/lp/registry/interfaces/distribution.py'
273--- lib/lp/registry/interfaces/distribution.py 2009-11-13 16:24:01 +0000
274+++ lib/lp/registry/interfaces/distribution.py 2009-11-19 15:18:17 +0000
275@@ -16,6 +16,7 @@
276 'IDistributionMirrorMenuMarker',
277 'IDistributionPublic',
278 'IDistributionSet',
279+ 'NoPartnerArchive',
280 'NoSuchDistribution',
281 ]
282
283@@ -580,5 +581,14 @@
284 _message_prefix = "No such distribution"
285
286
287+class NoPartnerArchive(Exception):
288+ """Raised when a partner archive is needed, but none exists."""
289+
290+ def __init__(self, distribution):
291+ Exception.__init__(
292+ self, "Partner archive for distro '%s' not found"
293+ % (distribution.name,))
294+
295+
296 # Monkey patching to fix circular imports done in
297 # _schema_circular_imports.py
298
299=== modified file 'lib/lp/registry/interfaces/sourcepackage.py'
300--- lib/lp/registry/interfaces/sourcepackage.py 2009-11-15 23:23:12 +0000
301+++ lib/lp/registry/interfaces/sourcepackage.py 2009-11-19 15:18:17 +0000
302@@ -224,6 +224,20 @@
303 title=u'The component in which the package was last published.',
304 schema=IComponent, readonly=True, required=False)
305
306+ def get_default_archive(component=None):
307+ """Get the default archive of this package.
308+
309+ If 'component' is a partner component, then the default archive is the
310+ partner archive. Otherwise, the primary archive of the associated
311+ distribution.
312+
313+ :param component: The `IComponent` to base the default archive
314+ decision on. If None, defaults to the last published component.
315+ :raise NoPartnerArchive: If returning the partner archive is
316+ appropriate, but no partner archive exists.
317+ :return: `IArchive`.
318+ """
319+
320 def getLatestTranslationsUploads():
321 """Find latest Translations tarballs as produced by Soyuz.
322
323
324=== modified file 'lib/lp/registry/model/sourcepackage.py'
325--- lib/lp/registry/model/sourcepackage.py 2009-11-18 11:52:25 +0000
326+++ lib/lp/registry/model/sourcepackage.py 2009-11-19 15:18:17 +0000
327@@ -23,10 +23,11 @@
328 from canonical.database.sqlbase import flush_database_updates, sqlvalues
329 from canonical.lazr.utils import smartquote
330 from lp.code.model.branch import Branch
331+from lp.code.model.hasbranches import HasBranchesMixin, HasMergeProposalsMixin
332 from lp.bugs.model.bug import get_bug_tags_open_count
333 from lp.bugs.model.bugtarget import BugTargetBase
334 from lp.bugs.model.bugtask import BugTask
335-from lp.code.model.hasbranches import HasBranchesMixin, HasMergeProposalsMixin
336+from lp.soyuz.interfaces.archive import IArchiveSet, ArchivePurpose
337 from lp.soyuz.model.build import Build, BuildSet
338 from lp.soyuz.model.distributionsourcepackagerelease import (
339 DistributionSourcePackageRelease)
340@@ -52,6 +53,7 @@
341 from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
342 from lp.registry.interfaces.packaging import PackagingType
343 from lp.translations.interfaces.potemplate import IHasTranslationTemplates
344+from lp.registry.interfaces.distribution import NoPartnerArchive
345 from lp.registry.interfaces.pocket import PackagePublishingPocket
346 from lp.soyuz.interfaces.publishing import PackagePublishingStatus
347 from lp.soyuz.interfaces.queue import PackageUploadCustomFormat
348@@ -579,6 +581,21 @@
349 else:
350 return None
351
352+ def get_default_archive(self, component=None):
353+ """See `ISourcePackage`."""
354+ if component is None:
355+ component = self.latest_published_component
356+ distribution = self.distribution
357+ if component is not None and component.name == 'partner':
358+ archive = getUtility(IArchiveSet).getByDistroPurpose(
359+ distribution, ArchivePurpose.PARTNER)
360+ if archive is None:
361+ raise NoPartnerArchive(distribution)
362+ else:
363+ return archive
364+ else:
365+ return distribution.main_archive
366+
367 def getTranslationTemplates(self):
368 """See `IHasTranslationTemplates`."""
369 result = POTemplate.selectBy(
370
371=== modified file 'lib/lp/registry/tests/test_sourcepackage.py'
372--- lib/lp/registry/tests/test_sourcepackage.py 2009-08-28 06:39:38 +0000
373+++ lib/lp/registry/tests/test_sourcepackage.py 2009-11-19 15:18:17 +0000
374@@ -12,9 +12,13 @@
375 from zope.security.proxy import removeSecurityProxy
376
377 from canonical.launchpad.ftests import login_person, logout
378+from lp.registry.interfaces.distribution import NoPartnerArchive
379 from lp.registry.interfaces.distroseries import DistroSeriesStatus
380 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
381 from lp.registry.interfaces.pocket import PackagePublishingPocket
382+from lp.soyuz.interfaces.archive import ArchivePurpose
383+from lp.soyuz.interfaces.component import IComponentSet
384+from lp.soyuz.interfaces.publishing import PackagePublishingStatus
385 from lp.code.interfaces.seriessourcepackagebranch import (
386 IMakeOfficialBranchLinks)
387 from lp.testing import TestCaseWithFactory
388@@ -166,6 +170,59 @@
389 distribution_sourcepackage,
390 sourcepackage.distribution_sourcepackage)
391
392+ def test_default_archive(self):
393+ # The default archive of a source package is the primary archive of
394+ # its distribution.
395+ sourcepackage = self.factory.makeSourcePackage()
396+ distribution = sourcepackage.distribution
397+ self.assertEqual(
398+ distribution.main_archive, sourcepackage.get_default_archive())
399+
400+ def test_default_archive_partner(self):
401+ # If the source package was most recently uploaded to a partner
402+ # component, then its default archive is the partner archive for the
403+ # distribution.
404+ sourcepackage = self.factory.makeSourcePackage()
405+ partner = getUtility(IComponentSet)['partner']
406+ self.factory.makeSourcePackagePublishingHistory(
407+ sourcepackagename=sourcepackage.sourcepackagename,
408+ distroseries=sourcepackage.distroseries,
409+ component=partner,
410+ status=PackagePublishingStatus.PUBLISHED)
411+ distribution = sourcepackage.distribution
412+ expected_archive = self.factory.makeArchive(
413+ distribution=distribution,
414+ purpose=ArchivePurpose.PARTNER)
415+ self.assertEqual(
416+ expected_archive, sourcepackage.get_default_archive())
417+
418+ def test_default_archive_specified_component(self):
419+ # If the component is explicitly specified as partner, then we return
420+ # the partner archive.
421+ sourcepackage = self.factory.makeSourcePackage()
422+ partner = getUtility(IComponentSet)['partner']
423+ distribution = sourcepackage.distribution
424+ expected_archive = self.factory.makeArchive(
425+ distribution=distribution,
426+ purpose=ArchivePurpose.PARTNER)
427+ self.assertEqual(
428+ expected_archive,
429+ sourcepackage.get_default_archive(component=partner))
430+
431+ def test_default_archive_partner_doesnt_exist(self):
432+ # If the default archive ought to be the partner archive (because the
433+ # last published upload was to a partner component) then
434+ # default_archive will raise an exception.
435+ sourcepackage = self.factory.makeSourcePackage()
436+ partner = getUtility(IComponentSet)['partner']
437+ self.factory.makeSourcePackagePublishingHistory(
438+ sourcepackagename=sourcepackage.sourcepackagename,
439+ distroseries=sourcepackage.distroseries,
440+ component=partner,
441+ status=PackagePublishingStatus.PUBLISHED)
442+ self.assertRaises(
443+ NoPartnerArchive, sourcepackage.get_default_archive)
444+
445
446 class TestSourcePackageSecurity(TestCaseWithFactory):
447 """Tests for source package branch linking security."""
448
449=== modified file 'lib/lp/soyuz/interfaces/component.py'
450--- lib/lp/soyuz/interfaces/component.py 2009-06-25 04:06:00 +0000
451+++ lib/lp/soyuz/interfaces/component.py 2009-11-19 15:18:17 +0000
452@@ -10,7 +10,7 @@
453 __all__ = [
454 'IComponent',
455 'IComponentSelection',
456- 'IComponentSet'
457+ 'IComponentSet',
458 ]
459
460 from zope.interface import Interface, Attribute
461@@ -18,6 +18,7 @@
462
463 from canonical.launchpad import _
464
465+
466 class IComponent(Interface):
467 """Represents the Component table.
468
469@@ -54,4 +55,3 @@
470
471 def new(name):
472 """Create a new component."""
473-
474
475=== modified file 'lib/lp/testing/factory.py'
476--- lib/lp/testing/factory.py 2009-11-14 22:44:10 +0000
477+++ lib/lp/testing/factory.py 2009-11-19 15:18:17 +0000
478@@ -34,17 +34,25 @@
479
480 from canonical.autodecorate import AutoDecorate
481 from canonical.config import config
482+from canonical.database.constants import UTC_NOW
483 from lp.codehosting.codeimport.worker import CodeImportSourceDetails
484 from canonical.database.sqlbase import flush_database_updates
485 from lp.soyuz.adapters.packagelocation import PackageLocation
486+from lp.soyuz.interfaces.publishing import PackagePublishingStatus
487+from lp.soyuz.interfaces.section import ISectionSet
488 from canonical.launchpad.database.account import Account
489 from canonical.launchpad.database.emailaddress import EmailAddress
490 from canonical.launchpad.database.message import Message, MessageChunk
491 from lp.soyuz.model.processor import ProcessorFamilySet
492+from lp.soyuz.model.publishing import (
493+ SecureSourcePackagePublishingHistory,
494+ SourcePackagePublishingHistory,
495+ )
496 from canonical.launchpad.interfaces import IMasterStore
497 from canonical.launchpad.interfaces.account import (
498 AccountCreationRationale, AccountStatus, IAccountSet)
499-from lp.soyuz.interfaces.archive import IArchiveSet, ArchivePurpose
500+from lp.soyuz.interfaces.archive import (
501+ default_name_by_purpose, IArchiveSet, ArchivePurpose)
502 from lp.blueprints.interfaces.sprint import ISprintSet
503 from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
504 from lp.bugs.interfaces.bugtask import BugTaskStatus
505@@ -101,7 +109,8 @@
506 from lp.registry.interfaces.product import IProductSet, License
507 from lp.registry.interfaces.productseries import IProductSeries
508 from lp.registry.interfaces.project import IProjectSet
509-from lp.registry.interfaces.sourcepackage import ISourcePackage
510+from lp.registry.interfaces.sourcepackage import (
511+ ISourcePackage, SourcePackageUrgency)
512 from lp.registry.interfaces.sourcepackagename import (
513 ISourcePackageNameSet)
514 from lp.registry.interfaces.ssh import ISSHKeySet, SSHKeyType
515@@ -123,6 +132,7 @@
516 password that needs to happen on the master store and this is forced.
517 However, if we then read it back the default Store has to be used.
518 """
519+
520 def with_default_master_store(*args, **kw):
521 try:
522 store_selector = getUtility(IStoreSelector)
523@@ -137,9 +147,9 @@
524 return mergeFunctionMetadata(func, with_default_master_store)
525
526
527-# We use this for default paramters where None has a specific meaning. For
528-# example, makeBranch(product=None) means "make a junk branch".
529-# None, because None means "junk branch".
530+# We use this for default parameters where None has a specific meaning. For
531+# example, makeBranch(product=None) means "make a junk branch". None, because
532+# None means "junk branch".
533 _DEFAULT = object()
534
535
536@@ -176,10 +186,9 @@
537 :return: A hexadecimal string, with 'a'-'f' in lower case.
538 """
539 hex_number = '%x' % self.getUniqueInteger()
540- if digits is None:
541- return hex_number
542- else:
543- return hex_number.zfill(digits)
544+ if digits is not None:
545+ hex_number = hex_number.zfill(digits)
546+ return hex_number
547
548 def getUniqueString(self, prefix=None):
549 """Return a string unique to this factory instance.
550@@ -263,9 +272,10 @@
551
552 def makeGPGKey(self, owner):
553 """Give 'owner' a crappy GPG key for the purposes of testing."""
554+ key_id = self.getUniqueHexString(digits=8).upper()
555 return getUtility(IGPGKeySet).new(
556 owner.id,
557- keyid=self.getUniqueHexString(digits=8).upper(),
558+ keyid=key_id,
559 fingerprint='A' * 40,
560 keysize=self.getUniqueInteger(),
561 algorithm=GPGKeyAlgorithm.R,
562@@ -1467,7 +1477,7 @@
563 return getUtility(IComponentSet).ensure(name)
564
565 def makeArchive(self, distribution=None, owner=None, name=None,
566- purpose = None):
567+ purpose=None):
568 """Create and return a new arbitrary archive.
569
570 :param distribution: Supply IDistribution, defaults to a new one
571@@ -1481,10 +1491,13 @@
572 distribution = self.makeDistribution()
573 if owner is None:
574 owner = self.makePerson()
575- if name is None:
576- name = self.getUniqueString()
577 if purpose is None:
578 purpose = ArchivePurpose.PPA
579+ if name is None:
580+ try:
581+ name = default_name_by_purpose[purpose]
582+ except KeyError:
583+ name = self.getUniqueString()
584
585 # Making a distribution makes an archive, and there can be only one
586 # per distribution.
587@@ -1724,6 +1737,111 @@
588 distroseries = self.makeDistroRelease()
589 return distroseries.getSourcePackage(sourcepackagename)
590
591+ def getAnySourcePackageUrgency(self):
592+ return SourcePackageUrgency.MEDIUM
593+
594+ def makeSourcePackagePublishingHistory(self, sourcepackagename=None,
595+ distroseries=None, maintainer=None,
596+ creator=None, component=None,
597+ section=None, urgency=None,
598+ version=None, archive=None,
599+ builddepends=None,
600+ builddependsindep=None,
601+ build_conflicts=None,
602+ build_conflicts_indep=None,
603+ architecturehintlist='all',
604+ dateremoved=None,
605+ date_uploaded=UTC_NOW,
606+ pocket=None,
607+ status=None,
608+ scheduleddeletiondate=None,
609+ dsc_standards_version='3.6.2',
610+ dsc_format='1.0',
611+ dsc_binaries='foo-bin',
612+ ):
613+ if sourcepackagename is None:
614+ sourcepackagename = self.makeSourcePackageName()
615+ spn = sourcepackagename
616+
617+ if distroseries is None:
618+ distroseries = self.makeDistroRelease()
619+
620+ if archive is None:
621+ archive = self.makeArchive(
622+ distribution=distroseries.distribution,
623+ purpose=ArchivePurpose.PRIMARY)
624+
625+ if component is None:
626+ component = self.makeComponent()
627+
628+ if pocket is None:
629+ pocket = self.getAnyPocket()
630+
631+ if status is None:
632+ status = PackagePublishingStatus.PENDING
633+
634+ if urgency is None:
635+ urgency = self.getAnySourcePackageUrgency()
636+
637+ if section is None:
638+ section = self.getUniqueString('section')
639+ section = getUtility(ISectionSet).ensure(section)
640+
641+ if maintainer is None:
642+ maintainer = self.makePerson()
643+
644+ maintainer_email = '%s <%s>' % (
645+ maintainer.displayname,
646+ maintainer.preferredemail.email)
647+
648+ if creator is None:
649+ creator = self.makePerson()
650+
651+ if version is None:
652+ version = self.getUniqueString('version')
653+
654+ gpg_key = self.makeGPGKey(creator)
655+
656+ spr = distroseries.createUploadedSourcePackageRelease(
657+ sourcepackagename=spn,
658+ maintainer=maintainer,
659+ creator=creator,
660+ component=component,
661+ section=section,
662+ urgency=urgency,
663+ version=version,
664+ builddepends=builddepends,
665+ builddependsindep=builddependsindep,
666+ build_conflicts=build_conflicts,
667+ build_conflicts_indep=build_conflicts_indep,
668+ architecturehintlist=architecturehintlist,
669+ changelog_entry=None,
670+ dsc=None,
671+ copyright=self.getUniqueString(),
672+ dscsigningkey=gpg_key,
673+ dsc_maintainer_rfc822=maintainer_email,
674+ dsc_standards_version=dsc_standards_version,
675+ dsc_format=dsc_format,
676+ dsc_binaries=dsc_binaries,
677+ archive=archive, dateuploaded=date_uploaded)
678+
679+ sspph = SecureSourcePackagePublishingHistory(
680+ distroseries=distroseries,
681+ sourcepackagerelease=spr,
682+ component=spr.component,
683+ section=spr.section,
684+ status=status,
685+ datecreated=date_uploaded,
686+ dateremoved=dateremoved,
687+ scheduleddeletiondate=scheduleddeletiondate,
688+ pocket=pocket,
689+ embargo=False,
690+ archive=archive)
691+
692+ # SPPH and SSPPH IDs are the same, since they are SPPH is a SQLVIEW
693+ # of SSPPH and other useful attributes.
694+ return SourcePackagePublishingHistory.get(sspph.id)
695+
696 def makePackageset(self, name=None, description=None, owner=None,
697 packages=(), distroseries=None):
698 """Make an `IPackageset`."""