Merge lp:~cjwatson/launchpad/pocket-permissions into lp:launchpad

Proposed by Colin Watson on 2012-06-07
Status: Merged
Approved by: Benji York on 2012-06-11
Approved revision: no longer in the source branch.
Merged at revision: 15394
Proposed branch: lp:~cjwatson/launchpad/pocket-permissions
Merge into: lp:launchpad
Diff against target: 1874 lines (+637/-678)
14 files modified
lib/lp/_schema_circular_imports.py (+11/-0)
lib/lp/code/tests/test_branch.py (+1/-19)
lib/lp/soyuz/doc/archive.txt (+15/-546)
lib/lp/soyuz/doc/archivearch.txt (+0/-72)
lib/lp/soyuz/doc/archivepermission.txt (+2/-2)
lib/lp/soyuz/interfaces/archive.py (+77/-1)
lib/lp/soyuz/interfaces/archivepermission.py (+49/-1)
lib/lp/soyuz/model/archive.py (+40/-8)
lib/lp/soyuz/model/archivepermission.py (+47/-4)
lib/lp/soyuz/stories/webservice/xx-archive.txt (+4/-0)
lib/lp/soyuz/stories/webservice/xx-packageset.txt (+1/-0)
lib/lp/soyuz/tests/test_archive.py (+360/-15)
lib/lp/soyuz/tests/test_archivearch.py (+29/-7)
lib/lp/soyuz/tests/test_packageupload.py (+1/-3)
To merge this branch: bzr merge lp:~cjwatson/launchpad/pocket-permissions
Reviewer Review Type Date Requested Status
Benji York (community) code 2012-06-07 Approve on 2012-06-11
Review via email: mp+109192@code.launchpad.net

Commit message

Allow creating upload permissions for pockets.

Description of the change

== Summary ==

Bug 914779: we'd like to have per-pocket upload permissions.

== Proposed fix ==

In https://code.launchpad.net/~cjwatson/launchpad/db-pocket-permissions/+merge/106091, I added a database column to support this work, which has been deployed. This adds the (I think) fairly obvious set of archive methods to create, delete, and query archive permissions for pockets.

== LOC Rationale ==

I hate doctests, so I went through archive.txt and archivearch.txt and variously deleted tests that duplicated existing unit tests and converted tests to unit tests when they weren't already covered there. This was enough to bring me down to -46.

== Tests ==

bin/test -vvct soyuz.tests.test_archive

== Demo and Q/A ==

On dogfood, create a pocket upload permission (say, for -backports) using Archive.newPocketUploader for a user who doesn't otherwise have upload permission, and try to upload something.

== Lint ==

Just two false positives due to pocketlint not understanding property setters:

./lib/lp/soyuz/model/archive.py
     380: redefinition of function 'suppress_subscription_notifications' from line 371
     396: redefinition of function 'private' from line 392

To post a comment you must log in.
Benji York (benji) wrote :

This branch looks good. I can't see anything worth changing.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/_schema_circular_imports.py'
2--- lib/lp/_schema_circular_imports.py 2012-06-10 09:06:08 +0000
3+++ lib/lp/_schema_circular_imports.py 2012-06-12 00:00:37 +0000
4@@ -397,9 +397,14 @@
5 IArchive, 'getQueueAdminsForComponent', IArchivePermission)
6 patch_collection_return_type(
7 IArchive, 'getComponentsForQueueAdmin', IArchivePermission)
8+patch_collection_return_type(
9+ IArchive, 'getPocketsForUploader', IArchivePermission)
10+patch_collection_return_type(
11+ IArchive, 'getUploadersForPocket', IArchivePermission)
12 patch_entry_return_type(IArchive, 'newPackageUploader', IArchivePermission)
13 patch_entry_return_type(IArchive, 'newPackagesetUploader', IArchivePermission)
14 patch_entry_return_type(IArchive, 'newComponentUploader', IArchivePermission)
15+patch_entry_return_type(IArchive, 'newPocketUploader', IArchivePermission)
16 patch_entry_return_type(IArchive, 'newQueueAdmin', IArchivePermission)
17 patch_plain_parameter_type(IArchive, 'syncSources', 'from_archive', IArchive)
18 patch_plain_parameter_type(IArchive, 'syncSource', 'from_archive', IArchive)
19@@ -433,6 +438,12 @@
20 IArchive, '_checkUpload', 'distroseries', IDistroSeries)
21 patch_choice_parameter_type(
22 IArchive, '_checkUpload', 'pocket', PackagePublishingPocket)
23+patch_choice_parameter_type(
24+ IArchive, 'getUploadersForPocket', 'pocket', PackagePublishingPocket)
25+patch_choice_parameter_type(
26+ IArchive, 'newPocketUploader', 'pocket', PackagePublishingPocket)
27+patch_choice_parameter_type(
28+ IArchive, 'deletePocketUploader', 'pocket', PackagePublishingPocket)
29 patch_plain_parameter_type(
30 IArchive, 'newPackagesetUploader', 'packageset', IPackageset)
31 patch_plain_parameter_type(
32
33=== modified file 'lib/lp/code/tests/test_branch.py'
34--- lib/lp/code/tests/test_branch.py 2012-05-31 03:54:13 +0000
35+++ lib/lp/code/tests/test_branch.py 2012-06-12 00:00:37 +0000
36@@ -274,25 +274,7 @@
37 None,
38 archive.verifyUpload(
39 person, spn, component, distroseries,
40- strict_component))
41-
42- def assertCannotUpload(
43- self, reason, person, spn, archive, component, distroseries=None):
44- """Assert that 'person' cannot upload to the archive.
45-
46- :param reason: The expected reason for not being able to upload. A
47- string.
48- :param person: The person trying to upload.
49- :param spn: The `ISourcePackageName` being uploaded to. None if the
50- package does not yet exist.
51- :param archive: The `IArchive` being uploaded to.
52- :param component: The IComponent to which the package belongs.
53- """
54- if distroseries is None:
55- distroseries = archive.distribution.currentseries
56- exception = archive.verifyUpload(
57- person, spn, component, distroseries)
58- self.assertEqual(reason, str(exception))
59+ strict_component=strict_component))
60
61 def test_package_upload_permissions_grant_branch_edit(self):
62 # If you can upload to the package, then you are also allowed to write
63
64=== modified file 'lib/lp/soyuz/doc/archive.txt'
65--- lib/lp/soyuz/doc/archive.txt 2012-05-23 05:42:24 +0000
66+++ lib/lp/soyuz/doc/archive.txt 2012-06-12 00:00:37 +0000
67@@ -64,23 +64,16 @@
68 >>> cprov_archive.failed_count
69 1
70
71-relative_build_score and external_dependencies are properties that can be set
72-only by LP admins and read by anyone.
73-
74-relative_build_score is a signed integer that represents a delta to all the
75-build scores for builds done in the archive. The default value is zero:
76-
77- >>> cprov_archive.relative_build_score
78- 0
79-
80-Amending it as an unprivileged user results in failure:
81-
82- >>> cprov_archive.relative_build_score = 100
83- Traceback (most recent call last):
84- ...
85- Unauthorized: (..., 'relative_build_score', 'launchpad.Moderate')
86-
87-external_dependencies is a text field that should contain a comma-separated
88+ >>> cprov_private_ppa = factory.makeArchive(
89+ ... owner=cprov, name='myprivateppa',
90+ ... distribution=cprov_archive.distribution)
91+ >>> login("foo.bar@canonical.com")
92+ >>> cprov_private_ppa.buildd_secret = 'really secret'
93+ >>> cprov_private_ppa.private = True
94+ >>> login(ANONYMOUS)
95+
96+external_dependencies is a property that can be set only by LP admins and
97+read by anyone. It is a text field that should contain a comma-separated
98 list of sources.list entries in the format:
99 deb http[s]://[user:pass@]<host>[/path] %(series)s[-pocket] [components]
100 where the series variable is replaced with the series name of the context
101@@ -97,69 +90,11 @@
102 ...
103 Unauthorized: (..., 'external_dependencies', 'launchpad.Commercial')
104
105-As a Launchpad admin, setting these properties will work.
106+As a Launchpad admin, setting this property will work.
107
108 >>> login("admin@canonical.com")
109- >>> cprov_archive.relative_build_score = 100
110 >>> cprov_archive.external_dependencies = "deb http://foo hardy bar"
111
112-The buildd_secret is used by the slave scanner when generating a
113-sources.list entry for the builder to access a private archive. It is
114-essentially the password to the archive for the builder.
115-
116-It can only be set by users with launchpad.Commercial permission in the
117-archive, i.e. an admin or a commercial admin. We create a new PPA for
118-cprov here as changing privacy of a PPA is not allowed when sources have
119-already been published.
120-
121- >>> cprov_private_ppa = factory.makeArchive(
122- ... owner=cprov, name='myprivateppa',
123- ... distribution=cprov_archive.distribution)
124- >>> login(ANONYMOUS)
125- >>> cprov_private_ppa.buildd_secret = 'boing'
126- Traceback (most recent call last):
127- ...
128- Unauthorized: (..., 'buildd_secret', 'launchpad.Commercial')
129-
130-Commercial Member, a commercial admin but not an admin, can set
131-'buildd_secret'.
132-
133- >>> login("commercial-member@canonical.com")
134- >>> cprov_private_ppa.buildd_secret = 'not so secret at all'
135-
136-Foo Bar, an admin, can set 'buildd_secret'.
137-
138- >>> login("foo.bar@canonical.com")
139- >>> cprov_archive.buildd_secret = 'not so secret'
140-
141-In a public PPA, 'buildd_secret' still visible to anyone.
142-
143- >>> login(ANONYMOUS)
144- >>> print cprov_archive.private
145- False
146-
147- >>> print cprov_archive.buildd_secret
148- not so secret
149-
150-Once made private, 'buildd_secret' content can only be read by users with
151-'launchpad.View' in the archive.
152-
153- >>> login("foo.bar@canonical.com")
154- >>> cprov_private_ppa.buildd_secret = 'really secret'
155- >>> cprov_private_ppa.private = True
156-
157- >>> login(ANONYMOUS)
158- >>> print cprov_private_ppa.buildd_secret
159- Traceback (most recent call last):
160- ...
161- Unauthorized: (..., 'buildd_secret', 'launchpad.View')
162-
163-Celso can read 'buildd_secret' contents for his PPA.
164-
165- >>> login('celso.providelo@canonical.com')
166- >>> print cprov_private_ppa.buildd_secret
167- really secret
168-
169 Useful properties:
170
171 >>> print cprov_archive.archive_url
172@@ -543,160 +478,6 @@
173 >>> status_lookup.count()
174 2
175
176-getBuildCounters
177-----------------
178-
179-IArchive.getBuildCounters() allows callsites to quickly present
180-the number of builds in a pre-defined status for a given IArchive.
181-
182- >>> def print_build_counters(build_counters):
183- ... sorted_keys = sorted(build_counters)
184- ... for key in sorted_keys:
185- ... print key, build_counters[key]
186-
187-Build counters for Celso's PPA.
188-
189- >>> print_build_counters(cprov_archive.getBuildCounters())
190- failed 1
191- pending 0
192- succeeded 3
193- superseded 0
194- total 4
195-
196-Build counters for ubuntu primary archive.
197-
198- >>> print_build_counters(ubuntu.main_archive.getBuildCounters())
199- failed 5
200- pending 2
201- succeeded 8
202- superseded 3
203- total 18
204-
205-An option argument can be used to exclude any builds that have the status
206-`NEEDSBUILD`:
207-
208- >>> print_build_counters(
209- ... ubuntu.main_archive.getBuildCounters(include_needsbuild=False))
210- failed 5
211- pending 1
212- succeeded 8
213- superseded 3
214- total 17
215-
216-
217-getBuildSummariesForSourceIds()
218--------------------------------
219-
220-IArchive.getBuildSummariesForSourceIds() allows callsites to get an update
221-on the build statuses for a set of source publishing record ids. This
222-is useful for dynamically updating a page which displays a small batch of
223-source packages, such as the PPA/Archive pages.
224-
225-Create a small function for displaying the results:
226-
227- >>> def print_build_summary(summary):
228- ... print "%s\n%s\nRelevant builds:\n%s" % (
229- ... summary['status'].title,
230- ... summary['status'].description,
231- ... "\n".join(
232- ... " - %s" % build.title for build in summary['builds'])
233- ... )
234-
235- >>> def print_build_summaries(summaries):
236- ... for source_id, summary in sorted(summaries.items()):
237- ... print "Source ID: %s" % source_id
238- ... print_build_summary(summary)
239-
240-Now print the build summaries for firefox and foo_bar:
241-
242- >>> firefox_source = ubuntu.getSourcePackage('mozilla-firefox')
243- >>> firefox_source_pub = firefox_source.publishing_history[0]
244- >>> foobar = ubuntu.getSourcePackage('foobar')
245- >>> foo_pub = foobar.publishing_history[0]
246-
247- >>> build_summaries = \
248- ... ubuntu.main_archive.getBuildSummariesForSourceIds(
249- ... [firefox_source_pub.id, foo_pub.id])
250- >>> print_build_summaries(build_summaries)
251- Source ID: 18
252- FULLYBUILT
253- All builds were built successfully.
254- Relevant builds:
255- - hppa build of mozilla-firefox 0.9 in ubuntu warty RELEASE
256- - i386 build of mozilla-firefox 0.9 in ubuntu warty RELEASE
257- Source ID: 22
258- FAILEDTOBUILD
259- There were build failures.
260- Relevant builds:
261- - i386 build of foobar 1.0 in ubuntu warty RELEASE
262-
263-
264-No public access to IArchiveView methods
265-----------------------------------------
266-
267-Both the getBuildCounters() and getBuildSummariesForSourceIds() methods are
268-not publically available, but available only to users who have permission to
269-view the archive:
270-
271- # First we create some source/binary packages in cprov's private
272- # PPA so that we'll have some results to view.
273- >>> login('admin@canonical.com')
274- >>> from lp.soyuz.tests.test_publishing import (
275- ... SoyuzTestPublisher)
276- >>> test_publisher = SoyuzTestPublisher()
277- >>> ignore = test_publisher.setUpDefaultDistroSeries(warty)
278- >>> test_publisher.addFakeChroots(warty)
279- >>> ignore = test_publisher.getPubBinaries(archive=cprov_private_ppa)
280-
281- # Grab some source IDs from the archive that we can use for calls to
282- # getBuildSummariesForSourceIds():
283- >>> source_ids = [cprov_private_ppa.getPublishedSources()[0].id]
284-
285-Then verify that an admin can see the counters and build summaries:
286-
287- >>> print_build_counters(cprov_private_ppa.getBuildCounters())
288- failed 0
289- ...
290- succeeded 1
291- ...
292- total 1
293- >>> print_build_summaries(cprov_private_ppa.getBuildSummariesForSourceIds(
294- ... source_ids))
295- Source ID:...
296- FULLYBUILT_PENDING
297- All builds were built successfully but have not yet been published.
298- Relevant builds:
299- - i386 build of foo 666 in ubuntu warty RELEASE
300-
301-Next veryify that cprov can still access the build counters:
302-
303- >>> login('celso.providelo@canonical.com')
304- >>> print_build_counters(cprov_private_ppa.getBuildCounters())
305- failed 0
306- ...
307- succeeded 1
308- ...
309- total 1
310- >>> print_build_summaries(cprov_private_ppa.getBuildSummariesForSourceIds(
311- ... source_ids))
312- Source ID:...
313- ...
314- - i386 build of foo 666 in ubuntu warty RELEASE
315-
316-But the public cannot:
317-
318- >>> login('no-priv@canonical.com')
319- >>> print_build_counters(cprov_private_ppa.getBuildCounters())
320- Traceback (most recent call last):
321- ...
322- Unauthorized: (..., 'getBuildCounters', 'launchpad.View')
323- >>> print_build_summaries(cprov_private_ppa.getBuildSummariesForSourceIds(
324- ... source_ids))
325- Traceback (most recent call last):
326- ...
327- Unauthorized: (..., 'getBuildSummariesForSourceIds', 'launchpad.View')
328-
329-
330 Package Counters
331 ----------------
332
333@@ -806,26 +587,6 @@
334 2
335
336
337-Getting an Archive's source-package releases
338---------------------------------------------
339-
340-The method getSourcePackageReleases() is provided to return the unique
341-source-package releases for the archive. By default, all releases will
342-be returned, but you can also ask for releases with builds in a certain
343-state.
344-
345- >>> from lp.buildmaster.enums import BuildStatus
346- >>> releases = cprov_archive.getSourcePackageReleases(
347- ... build_status=BuildStatus.FULLYBUILT)
348- >>> for release in releases:
349- ... print release.title
350- mozilla-firefox - 0.9
351- pmount - 0.1-1
352- iceweasel - 1.0
353-
354-For further details see the `TestGetSourcePackageReleases` unit-test.
355-
356-
357 Sources available for deletions
358 -------------------------------
359
360@@ -932,6 +693,7 @@
361
362 Or return build records in a specific status:
363
364+ >>> from lp.buildmaster.enums import BuildStatus
365 >>> cprov_archive.getBuildRecords(
366 ... build_state=BuildStatus.FULLYBUILT).count()
367 3
368@@ -1963,24 +1725,6 @@
369 ultimate-copy copy-owner1 False True
370
371
372-Getting publishing records across a set of Archives
373----------------------------------------------------
374-
375-The IArchiveSet utility provides a getPublicationsInArchives() method
376-that can be used to find the current publishing records for a source
377-package in the provided list of archives for a specific distribution.
378-
379- >>> pubs = archive_set.getPublicationsInArchives(
380- ... firefox_source_pub.sourcepackagerelease.sourcepackagename,
381- ... [ubuntu.main_archive],
382- ... firefox_source_pub.distroseries.distribution)
383- >>> for pub in pubs:
384- ... print "%s - %s in %s" % (
385- ... pub.source_package_name,
386- ... pub.source_package_version,
387- ... pub.archive.displayname)
388- mozilla-firefox - 0.9 in Primary Archive for Ubuntu Linux
389-
390 Archive Permission Checking
391 ---------------------------
392
393@@ -2257,6 +2001,9 @@
394 First we use the SoyuzTestPublisher to make some test publications in
395 hoary:
396
397+ >>> from lp.soyuz.tests.test_publishing import (
398+ ... SoyuzTestPublisher)
399+ >>> test_publisher = SoyuzTestPublisher()
400 >>> test_publisher.addFakeChroots(hoary)
401 >>> unused = test_publisher.setUpDefaultDistroSeries(hoary)
402 >>> discard = test_publisher.getPubSource(
403@@ -2482,281 +2229,3 @@
404 >>> copy = mark.archive.getPublishedSources(name="overridden").one()
405 >>> print copy.section.name
406 python
407-
408-
409-Publish flag
410-------------
411-
412-Every archive has a "publish" flag that governs whether it should be
413-published or not. Upon creation that flag is false for copy archives but
414-true for all other archive types.
415-
416- >>> uber = getUtility(IDistributionSet).new(
417- ... 'uberdistro', 'The uberdistro', 'The mother of all distros',
418- ... 'All you would want from a distro', 'zero', 'uberdistro.org',
419- ... mark, cprov, cprov)
420-
421-The primary archive for the √úberdistro was created by the
422-IDistributionSet.new() method. Let's check its publish flag.
423-
424- >>> uber_primary = getUtility(IArchiveSet).getByDistroPurpose(
425- ... uber, ArchivePurpose.PRIMARY)
426- >>> uber_primary.publish
427- True
428-
429- >>> uber_partner = getUtility(IArchiveSet).new(
430- ... owner=cprov, purpose=ArchivePurpose.PARTNER,
431- ... distribution=uber, name='uber-partner')
432- >>> uber_partner.publish
433- True
434-
435-The 'sandbox archive' is a PPA that was newly created above.
436-
437- >>> sandbox_archive.is_ppa
438- True
439- >>> sandbox_archive.publish
440- True
441-
442- >>> uber_copy = getUtility(IArchiveSet).new(
443- ... owner=cprov, purpose=ArchivePurpose.COPY,
444- ... distribution=uber, name='uber-copy')
445- >>> uber_copy.publish
446- False
447-
448-
449-The name uniqueness constraints for archives
450---------------------------------------------
451-
452-The names of archives other than PPAs must be unique for a given
453-distribution. Trying to create an archive with the same name and
454-distribution but with a different owner will fail.
455-
456- >>> copycat_archive = getUtility(IArchiveSet).new(
457- ... owner=mark, purpose=ArchivePurpose.COPY,
458- ... distribution=uber, name='uber-copy')
459- Traceback (most recent call last):
460- ...
461- AssertionError: archive 'uber-copy' exists ... in 'uberdistro'.
462-
463-The same constraint is enforced for other archive types e.g. for partner
464-archives.
465-
466- >>> copycat_archive = getUtility(IArchiveSet).new(
467- ... owner=mark, purpose=ArchivePurpose.PARTNER,
468- ... distribution=uber, name='uber-partner')
469- Traceback (most recent call last):
470- ...
471- AssertionError: archive 'uber-partner' exists ... in 'uberdistro'.
472-
473-The names of PPAs must be unique per owner and distribution.
474-
475- >>> print mark.archive.displayname
476- PPA for Mark Shuttleworth
477-
478- >>> print mark.archive.name
479- ppa
480-
481- >>> dup_ppa = getUtility(IArchiveSet).new(
482- ... owner=mark, purpose=ArchivePurpose.PPA,
483- ... distribution=ubuntu, name='ppa')
484- Traceback (most recent call last):
485- ...
486- AssertionError: Person 'mark' already has a PPA named 'ppa'.
487-
488-While multiple PPAs per user isn't yet fully suported we may create
489-other PPAs, but they won't affect the existing traversal from IPerson
490-to a single IArchive.
491-
492- >>> another_ppa = getUtility(IArchiveSet).new(
493- ... owner=mark, purpose=ArchivePurpose.PPA,
494- ... distribution=ubuntu, name='nightly')
495-
496- >>> print another_ppa.owner.displayname
497- Mark Shuttleworth
498-
499- >>> print another_ppa.name
500- nightly
501-
502-`IPerson.archive` is still pointing to the PPA named 'ppa'.
503-
504- >>> print mark.archive.displayname
505- PPA for Mark Shuttleworth
506-
507- >>> print mark.archive.name
508- ppa
509-
510-The ppas named differently than the default ('ppa') have a slightly
511-different displayname format, including their names.
512-
513- >>> print another_ppa.displayname
514- PPA named nightly for Mark Shuttleworth
515-
516-Additionally, archives, despite of their purpose, cannot have the same
517-name as their distribution.
518-
519- >>> boingolinux = factory.makeDistribution(name='boingolinux')
520-
521- >>> getUtility(IArchiveSet).new(
522- ... owner=mark, purpose=ArchivePurpose.PRIMARY,
523- ... distribution=boingolinux, name=boingolinux.name)
524- Traceback (most recent call last):
525- ...
526- AssertionError: Archives cannot have the same name as their
527- distribution.
528-
529-
530-Looking up named PPAs
531----------------------
532-
533-Additionally to the locked 'archive' property, `IPerson` also offers
534-`ppas` property and `getPPAByName` method.
535-
536-`IPerson.ppas` returns a list with all PPA owned by the context person
537-or team ordered by name.
538-
539- >>> for ppa in mark.ppas:
540- ... print ppa.name
541- nightly
542- ppa
543-
544-`IPerson.getPPAByName` allows call sites to look up PPAs owned by the
545-context person with a given name (exact match).
546-
547- >>> default_ppa = mark.getPPAByName('ppa')
548- >>> default_ppa == mark.archive
549- True
550-
551- >>> nightly_ppa = mark.getPPAByName('nightly')
552- >>> another_ppa == nightly_ppa
553- True
554-
555-When a suitable PPA couldn't be found, NoSuchPPA is raised.
556-
557- >>> print mark.getPPAByName('not-found')
558- Traceback (most recent call last):
559- ...
560- NoSuchPPA: No such ppa: 'not-found'.
561-
562-
563-Editable displayname
564---------------------
565-
566-If 'displayname' is omitted on archive created, a default form is
567-automatically used.
568- >>> new_ppa = getUtility(IArchiveSet).new(
569- ... owner=cprov, purpose=ArchivePurpose.PPA,
570- ... distribution=ubuntu, name='test-ppa')
571- >>> print new_ppa.displayname
572- PPA named test-ppa for Celso Providelo
573-
574-When provided 'displayname' is used as given.
575-
576- >>> new_copy = getUtility(IArchiveSet).new(
577- ... owner=cprov, purpose=ArchivePurpose.COPY,
578- ... displayname='Rock and roll with rebuilds!',
579- ... distribution=ubuntu, name='test-rebuild')
580- >>> print new_copy.displayname
581- Rock and roll with rebuilds!
582-
583-After archive creation, the 'displayname' can be edited by the archive
584-anyone with 'edit' permissions on the archive.
585-
586- >>> login("no-priv@canonical.com")
587- >>> new_ppa.displayname = 'No-way!'
588- Traceback (most recent call last):
589- ...
590- Unauthorized: (<Archive at ...>, 'displayname', 'launchpad.Edit')
591-
592- >>> login('celso.providelo@canonical.com')
593- >>> new_ppa.displayname = 'My testing packages for jaunty'
594-
595-
596-Signing-key propagation
597------------------------
598-
599-Signing keys are, by default, shared between PPAs owned by the same
600-user/team.
601-
602-Celso's default PPA currently has no signing-key.
603-
604- >>> print cprov.archive.signing_key
605- None
606-
607-When a named-ppa is created there is no key to be shared, this case
608-is worked out when generating new signing key. See archive-signing.txt
609-for more information.
610-
611- >>> no_key_ppa = getUtility(IArchiveSet).new(
612- ... owner=cprov, purpose=ArchivePurpose.PPA,
613- ... distribution=ubuntu, name='no-key')
614-
615- >>> print no_key_ppa.signing_key
616- None
617-
618-We will select the only available IGPGKey from the sampledata.
619-
620- >>> foo_bar = getUtility(IPersonSet).getByName('name16')
621- >>> [a_key] = foo_bar.gpg_keys
622- >>> print a_key.displayname
623- 1024D/12345678
624-
625-And use it as the Celso's default PPA signing key.
626-
627- >>> login('foo.bar@canonical.com')
628- >>> cprov.archive.signing_key = a_key
629- >>> login('celso.providelo@canonical.com')
630-
631-Now there is a signing-key to be propagated and a new named-ppa is
632-already created accordingly.
633-
634- >>> ppa_with_key = getUtility(IArchiveSet).new(
635- ... owner=cprov, purpose=ArchivePurpose.PPA,
636- ... distribution=ubuntu, name='has-key')
637-
638- >>> ppa_with_key.signing_key == cprov.archive.signing_key
639- True
640-
641-
642-Download counts
643----------------
644-
645-Counts of downloads per binary package release, day and country are kept
646-up to date by a log-processing script. Archives have a method to get the
647-total number of downloads for a particular binary package release.
648-
649- >>> login('mark@example.com')
650- >>> binaries = test_publisher.getPubBinaries(
651- ... architecturespecific=True)
652- >>> archive = binaries[0].archive
653- >>> binary0, binary1 = (b.binarypackagerelease for b in binaries)
654-
655-The new packages have no downloads yet.
656-
657- >>> print archive.getPackageDownloadTotal(binary0)
658- 0
659- >>> print archive.getPackageDownloadTotal(binary1)
660- 0
661-
662-We will fake some package downloads.
663-
664- >>> from datetime import date
665- >>> from lp.services.worlddata.interfaces.country import ICountrySet
666- >>> australia = getUtility(ICountrySet)['AU']
667- >>> uk = getUtility(ICountrySet)['GB']
668-
669- >>> archive.updatePackageDownloadCount(
670- ... binary0, date(2010, 2, 21), None, 10)
671- >>> archive.updatePackageDownloadCount(
672- ... binary0, date(2010, 2, 22), uk, 5)
673- >>> archive.updatePackageDownloadCount(
674- ... binary0, date(2010, 2, 22), australia, 4)
675-
676- >>> archive.updatePackageDownloadCount(
677- ... binary1, date(2010, 2, 21), australia, 2)
678- >>> archive.updatePackageDownloadCount(
679- ... binary1, date(2010, 2, 21), uk, 1)
680-
681- >>> print archive.getPackageDownloadTotal(binary0)
682- 19
683- >>> print archive.getPackageDownloadTotal(binary1)
684- 3
685
686=== removed file 'lib/lp/soyuz/doc/archivearch.txt'
687--- lib/lp/soyuz/doc/archivearch.txt 2010-08-23 16:51:11 +0000
688+++ lib/lp/soyuz/doc/archivearch.txt 1970-01-01 00:00:00 +0000
689@@ -1,72 +0,0 @@
690-The `ArchiveArch` table facilitates the association of archives and
691-processor families. This allows a user to specify (or limit) what
692-processors the source packages in a certain archives will be built
693-for.
694-
695- >>> from lp.soyuz.enums import ArchivePurpose
696- >>> rebuild_archive = factory.makeArchive(
697- ... purpose=ArchivePurpose.COPY, name='archivearch-test')
698-
699-The rebuild archive has no associated processor families yet.
700-
701- >>> from lp.soyuz.interfaces.archivearch import IArchiveArchSet
702- >>> aa_set = getUtility(IArchiveArchSet)
703- >>> rset = aa_set.getByArchive(rebuild_archive)
704- >>> print rset.count()
705- 0
706-
707-The utility allows us to associate archives with processor families
708-and we'll tie the rebuild archive to the 'amd64' processor family.
709-
710- # Retrieve the 'amd64' and 'x86' processor families available
711- # in the sampledata.
712- >>> from lp.soyuz.interfaces.processor import IProcessorFamilySet
713- >>> amd64 = getUtility(IProcessorFamilySet).getByName('amd64')
714- >>> x86 = getUtility(IProcessorFamilySet).getByName('x86')
715-
716- >>> ignore = aa_set.new(rebuild_archive, amd64)
717-
718-Now we have an association between the rebuild archive to the 'amd64'
719-processor family.
720-
721- >>> archive_arches = aa_set.getByArchive(rebuild_archive)
722- >>> archive_arches.count()
723- 1
724-
725- >>> [archive_arch] = list(archive_arches)
726- >>> print archive_arch.archive.name
727- archivearch-test
728-
729- >>> print archive_arch.processorfamily.name
730- amd64
731-
732-Let's add another association for 'x86' processor family.
733-
734- >>> ignore = aa_set.new(rebuild_archive, x86)
735-
736- >>> archive_arches = aa_set.getByArchive(rebuild_archive)
737- >>> print archive_arches.count()
738- 2
739-
740-The result follows the creation order, so the just-created
741-`ArchiveArch` comes last.
742-
743- >>> [old, x86_archive_arch] = list(archive_arches)
744- >>> print x86_archive_arch.archive.name
745- archivearch-test
746-
747- >>> print x86_archive_arch.processorfamily.name
748- x86
749-
750-Last but not least, we query for a specific association.
751-
752- >>> archive_arches = aa_set.getByArchive(rebuild_archive, amd64)
753- >>> archive_arches.count()
754- 1
755-
756- >>> [amd64_archive_arch] = list(archive_arches)
757- >>> print amd64_archive_arch.archive.name
758- archivearch-test
759-
760- >>> print amd64_archive_arch.processorfamily.name
761- amd64
762
763=== modified file 'lib/lp/soyuz/doc/archivepermission.txt'
764--- lib/lp/soyuz/doc/archivepermission.txt 2012-04-24 13:35:22 +0000
765+++ lib/lp/soyuz/doc/archivepermission.txt 2012-06-12 00:00:37 +0000
766@@ -112,8 +112,8 @@
767 ... ubuntu)
768 Traceback (most recent call last):
769 ...
770- AssertionError: 'item' ... is not an IComponent, IPackageset or an
771- ISourcePackageName
772+ AssertionError: 'item' ... is not an IComponent, IPackageset,
773+ ISourcePackageName or PackagePublishingPocket
774
775 IArchivePermissionSet also has some helpers to make it very easy to
776 check permissions.
777
778=== modified file 'lib/lp/soyuz/interfaces/archive.py'
779--- lib/lp/soyuz/interfaces/archive.py 2012-06-09 16:26:32 +0000
780+++ lib/lp/soyuz/interfaces/archive.py 2012-06-12 00:00:37 +0000
781@@ -626,7 +626,7 @@
782 """
783
784 def verifyUpload(person, sourcepackagename, component,
785- distroseries, strict_component=True):
786+ distroseries, strict_component=True, pocket=None):
787 """Can 'person' upload 'sourcepackagename' to this archive ?
788
789 :param person: The `IPerson` trying to upload to the package. Referred
790@@ -639,6 +639,8 @@
791 :param strict_component: True if access to the specific component for
792 the package is needed to upload to it. If False, then access to
793 any component will do.
794+ :param pocket: The `PackagePublishingPocket` being uploaded to. If
795+ None, then pocket permissions are not checked.
796 :return: CannotUploadToArchive if 'person' cannot upload to the
797 archive,
798 None otherwise.
799@@ -762,6 +764,21 @@
800 """
801
802 @operation_parameters(
803+ person=Reference(schema=IPerson))
804+ # Really IArchivePermission, set in _schema_circular_imports to avoid
805+ # circular import.
806+ @operation_returns_collection_of(Interface)
807+ @export_read_operation()
808+ @operation_for_version("devel")
809+ def getPocketsForUploader(person):
810+ """Return the pockets that 'person' can upload to this archive.
811+
812+ :param person: An `IPerson` wishing to upload to an archive.
813+ :return: A `set` of `PackagePublishingPocket` items that 'person'
814+ can upload to.
815+ """
816+
817+ @operation_parameters(
818 sourcepackagename=TextLine(
819 title=_("Source package name"), required=True),
820 person=Reference(schema=IPerson))
821@@ -1166,6 +1183,24 @@
822 :return: A list of `IArchivePermission` records.
823 """
824
825+ @operation_parameters(
826+ pocket=Choice(
827+ title=_("Pocket"),
828+ # Really PackagePublishingPocket, circular import fixed below.
829+ vocabulary=DBEnumeratedType,
830+ required=True),
831+ )
832+ # Really IArchivePermission, set below to avoid circular import.
833+ @operation_returns_collection_of(Interface)
834+ @export_read_operation()
835+ @operation_for_version("devel")
836+ def getUploadersForPocket(pocket):
837+ """Return `IArchivePermission` records for the pocket's uploaders.
838+
839+ :param pocket: A `PackagePublishingPocket`.
840+ :return: A list of `IArchivePermission` records.
841+ """
842+
843 def hasAnyPermission(person):
844 """Whether or not this person has any permission at all on this
845 archive.
846@@ -1500,6 +1535,30 @@
847
848 @operation_parameters(
849 person=Reference(schema=IPerson),
850+ pocket=Choice(
851+ title=_("Pocket"),
852+ # Really PackagePublishingPocket, circular import fixed below.
853+ vocabulary=DBEnumeratedType,
854+ required=True),
855+ )
856+ # Really IArchivePermission, set below to avoid circular import.
857+ @export_factory_operation(Interface, [])
858+ @operation_for_version("devel")
859+ def newPocketUploader(person, pocket):
860+ """Add permission for a person to upload to a pocket.
861+
862+ :param person: An `IPerson` whom should be given permission.
863+ :param component: A `PackagePublishingPocket`.
864+ :return: An `IArchivePermission` which is the newly-created
865+ permission.
866+ :raises InvalidPocketForPartnerArchive: if this archive is a partner
867+ archive and the pocket is not RELEASE or PROPOSED.
868+ :raises InvalidPocketForPPA: if this archive is a PPA and the pocket
869+ is not RELEASE.
870+ """
871+
872+ @operation_parameters(
873+ person=Reference(schema=IPerson),
874 component_name=TextLine(
875 title=_("Component Name"), required=True))
876 # Really IArchivePermission, set below to avoid circular import.
877@@ -1566,6 +1625,23 @@
878
879 @operation_parameters(
880 person=Reference(schema=IPerson),
881+ pocket=Choice(
882+ title=_("Pocket"),
883+ # Really PackagePublishingPocket, circular import fixed below.
884+ vocabulary=DBEnumeratedType,
885+ required=True),
886+ )
887+ @export_write_operation()
888+ @operation_for_version("devel")
889+ def deletePocketUploader(person, pocket):
890+ """Revoke permission for the person to upload to the pocket.
891+
892+ :param person: An `IPerson` whose permission should be revoked.
893+ :param pocket: A `PackagePublishingPocket`.
894+ """
895+
896+ @operation_parameters(
897+ person=Reference(schema=IPerson),
898 component_name=TextLine(
899 title=_("Component Name"), required=True))
900 @export_write_operation()
901
902=== modified file 'lib/lp/soyuz/interfaces/archivepermission.py'
903--- lib/lp/soyuz/interfaces/archivepermission.py 2011-12-24 16:54:44 +0000
904+++ lib/lp/soyuz/interfaces/archivepermission.py 2012-06-12 00:00:37 +0000
905@@ -1,4 +1,4 @@
906-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
907+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
908 # GNU Affero General Public License version 3 (see the file LICENSE).
909
910 # pylint: disable-msg=E0213
911@@ -31,6 +31,7 @@
912 )
913
914 from lp import _
915+from lp.registry.interfaces.pocket import PackagePublishingPocket
916 from lp.registry.interfaces.sourcepackagename import ISourcePackageName
917 from lp.services.fields import PublicPersonChoice
918 from lp.soyuz.enums import ArchivePermissionType
919@@ -117,6 +118,13 @@
920 "package set."),
921 required=True))
922
923+ pocket = exported(
924+ Choice(
925+ title=_("Pocket"),
926+ description=_("The pocket that this permission is for."),
927+ vocabulary=PackagePublishingPocket,
928+ required=True))
929+
930
931 class IArchiveUploader(IArchivePermission):
932 """Marker interface for URL traversal of uploader permissions."""
933@@ -305,6 +313,27 @@
934 authorized to upload to the named source package set.
935 """
936
937+ def uploadersForPocket(archive, pocket):
938+ """The `ArchivePermission` records for authorised pocket uploaders.
939+
940+ :param archive: The context `IArchive` for the permission check.
941+ :param pocket: A `PackagePublishingPocket`.
942+
943+ :return: `ArchivePermission` records for all the uploaders who
944+ are authorised for the supplied pocket.
945+ """
946+
947+ def pocketsForUploader(archive, person):
948+ """The `ArchivePermission` records for the person's upload pockets.
949+
950+ :param archive: The context `IArchive` for the permission check.
951+ :param person: An `IPerson` for whom you want to find out which
952+ pockets he has access to.
953+
954+ :return: `ArchivePermission` records for all the pockets that
955+ 'person' is allowed to upload to.
956+ """
957+
958 def queueAdminsForComponent(archive, component):
959 """The `ArchivePermission` records for authorised queue admins.
960
961@@ -370,6 +399,17 @@
962 already exists.
963 """
964
965+ def newPocketUploader(archive, person, pocket):
966+ """Create and return a new `ArchivePermission` for an uploader.
967+
968+ :param archive: The context `IArchive` for the permission check.
969+ :param person: An `IPerson` for whom you want to add permission.
970+ :param component: A `PackagePublishingPocket`.
971+
972+ :return: The new `ArchivePermission`, or the existing one if it
973+ already exists.
974+ """
975+
976 def newQueueAdmin(archive, person, component):
977 """Create and return a new `ArchivePermission` for a queue admin.
978
979@@ -411,6 +451,14 @@
980 :param component: An `IComponent` or a string package name.
981 """
982
983+ def deletePocketUploader(archive, person, pocket):
984+ """Revoke upload permissions for a person.
985+
986+ :param archive: The context `IArchive` for the permission check.
987+ :param person: An `IPerson` for whom you want to revoke permission.
988+ :param pocket: A `PackagePublishingPocket`.
989+ """
990+
991 def deleteQueueAdmin(archive, person, component):
992 """Revoke queue admin permissions for a person.
993
994
995=== modified file 'lib/lp/soyuz/model/archive.py'
996--- lib/lp/soyuz/model/archive.py 2012-06-10 13:15:28 +0000
997+++ lib/lp/soyuz/model/archive.py 2012-06-12 00:00:37 +0000
998@@ -1124,6 +1124,11 @@
999 permission_set = getUtility(IArchivePermissionSet)
1000 return permission_set.uploadersForComponent(self, component_name)
1001
1002+ def getUploadersForPocket(self, pocket):
1003+ """See `IArchive`."""
1004+ permission_set = getUtility(IArchivePermissionSet)
1005+ return permission_set.uploadersForPocket(self, pocket)
1006+
1007 def getQueueAdminsForComponent(self, component_name):
1008 """See `IArchive`."""
1009 permission_set = getUtility(IArchivePermissionSet)
1010@@ -1232,7 +1237,7 @@
1011 source_ids,
1012 archive=self)
1013
1014- def checkArchivePermission(self, user, component_or_package=None):
1015+ def checkArchivePermission(self, user, item=None):
1016 """See `IArchive`."""
1017 # PPA access is immediately granted if the user is in the PPA
1018 # team.
1019@@ -1247,7 +1252,7 @@
1020 # interface will no longer require them because we can
1021 # then relax the database constraint on
1022 # ArchivePermission.
1023- component_or_package = self.default_component
1024+ item = self.default_component
1025
1026 # Flatly refuse uploads to copy archives, at least for now.
1027 if self.is_copy:
1028@@ -1255,8 +1260,7 @@
1029
1030 # Otherwise any archive, including PPAs, uses the standard
1031 # ArchivePermission entries.
1032- return self._authenticate(
1033- user, component_or_package, ArchivePermissionType.UPLOAD)
1034+ return self._authenticate(user, item, ArchivePermissionType.UPLOAD)
1035
1036 def canUploadSuiteSourcePackage(self, person, suitesourcepackage):
1037 """See `IArchive`."""
1038@@ -1319,10 +1323,10 @@
1039 return reason
1040 return self.verifyUpload(
1041 person, sourcepackagename, component, distroseries,
1042- strict_component)
1043+ strict_component=strict_component, pocket=pocket)
1044
1045 def verifyUpload(self, person, sourcepackagename, component,
1046- distroseries, strict_component=True):
1047+ distroseries, strict_component=True, pocket=None):
1048 """See `IArchive`."""
1049 if not self.enabled:
1050 return ArchiveDisabled(self.displayname)
1051@@ -1334,6 +1338,11 @@
1052 else:
1053 return None
1054
1055+ # Users with pocket upload permissions may upload to anything in the
1056+ # given pocket.
1057+ if pocket is not None and self.checkArchivePermission(person, pocket):
1058+ return None
1059+
1060 if sourcepackagename is not None:
1061 # Check whether user may upload because they hold a permission for
1062 # - the given source package directly
1063@@ -1364,9 +1373,9 @@
1064 return self._authenticate(
1065 user, component, ArchivePermissionType.QUEUE_ADMIN)
1066
1067- def _authenticate(self, user, component, permission):
1068+ def _authenticate(self, user, item, permission):
1069 """Private helper method to check permissions."""
1070- permissions = self.getPermissions(user, component, permission)
1071+ permissions = self.getPermissions(user, item, permission)
1072 return bool(permissions)
1073
1074 def newPackageUploader(self, person, source_package_name):
1075@@ -1400,6 +1409,19 @@
1076 return permission_set.newComponentUploader(
1077 self, person, component_name)
1078
1079+ def newPocketUploader(self, person, pocket):
1080+ if self.is_partner:
1081+ if pocket not in (
1082+ PackagePublishingPocket.RELEASE,
1083+ PackagePublishingPocket.PROPOSED):
1084+ raise InvalidPocketForPartnerArchive()
1085+ elif self.is_ppa:
1086+ if pocket != PackagePublishingPocket.RELEASE:
1087+ raise InvalidPocketForPPA()
1088+
1089+ permission_set = getUtility(IArchivePermissionSet)
1090+ return permission_set.newPocketUploader(self, person, pocket)
1091+
1092 def newQueueAdmin(self, person, component_name):
1093 """See `IArchive`."""
1094 permission_set = getUtility(IArchivePermissionSet)
1095@@ -1417,6 +1439,11 @@
1096 return permission_set.deleteComponentUploader(
1097 self, person, component_name)
1098
1099+ def deletePocketUploader(self, person, pocket):
1100+ """See `IArchive`."""
1101+ permission_set = getUtility(IArchivePermissionSet)
1102+ return permission_set.deletePocketUploader(self, person, pocket)
1103+
1104 def deleteQueueAdmin(self, person, component_name):
1105 """See `IArchive`."""
1106 permission_set = getUtility(IArchivePermissionSet)
1107@@ -1439,6 +1466,11 @@
1108 permission_set = getUtility(IArchivePermissionSet)
1109 return permission_set.componentsForUploader(self, person)
1110
1111+ def getPocketsForUploader(self, person):
1112+ """See `IArchive`."""
1113+ permission_set = getUtility(IArchivePermissionSet)
1114+ return permission_set.pocketsForUploader(self, person)
1115+
1116 def getPackagesetsForUploader(self, person):
1117 """See `IArchive`."""
1118 permission_set = getUtility(IArchivePermissionSet)
1119
1120=== modified file 'lib/lp/soyuz/model/archivepermission.py'
1121--- lib/lp/soyuz/model/archivepermission.py 2011-12-30 06:14:56 +0000
1122+++ lib/lp/soyuz/model/archivepermission.py 2012-06-12 00:00:37 +0000
1123@@ -1,4 +1,4 @@
1124-# Copyright 2009 Canonical Ltd. This software is licensed under the
1125+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
1126 # GNU Affero General Public License version 3 (see the file LICENSE).
1127
1128 """Database class for table ArchivePermission."""
1129@@ -10,6 +10,7 @@
1130 'ArchivePermissionSet',
1131 ]
1132
1133+from lazr.enum import DBItem
1134 from sqlobject import (
1135 BoolCol,
1136 ForeignKey,
1137@@ -25,9 +26,11 @@
1138 alsoProvides,
1139 implements,
1140 )
1141+from zope.security.proxy import isinstance as zope_isinstance
1142
1143 from lp.app.errors import NotFoundError
1144 from lp.registry.interfaces.distribution import IDistributionSet
1145+from lp.registry.interfaces.pocket import PackagePublishingPocket
1146 from lp.registry.interfaces.sourcepackagename import (
1147 ISourcePackageName,
1148 ISourcePackageNameSet,
1149@@ -101,6 +104,8 @@
1150
1151 explicit = BoolCol(dbName='explicit', notNull=True, default=False)
1152
1153+ pocket = EnumCol(dbName="pocket", schema=PackagePublishingPocket)
1154+
1155 def _init(self, *args, **kw):
1156 """Provide the right interface for URL traversal."""
1157 SQLBase._init(self, *args, **kw)
1158@@ -176,10 +181,13 @@
1159 clauses.append(
1160 "ArchivePermission.packageset = %s" % sqlvalues(item.id))
1161 prejoins.append("packageset")
1162+ elif (zope_isinstance(item, DBItem) and
1163+ item.enum.name == "PackagePublishingPocket"):
1164+ clauses.append("ArchivePermission.pocket = %s" % sqlvalues(item))
1165 else:
1166 raise AssertionError(
1167- "'item' %r is not an IComponent, IPackageset or an "
1168- "ISourcePackageName" % item)
1169+ "'item' %r is not an IComponent, IPackageset, "
1170+ "ISourcePackageName or PackagePublishingPocket" % item)
1171
1172 query = " AND ".join(clauses)
1173 auth = ArchivePermission.select(
1174@@ -235,7 +243,7 @@
1175 prejoins=["component"])
1176
1177 def componentsForUploader(self, archive, person):
1178- """See `IArchivePermissionSet`,"""
1179+ """See `IArchivePermissionSet`."""
1180 return self._componentsFor(
1181 archive, person, ArchivePermissionType.UPLOAD)
1182
1183@@ -277,6 +285,24 @@
1184 sourcepackagename=sourcepackagename)
1185 return results.prejoin(["sourcepackagename"])
1186
1187+ def pocketsForUploader(self, archive, person):
1188+ """See `IArchivePermissionSet`."""
1189+ return ArchivePermission.select("""
1190+ ArchivePermission.archive = %s AND
1191+ ArchivePermission.permission = %s AND
1192+ ArchivePermission.pocket IS NOT NULL AND
1193+ EXISTS (SELECT TeamParticipation.person
1194+ FROM TeamParticipation
1195+ WHERE TeamParticipation.person = %s AND
1196+ TeamParticipation.team = ArchivePermission.person)
1197+ """ % sqlvalues(archive, ArchivePermissionType.UPLOAD, person))
1198+
1199+ def uploadersForPocket(self, archive, pocket):
1200+ "See `IArchivePermissionSet`."""
1201+ return ArchivePermission.selectBy(
1202+ archive=archive, permission=ArchivePermissionType.UPLOAD,
1203+ pocket=pocket)
1204+
1205 def queueAdminsForComponent(self, archive, component):
1206 "See `IArchivePermissionSet`."""
1207 component = self._nameToComponent(component)
1208@@ -315,6 +341,17 @@
1209 archive=archive, person=person, component=component,
1210 permission=ArchivePermissionType.UPLOAD)
1211
1212+ def newPocketUploader(self, archive, person, pocket):
1213+ """See `IArchivePermissionSet`."""
1214+ existing = self.checkAuthenticated(
1215+ person, archive, ArchivePermissionType.UPLOAD, pocket)
1216+ try:
1217+ return existing[0]
1218+ except IndexError:
1219+ return ArchivePermission(
1220+ archive=archive, person=person, pocket=pocket,
1221+ permission=ArchivePermissionType.UPLOAD)
1222+
1223 def newQueueAdmin(self, archive, person, component):
1224 """See `IArchivePermissionSet`."""
1225 component = self._nameToComponent(component)
1226@@ -353,6 +390,12 @@
1227 permission=ArchivePermissionType.UPLOAD)
1228 self._remove_permission(permission)
1229
1230+ def deletePocketUploader(self, archive, person, pocket):
1231+ permission = ArchivePermission.selectOneBy(
1232+ archive=archive, person=person, pocket=pocket,
1233+ permission=ArchivePermissionType.UPLOAD)
1234+ self._remove_permission(permission)
1235+
1236 def deleteQueueAdmin(self, archive, person, component):
1237 """See `IArchivePermissionSet`."""
1238 component = self._nameToComponent(component)
1239
1240=== modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt'
1241--- lib/lp/soyuz/stories/webservice/xx-archive.txt 2012-05-27 18:53:35 +0000
1242+++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2012-06-12 00:00:37 +0000
1243@@ -168,6 +168,7 @@
1244 date_created: ...
1245 permission: u'Archive Upload Rights'
1246 person_link: u'http://.../~ubuntu-team'
1247+ pocket: None
1248 resource_type_link: ...
1249 self_link: u'http://.../ubuntu/+archive/primary/+upload/ubuntu-team?type=component&item=main'
1250 source_package_name: None
1251@@ -183,6 +184,7 @@
1252 date_created: ...
1253 permission: u'Archive Upload Rights'
1254 person_link: u'http://.../~carlos'
1255+ pocket: None
1256 resource_type_link: ...
1257 self_link:
1258 u'http://.../ubuntu/+archive/primary/+upload/carlos?type=packagename&item=mozilla-firefox'
1259@@ -199,6 +201,7 @@
1260 date_created: ...
1261 permission: u'Queue Administration Rights'
1262 person_link: u'http://.../~ubuntu-team'
1263+ pocket: None
1264 resource_type_link: ...
1265 self_link:
1266 u'http://.../ubuntu/+archive/primary/+queue-admin/ubuntu-team?type=component&item=main'
1267@@ -215,6 +218,7 @@
1268 date_created: ...
1269 permission: u'Queue Administration Rights'
1270 person_link: u'http://.../~name12'
1271+ pocket: None
1272 resource_type_link: ...
1273 self_link:
1274 u'http://.../ubuntu/+archive/primary/+queue-admin/name12?type=component&item=universe'
1275
1276=== modified file 'lib/lp/soyuz/stories/webservice/xx-packageset.txt'
1277--- lib/lp/soyuz/stories/webservice/xx-packageset.txt 2011-12-24 17:49:30 +0000
1278+++ lib/lp/soyuz/stories/webservice/xx-packageset.txt 2012-06-12 00:00:37 +0000
1279@@ -573,6 +573,7 @@
1280 package_set_name: u'firefox'
1281 permission: u'Archive Upload Rights'
1282 person_link: u'http://.../~name12'
1283+ pocket: None
1284 resource_type_link: ...
1285 self_link: u'http://.../+archive/primary/+upload/name12?type=packageset&item=firefox&series=hoary'
1286 source_package_name: None
1287
1288=== modified file 'lib/lp/soyuz/tests/test_archive.py'
1289--- lib/lp/soyuz/tests/test_archive.py 2012-06-10 09:06:08 +0000
1290+++ lib/lp/soyuz/tests/test_archive.py 2012-06-12 00:00:37 +0000
1291@@ -24,6 +24,7 @@
1292 from lp.app.errors import NotFoundError
1293 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
1294 from lp.buildmaster.enums import BuildStatus
1295+from lp.registry.interfaces.distribution import IDistributionSet
1296 from lp.registry.interfaces.person import (
1297 IPersonSet,
1298 TeamSubscriptionPolicy,
1299@@ -64,10 +65,12 @@
1300 InvalidPocketForPPA,
1301 NoRightsForArchive,
1302 NoRightsForComponent,
1303+ NoSuchPPA,
1304 VersionRequiresName,
1305 )
1306 from lp.soyuz.interfaces.archivearch import IArchiveArchSet
1307 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
1308+from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
1309 from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
1310 from lp.soyuz.interfaces.component import IComponentSet
1311 from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
1312@@ -92,6 +95,7 @@
1313 )
1314 from lp.testing.layers import (
1315 DatabaseFunctionalLayer,
1316+ LaunchpadFunctionalLayer,
1317 LaunchpadZopelessLayer,
1318 )
1319 from lp.testing.sampledata import COMMERCIAL_ADMIN_EMAIL
1320@@ -523,11 +527,11 @@
1321 # Somebody unrelated does not
1322 self.assertFalse(archive.checkArchivePermission(somebody))
1323
1324- def makeArchiveAndActiveDistroSeries(self, purpose=ArchivePurpose.PRIMARY):
1325+ def makeArchiveAndActiveDistroSeries(self, purpose=ArchivePurpose.PRIMARY,
1326+ status=SeriesStatus.DEVELOPMENT):
1327 archive = self.factory.makeArchive(purpose=purpose)
1328 distroseries = self.factory.makeDistroSeries(
1329- distribution=archive.distribution,
1330- status=SeriesStatus.DEVELOPMENT)
1331+ distribution=archive.distribution, status=status)
1332 return archive, distroseries
1333
1334 def makePersonWithComponentPermission(self, archive):
1335@@ -706,6 +710,21 @@
1336 self.assertCanUpload(
1337 archive, person, sourcepackagename, distroseries=distroseries)
1338
1339+ def makePersonWithPocketPermission(self, archive, pocket):
1340+ person = self.factory.makePerson()
1341+ removeSecurityProxy(archive).newPocketUploader(person, pocket)
1342+ return person
1343+
1344+ def test_checkUpload_pocket_permission(self):
1345+ archive, distroseries = self.makeArchiveAndActiveDistroSeries(
1346+ purpose=ArchivePurpose.PRIMARY, status=SeriesStatus.CURRENT)
1347+ sourcepackagename = self.factory.makeSourcePackageName()
1348+ pocket = PackagePublishingPocket.SECURITY
1349+ person = self.makePersonWithPocketPermission(archive, pocket)
1350+ self.assertCanUpload(
1351+ archive, person, sourcepackagename, distroseries=distroseries,
1352+ pocket=pocket)
1353+
1354 def make_person_with_packageset_permission(self, archive, distroseries,
1355 packages=()):
1356 packageset = self.factory.makePackageset(
1357@@ -801,11 +820,10 @@
1358
1359 def makePackageToUpload(self, distroseries):
1360 sourcepackagename = self.factory.makeSourcePackageName()
1361- suitesourcepackage = self.factory.makeSuiteSourcePackage(
1362+ return self.factory.makeSuiteSourcePackage(
1363 pocket=PackagePublishingPocket.RELEASE,
1364 sourcepackagename=sourcepackagename,
1365 distroseries=distroseries)
1366- return suitesourcepackage
1367
1368 def test_canUploadSuiteSourcePackage_invalid_pocket(self):
1369 # Test that canUploadSuiteSourcePackage calls checkUpload for
1370@@ -927,6 +945,7 @@
1371 self.archive.updatePackageDownloadCount(
1372 self.bpr_1, day, self.australia, 10)
1373 self.assertCount(10, self.archive, self.bpr_1, day, self.australia)
1374+ self.assertEqual(10, self.archive.getPackageDownloadTotal(self.bpr_1))
1375
1376 def test_reuses_existing_entry(self):
1377 # A second update will simply add to the count on the existing
1378@@ -937,6 +956,7 @@
1379 self.archive.updatePackageDownloadCount(
1380 self.bpr_1, day, self.australia, 3)
1381 self.assertCount(13, self.archive, self.bpr_1, day, self.australia)
1382+ self.assertEqual(13, self.archive.getPackageDownloadTotal(self.bpr_1))
1383
1384 def test_differentiates_between_countries(self):
1385 # A different country will cause a new entry to be created.
1386@@ -948,6 +968,7 @@
1387
1388 self.assertCount(10, self.archive, self.bpr_1, day, self.australia)
1389 self.assertCount(3, self.archive, self.bpr_1, day, self.new_zealand)
1390+ self.assertEqual(13, self.archive.getPackageDownloadTotal(self.bpr_1))
1391
1392 def test_country_can_be_none(self):
1393 # The country can be None, indicating that it is unknown.
1394@@ -959,6 +980,7 @@
1395
1396 self.assertCount(10, self.archive, self.bpr_1, day, self.australia)
1397 self.assertCount(3, self.archive, self.bpr_1, day, None)
1398+ self.assertEqual(13, self.archive.getPackageDownloadTotal(self.bpr_1))
1399
1400 def test_differentiates_between_days(self):
1401 # A different date will also cause a new entry to be created.
1402@@ -972,6 +994,7 @@
1403 self.assertCount(10, self.archive, self.bpr_1, day, self.australia)
1404 self.assertCount(
1405 3, self.archive, self.bpr_1, another_day, self.australia)
1406+ self.assertEqual(13, self.archive.getPackageDownloadTotal(self.bpr_1))
1407
1408 def test_differentiates_between_bprs(self):
1409 # And even a different package will create a new entry.
1410@@ -983,6 +1006,8 @@
1411
1412 self.assertCount(10, self.archive, self.bpr_1, day, self.australia)
1413 self.assertCount(3, self.archive, self.bpr_2, day, self.australia)
1414+ self.assertEqual(10, self.archive.getPackageDownloadTotal(self.bpr_1))
1415+ self.assertEqual(3, self.archive.getPackageDownloadTotal(self.bpr_2))
1416
1417
1418 class TestEnabledRestrictedBuilds(TestCaseWithFactory):
1419@@ -1015,11 +1040,9 @@
1420 distro.main_archive.require_virtualized = False
1421 # Restricting to all restricted architectures is fine
1422 distro.main_archive.enabled_restricted_families = [self.arm]
1423-
1424- def restrict():
1425- distro.main_archive.enabled_restricted_families = []
1426-
1427- self.assertRaises(CannotRestrictArchitectures, restrict)
1428+ self.assertRaises(
1429+ CannotRestrictArchitectures, setattr, distro.main_archive,
1430+ "enabled_restricted_families", [])
1431
1432 def test_main_virtualized_archive_can_be_restricted(self):
1433 # A main archive can be restricted to certain architectures
1434@@ -1071,15 +1094,63 @@
1435 self.assertContentEqual([], self.archive.enabled_restricted_families)
1436
1437
1438+class TestBuilddSecret(TestCaseWithFactory):
1439+ """Test buildd_secret security.
1440+
1441+ The buildd_secret is used by the slave scanner when generating a
1442+ sources.list entry for the builder to access a private archive. It is
1443+ essentially the password to the archive for the builder.
1444+ """
1445+
1446+ layer = DatabaseFunctionalLayer
1447+
1448+ def setUp(self):
1449+ super(TestBuilddSecret, self).setUp()
1450+ self.archive = self.factory.makeArchive()
1451+
1452+ def test_anonymous_cannot_set_buildd_secret(self):
1453+ login(ANONYMOUS)
1454+ e = self.assertRaises(
1455+ Unauthorized, setattr, self.archive, "buildd_secret", "boing")
1456+ self.assertEqual("launchpad.Commercial", e.args[2])
1457+
1458+ def test_commercial_admin_can_set_buildd_secret(self):
1459+ with celebrity_logged_in("commercial_admin"):
1460+ self.archive.buildd_secret = "not so secret at all"
1461+
1462+ def test_admin_can_set_buildd_secret(self):
1463+ with celebrity_logged_in("admin"):
1464+ self.archive.buildd_secret = "not so secret"
1465+
1466+ def test_public_archive_has_public_buildd_secret(self):
1467+ # In a public PPA, the buildd "secret" is visible to anyone.
1468+ with celebrity_logged_in("admin"):
1469+ self.archive.buildd_secret = "not so secret"
1470+ login(ANONYMOUS)
1471+ self.assertFalse(self.archive.private)
1472+ self.assertEqual("not so secret", self.archive.buildd_secret)
1473+
1474+ def test_private_archive_has_private_buildd_secret(self):
1475+ # In a private PPA, the buildd secret can only be read by users with
1476+ # launchpad.View on the archive.
1477+ with celebrity_logged_in("admin"):
1478+ self.archive.buildd_secret = "really secret"
1479+ self.archive.private = True
1480+ login(ANONYMOUS)
1481+ e = self.assertRaises(
1482+ Unauthorized, getattr, self.archive, "buildd_secret")
1483+ self.assertEqual("launchpad.View", e.args[2])
1484+ with person_logged_in(self.archive.owner):
1485+ self.assertEqual("really secret", self.archive.buildd_secret)
1486+
1487+
1488 class TestArchiveTokens(TestCaseWithFactory):
1489 layer = LaunchpadZopelessLayer
1490
1491 def setUp(self):
1492 super(TestArchiveTokens, self).setUp()
1493 owner = self.factory.makePerson()
1494- self.private_ppa = self.factory.makeArchive(owner=owner)
1495- self.private_ppa.buildd_secret = 'blah'
1496- self.private_ppa.private = True
1497+ self.private_ppa = self.factory.makeArchive(owner=owner, private=True)
1498 self.joe = self.factory.makePerson(name='joe')
1499 self.private_ppa.newSubscription(self.joe, owner)
1500
1501@@ -1631,8 +1702,7 @@
1502 # By default, a person cannot upload to any component of an archive.
1503 archive = self.factory.makeArchive()
1504 person = self.factory.makePerson()
1505- self.assertEqual(set(),
1506- set(archive.getComponentsForUploader(person)))
1507+ self.assertFalse(set(archive.getComponentsForUploader(person)))
1508
1509 def test_components_for_person_with_permissions(self):
1510 # If a person has been explicitly granted upload permissions to a
1511@@ -1649,6 +1719,30 @@
1512 set(archive.getComponentsForUploader(person)))
1513
1514
1515+class TestPockets(TestCaseWithFactory):
1516+
1517+ layer = DatabaseFunctionalLayer
1518+
1519+ def test_no_pockets_for_arbitrary_person(self):
1520+ # By default, a person cannot upload to any pocket of an archive.
1521+ archive = self.factory.makeArchive()
1522+ person = self.factory.makePerson()
1523+ self.assertEqual(set(), set(archive.getPocketsForUploader(person)))
1524+
1525+ def test_pockets_for_person_with_permissions(self):
1526+ # If a person has been explicitly granted upload permissions to a
1527+ # particular pocket, then those pockets are included in
1528+ # IArchive.getPocketsForUploader.
1529+ archive = self.factory.makeArchive()
1530+ person = self.factory.makePerson()
1531+ # Only admins or techboard members can add permissions normally. That
1532+ # restriction isn't relevant to this test.
1533+ ap_set = removeSecurityProxy(getUtility(IArchivePermissionSet))
1534+ ap = ap_set.newPocketUploader(
1535+ archive, person, PackagePublishingPocket.SECURITY)
1536+ self.assertEqual(set([ap]), set(archive.getPocketsForUploader(person)))
1537+
1538+
1539 class TestValidatePPA(TestCaseWithFactory):
1540
1541 layer = DatabaseFunctionalLayer
1542@@ -2489,3 +2583,254 @@
1543 with person_logged_in(archive2.owner):
1544 self.assertRaises(
1545 AssertionError, archive2.removeCopyNotification, job2.id)
1546+
1547+
1548+class TestPublishFlag(TestCaseWithFactory):
1549+
1550+ layer = DatabaseFunctionalLayer
1551+
1552+ def test_primary_archive_published_by_default(self):
1553+ distribution = self.factory.makeDistribution()
1554+ self.assertTrue(distribution.main_archive.publish)
1555+
1556+ def test_partner_archive_published_by_default(self):
1557+ partner = self.factory.makeArchive(purpose=ArchivePurpose.PARTNER)
1558+ self.assertTrue(partner.publish)
1559+
1560+ def test_ppa_published_by_default(self):
1561+ ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
1562+ self.assertTrue(ppa.publish)
1563+
1564+ def test_copy_archive_not_published_by_default(self):
1565+ copy = self.factory.makeArchive(purpose=ArchivePurpose.COPY)
1566+ self.assertFalse(copy.publish)
1567+
1568+
1569+class TestPPANaming(TestCaseWithFactory):
1570+
1571+ layer = DatabaseFunctionalLayer
1572+
1573+ def test_unique_copy_archive_name(self):
1574+ # Non-PPA archive names must be unique for a given distribution.
1575+ uber = self.factory.makeDistribution()
1576+ self.factory.makeArchive(
1577+ purpose=ArchivePurpose.COPY, distribution=uber, name="uber-copy")
1578+ self.assertRaises(
1579+ AssertionError, self.factory.makeArchive,
1580+ purpose=ArchivePurpose.COPY, distribution=uber, name="uber-copy")
1581+
1582+ def test_unique_partner_archive_name(self):
1583+ # Partner archive names must be unique for a given distribution.
1584+ uber = self.factory.makeDistribution()
1585+ self.factory.makeArchive(
1586+ purpose=ArchivePurpose.PARTNER, distribution=uber,
1587+ name="uber-partner")
1588+ self.assertRaises(
1589+ AssertionError, self.factory.makeArchive,
1590+ purpose=ArchivePurpose.PARTNER, distribution=uber,
1591+ name="uber-partner")
1592+
1593+ def test_unique_ppa_name_per_owner_and_distribution(self):
1594+ person = self.factory.makePerson()
1595+ self.factory.makeArchive(owner=person, name="ppa")
1596+ self.assertEqual(
1597+ "PPA for %s" % person.displayname, person.archive.displayname)
1598+ self.assertEqual("ppa", person.archive.name)
1599+ self.assertRaises(
1600+ AssertionError, self.factory.makeArchive, owner=person, name="ppa")
1601+
1602+ def test_default_archive(self):
1603+ # Creating multiple PPAs does not affect the existing traversal from
1604+ # IPerson to a single IArchive.
1605+ person = self.factory.makePerson()
1606+ ppa = self.factory.makeArchive(owner=person, name="ppa")
1607+ self.factory.makeArchive(owner=person, name="nightly")
1608+ self.assertEqual(ppa, person.archive)
1609+
1610+ def test_non_default_ppas_have_different_displayname(self):
1611+ person = self.factory.makePerson()
1612+ another_ppa = self.factory.makeArchive(owner=person, name="nightly")
1613+ self.assertEqual(
1614+ "PPA named nightly for %s" % person.displayname,
1615+ another_ppa.displayname)
1616+
1617+ def test_archives_cannot_have_same_name_as_distribution(self):
1618+ boingolinux = self.factory.makeDistribution(name="boingolinux")
1619+ self.assertRaises(
1620+ AssertionError, getUtility(IArchiveSet).new,
1621+ owner=self.factory.makePerson(), purpose=ArchivePurpose.PRIMARY,
1622+ distribution=boingolinux, name=boingolinux.name)
1623+
1624+
1625+class TestPPALookup(TestCaseWithFactory):
1626+
1627+ layer = DatabaseFunctionalLayer
1628+
1629+ def setUp(self):
1630+ super(TestPPALookup, self).setUp()
1631+ self.person = self.factory.makePerson()
1632+ self.factory.makeArchive(owner=self.person, name="ppa")
1633+ self.nightly = self.factory.makeArchive(
1634+ owner=self.person, name="nightly")
1635+
1636+ def test_ppas(self):
1637+ # IPerson.ppas returns all owned PPAs ordered by name.
1638+ self.assertEqual(
1639+ ["nightly", "ppa"], [ppa.name for ppa in self.person.ppas])
1640+
1641+ def test_getPPAByName(self):
1642+ default_ppa = self.person.getPPAByName("ppa")
1643+ self.assertEqual(self.person.archive, default_ppa)
1644+ nightly_ppa = self.person.getPPAByName("nightly")
1645+ self.assertEqual(self.nightly, nightly_ppa)
1646+
1647+ def test_NoSuchPPA(self):
1648+ self.assertRaises(NoSuchPPA, self.person.getPPAByName, "not-found")
1649+
1650+
1651+class TestDisplayName(TestCaseWithFactory):
1652+
1653+ layer = DatabaseFunctionalLayer
1654+
1655+ def test_default(self):
1656+ # If 'displayname' is omitted when creating the archive, there is a
1657+ # sensible default.
1658+ archive = self.factory.makeArchive(name="test-ppa")
1659+ self.assertEqual(
1660+ "PPA named test-ppa for %s" % archive.owner.displayname,
1661+ archive.displayname)
1662+
1663+ def test_provided(self):
1664+ # If 'displayname' is provided, it is used.
1665+ archive = self.factory.makeArchive(
1666+ purpose=ArchivePurpose.COPY,
1667+ displayname="Rock and roll with rebuilds!", name="test-rebuild")
1668+ self.assertEqual("Rock and roll with rebuilds!", archive.displayname)
1669+
1670+ def test_editable(self):
1671+ # Anyone with edit permission on the archive can change displayname.
1672+ archive = self.factory.makeArchive(name="test-ppa")
1673+ login("no-priv@canonical.com")
1674+ e = self.assertRaises(
1675+ Unauthorized, setattr, archive, "displayname", "No-way!")
1676+ self.assertEqual("launchpad.Edit", e.args[2])
1677+ with person_logged_in(archive.owner):
1678+ archive.displayname = "My testing packages"
1679+
1680+
1681+class TestSigningKeyPropagation(TestCaseWithFactory):
1682+ """Signing keys are shared between PPAs owned by the same person/team."""
1683+
1684+ layer = DatabaseFunctionalLayer
1685+
1686+ def test_ppa_created_with_no_signing_key(self):
1687+ ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
1688+ self.assertIsNone(ppa.signing_key)
1689+
1690+ def test_default_signing_key_propagated_to_new_ppa(self):
1691+ person = self.factory.makePerson()
1692+ ppa = self.factory.makeArchive(
1693+ owner=person, purpose=ArchivePurpose.PPA, name="ppa")
1694+ self.assertEqual(ppa, person.archive)
1695+ self.factory.makeGPGKey(person)
1696+ with celebrity_logged_in("admin"):
1697+ person.archive.signing_key = person.gpg_keys[0]
1698+ ppa_with_key = self.factory.makeArchive(
1699+ owner=person, purpose=ArchivePurpose.PPA)
1700+ self.assertEqual(person.gpg_keys[0], ppa_with_key.signing_key)
1701+
1702+
1703+class TestCountersAndSummaries(TestCaseWithFactory):
1704+
1705+ layer = LaunchpadFunctionalLayer
1706+
1707+ def assertDictEqual(self, one, two):
1708+ self.assertContentEqual(one.items(), two.items())
1709+
1710+ def test_cprov_build_counters_in_sampledata(self):
1711+ cprov_archive = getUtility(IPersonSet).getByName("cprov").archive
1712+ expected_counters = {
1713+ "failed": 1,
1714+ "pending": 0,
1715+ "succeeded": 3,
1716+ "superseded": 0,
1717+ "total": 4,
1718+ }
1719+ self.assertDictEqual(
1720+ expected_counters, cprov_archive.getBuildCounters())
1721+
1722+ def test_ubuntu_build_counters_in_sampledata(self):
1723+ ubuntu_archive = getUtility(IDistributionSet)["ubuntu"].main_archive
1724+ expected_counters = {
1725+ "failed": 5,
1726+ "pending": 2,
1727+ "succeeded": 8,
1728+ "superseded": 3,
1729+ "total": 18,
1730+ }
1731+ self.assertDictEqual(
1732+ expected_counters, ubuntu_archive.getBuildCounters())
1733+ # include_needsbuild=False excludes builds in status NEEDSBUILD.
1734+ expected_counters["pending"] -= 1
1735+ expected_counters["total"] -= 1
1736+ self.assertDictEqual(
1737+ expected_counters,
1738+ ubuntu_archive.getBuildCounters(include_needsbuild=False))
1739+
1740+ def assertBuildSummaryMatches(self, status, builds, summary):
1741+ self.assertEqual(status, summary["status"])
1742+ self.assertContentEqual(
1743+ builds, [build.title for build in summary["builds"]])
1744+
1745+ def test_build_summaries_in_sampledata(self):
1746+ ubuntu = getUtility(IDistributionSet)["ubuntu"]
1747+ firefox_source = ubuntu.getSourcePackage("mozilla-firefox")
1748+ firefox_source_pub = firefox_source.publishing_history[0]
1749+ foobar = ubuntu.getSourcePackage("foobar")
1750+ foobar_pub = foobar.publishing_history[0]
1751+ build_summaries = ubuntu.main_archive.getBuildSummariesForSourceIds(
1752+ [firefox_source_pub.id, foobar_pub.id])
1753+ self.assertEqual(2, len(build_summaries))
1754+ expected_firefox_builds = [
1755+ "hppa build of mozilla-firefox 0.9 in ubuntu warty RELEASE",
1756+ "i386 build of mozilla-firefox 0.9 in ubuntu warty RELEASE",
1757+ ]
1758+ self.assertBuildSummaryMatches(
1759+ BuildSetStatus.FULLYBUILT, expected_firefox_builds,
1760+ build_summaries[firefox_source_pub.id])
1761+ expected_foobar_builds = [
1762+ "i386 build of foobar 1.0 in ubuntu warty RELEASE",
1763+ ]
1764+ self.assertBuildSummaryMatches(
1765+ BuildSetStatus.FAILEDTOBUILD, expected_foobar_builds,
1766+ build_summaries[foobar_pub.id])
1767+
1768+ def test_private_archives_have_private_counters_and_summaries(self):
1769+ archive = self.factory.makeArchive()
1770+ distroseries = self.factory.makeDistroSeries(
1771+ distribution=archive.distribution)
1772+ with celebrity_logged_in("admin"):
1773+ archive.private = True
1774+ publisher = SoyuzTestPublisher()
1775+ publisher.setUpDefaultDistroSeries(distroseries)
1776+ publisher.addFakeChroots(distroseries)
1777+ publisher.getPubBinaries(archive=archive)
1778+ source_id = archive.getPublishedSources()[0].id
1779+
1780+ # An admin can see the counters and build summaries.
1781+ archive.getBuildCounters()["total"]
1782+ archive.getBuildSummariesForSourceIds([source_id])
1783+
1784+ # The archive owner can see the counters and build summaries.
1785+ with person_logged_in(archive.owner):
1786+ archive.getBuildCounters()["total"]
1787+ archive.getBuildSummariesForSourceIds([source_id])
1788+
1789+ # The public cannot.
1790+ login("no-priv@canonical.com")
1791+ e = self.assertRaises(
1792+ Unauthorized, getattr, archive, "getBuildCounters")
1793+ self.assertEqual("launchpad.View", e.args[2])
1794+ e = self.assertRaises(
1795+ Unauthorized, getattr, archive, "getBuildSummariesForSourceIds")
1796+ self.assertEqual("launchpad.View", e.args[2])
1797
1798=== modified file 'lib/lp/soyuz/tests/test_archivearch.py'
1799--- lib/lp/soyuz/tests/test_archivearch.py 2012-01-01 02:58:52 +0000
1800+++ lib/lp/soyuz/tests/test_archivearch.py 2012-06-12 00:00:37 +0000
1801@@ -1,4 +1,4 @@
1802-# Copyright 2010 Canonical Ltd. This software is licensed under the
1803+# Copyright 2010-2012 Canonical Ltd. This software is licensed under the
1804 # GNU Affero General Public License version 3 (see the file LICENSE).
1805
1806 """Test ArchiveArch features."""
1807@@ -49,7 +49,7 @@
1808 self.archive_arch_set.getRestrictedFamilies(self.ppa))
1809 results = dict(
1810 (row[0].name, row[1] is not None) for row in result_set)
1811- self.assertEquals(
1812+ self.assertEqual(
1813 {'arm': False, 'cell-proc': True, 'omap': False},
1814 results)
1815
1816@@ -62,7 +62,7 @@
1817 self.archive_arch_set.getRestrictedFamilies(self.ppa))
1818 results = dict(
1819 (row[0].name, row[1] is not None) for row in result_set)
1820- self.assertEquals(
1821+ self.assertEqual(
1822 {'arm': False, 'cell-proc': True, 'omap': False},
1823 results)
1824
1825@@ -70,8 +70,30 @@
1826 # Test ArchiveArchSet.getByArchive returns no other archives.
1827 self.archive_arch_set.new(self.ppa, self.cell_proc)
1828 self.archive_arch_set.new(self.ubuntu_archive, self.omap)
1829+ result_set = list(self.archive_arch_set.getByArchive(self.ppa))
1830+ self.assertEqual(1, len(result_set))
1831+ self.assertEqual(self.ppa, result_set[0].archive)
1832+ self.assertEqual(self.cell_proc, result_set[0].processorfamily)
1833+
1834+ def test_getByArchive_follows_creation_order(self):
1835+ # The result of ArchiveArchSet.getByArchive follows the order in
1836+ # which architecture associations were added.
1837+ self.archive_arch_set.new(self.ppa, self.cell_proc)
1838+ self.archive_arch_set.new(self.ppa, self.omap)
1839+ result_set = list(self.archive_arch_set.getByArchive(self.ppa))
1840+ self.assertEqual(2, len(result_set))
1841+ self.assertEqual(self.ppa, result_set[0].archive)
1842+ self.assertEqual(self.cell_proc, result_set[0].processorfamily)
1843+ self.assertEqual(self.ppa, result_set[1].archive)
1844+ self.assertEqual(self.omap, result_set[1].processorfamily)
1845+
1846+ def test_getByArchive_specific_architecture(self):
1847+ # ArchiveArchSet.getByArchive can query for a specific architecture
1848+ # association.
1849+ self.archive_arch_set.new(self.ppa, self.cell_proc)
1850+ self.archive_arch_set.new(self.ppa, self.omap)
1851 result_set = list(
1852- self.archive_arch_set.getByArchive(self.ppa))
1853- self.assertEquals(1, len(result_set))
1854- self.assertEquals(self.ppa, result_set[0].archive)
1855- self.assertEquals(self.cell_proc, result_set[0].processorfamily)
1856+ self.archive_arch_set.getByArchive(self.ppa, self.cell_proc))
1857+ self.assertEqual(1, len(result_set))
1858+ self.assertEqual(self.ppa, result_set[0].archive)
1859+ self.assertEqual(self.cell_proc, result_set[0].processorfamily)
1860
1861=== modified file 'lib/lp/soyuz/tests/test_packageupload.py'
1862--- lib/lp/soyuz/tests/test_packageupload.py 2012-05-30 08:50:50 +0000
1863+++ lib/lp/soyuz/tests/test_packageupload.py 2012-06-12 00:00:37 +0000
1864@@ -115,9 +115,7 @@
1865 self.test_publisher.prepareBreezyAutotest()
1866 ppa = self.factory.makeArchive(
1867 distribution=self.test_publisher.ubuntutest,
1868- purpose=ArchivePurpose.PPA)
1869- ppa.buildd_secret = 'x'
1870- ppa.private = True
1871+ purpose=ArchivePurpose.PPA, private=True)
1872
1873 changesfile_path = (
1874 'lib/lp/archiveuploader/tests/data/suite/'