Merge lp:~abentley/launchpad/ppa-api into lp:launchpad
- ppa-api
- Merge into devel
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 13204 | ||||||||
Proposed branch: | lp:~abentley/launchpad/ppa-api | ||||||||
Merge into: | lp:launchpad | ||||||||
Diff against target: |
1234 lines (+408/-254) 11 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+9/-1) lib/lp/soyuz/browser/archive.py (+10/-70) lib/lp/soyuz/browser/tests/archive-views.txt (+12/-9) lib/lp/soyuz/browser/tests/test_archive_webservice.py (+119/-2) lib/lp/soyuz/doc/archive-dependencies.txt (+10/-108) lib/lp/soyuz/doc/archive.txt (+4/-5) lib/lp/soyuz/interfaces/archive.py (+127/-46) lib/lp/soyuz/model/archive.py (+36/-6) lib/lp/soyuz/stories/webservice/xx-archive.txt (+4/-0) lib/lp/soyuz/tests/test_archive.py (+72/-6) lib/lp/testing/factory.py (+5/-1) |
||||||||
To merge this branch: | bzr merge lp:~abentley/launchpad/ppa-api | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Graham Binns (community) | code | Approve | |
Review via email: mp+63601@code.launchpad.net |
This proposal supersedes a proposal from 2011-06-01.
Commit message
Description of the change
Summary
=======
This code was approved and merged, but then rolled back because there was validation in the browser code that was missing from the model code. The changes from the previous version are here:
http://
Original summary: Fix bug #776444 and #776449 about missing APIs
Proposed change
===============
Export APIs
Implementation Details
=======
Implemented _addArchiveDepe
Also fixed the permissions of addArchiveDepen
Moved the validation code from the view to the model, so that the API respects it.
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/canonical
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
./lib/lp/
1: narrative uses a moin header.
9: narrative uses a moin header.
486: narrative uses a moin header.
529: narrative uses a moin header.
656: narrative uses a moin header.
1040: narrative uses a moin header.
1317: narrative uses a moin header.
1383: narrative uses a moin header.
./lib/lp/
1: narrative uses a moin header.
8: narrative uses a moin header.
30: narrative uses a moin header.
81: narrative uses a moin header.
156: narrative uses a moin header.
330: narrative uses a moin header.
449: narrative uses a moin header.
479: narrative uses a moin header.
./lib/lp/
20: want exceeds 78 characters.
87: want exceeds 78 characters.
113: source exceeds 78 characters.
120: narrative uses a moin header.
131: source exceeds 78 characters.
140: want exceeds 78 characters.
145: source exceeds 78 characters.
155: want exceeds 78 characters.
160: source exceeds 78 characters.
170: want exceeds 78 characters.
175: source exceeds 78 characters.
185: want exceeds 78 characters.
189: narrative uses a moin header.
272: want exceeds 78 characters.
333: want exceeds 78 characters.
341: narrative has trailing whitespace.
346: source has trailing whitespace.
374: source has trailing whitespace.
464: want exceeds 78 characters.
490: narrative uses a moin header.
497: source exceeds 78 characters.
506: source exceeds 78 characters.
514: source exceeds 78 characters.
520: narrative exceeds 78 characters.
523: source exceeds 78 characters.
530: narrative uses a moin header.
558: narrative uses a moin header.
587: narrative uses a moin header.
766: narrative uses a moin header.
768: narrative exceeds 78 characters.
787: source exceeds 78 characters.
797: narrative uses a moin header.
824: want exceeds 78 characters.
840: want exceeds 78 characters.
854: narrative uses a moin header.
943: narrative uses a moin header.
956: narrative uses a moin header.
1004: narrative uses a moin header.
Checking for conflicts and issues in changed files.
Gavin Panella (allenap) wrote : Posted in a previous version of this proposal | # |
Aaron Bentley (abentley) wrote : Posted in a previous version of this proposal | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 11-06-02 11:43 AM, Gavin Panella wrote:
> Review: Approve
> Looks good :)
>
>
> [1]
>
> + with ExpectedExcepti
> + ws_archive.
>
> Oh wow, that's neat.
Thanks. I added that to testtools myself. I like that it preserves the
normal way of writing function calls, so it's easier to read than
assertRaises.
> [2]
>
> +class InvalidExternal
> + """Tried to set external dependencies to an invalid value."""
> +
> + webservice_
> +
> + def __init__(self, errors):
> + error_msg = 'Invalid external dependencies:
> + Exception.
>
> Although it's a single line of inheritance, Exception is a new style
> class, so please use super() here.
Thanks. I guess I missed that when we switched to python 2.5.
Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk3
wWMAn2WOlUv7dDn
=tvir
-----END PGP SIGNATURE-----
Graham Binns (gmb) wrote : | # |
r=me on the latest changes.
Preview Diff
1 | === modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py' |
2 | --- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-06-09 08:07:52 +0000 |
3 | +++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-06-09 17:59:33 +0000 |
4 | @@ -1,4 +1,4 @@ |
5 | -# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
6 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
7 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | |
9 | """Update the interface schema values due to circular imports. |
10 | @@ -434,6 +434,14 @@ |
11 | IArchive, 'getUploadersForPackageset', 'packageset', IPackageset) |
12 | patch_plain_parameter_type( |
13 | IArchive, 'deletePackagesetUploader', 'packageset', IPackageset) |
14 | +patch_plain_parameter_type( |
15 | + IArchive, 'removeArchiveDependency', 'dependency', IArchive) |
16 | +patch_plain_parameter_type( |
17 | + IArchive, '_addArchiveDependency', 'dependency', IArchive) |
18 | +patch_choice_parameter_type( |
19 | + IArchive, '_addArchiveDependency', 'pocket', PackagePublishingPocket) |
20 | +patch_entry_return_type( |
21 | + IArchive, '_addArchiveDependency', IArchiveDependency) |
22 | |
23 | |
24 | # IBuildFarmJob |
25 | |
26 | === modified file 'lib/lp/soyuz/browser/archive.py' |
27 | --- lib/lp/soyuz/browser/archive.py 2011-06-09 08:07:52 +0000 |
28 | +++ lib/lp/soyuz/browser/archive.py 2011-06-09 17:59:33 +0000 |
29 | @@ -33,7 +33,6 @@ |
30 | datetime, |
31 | timedelta, |
32 | ) |
33 | -from urlparse import urlparse |
34 | |
35 | import pytz |
36 | from sqlobject import SQLObjectNotFound |
37 | @@ -138,11 +137,13 @@ |
38 | PackagePublishingStatus, |
39 | ) |
40 | from lp.soyuz.interfaces.archive import ( |
41 | + ArchiveDependencyError, |
42 | CannotCopy, |
43 | IArchive, |
44 | IArchiveEditDependenciesForm, |
45 | IArchiveSet, |
46 | NoSuchPPA, |
47 | + validate_external_dependencies, |
48 | ) |
49 | from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet |
50 | from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet |
51 | @@ -1864,45 +1865,6 @@ |
52 | self._messages.append(structured( |
53 | '<p>Primary dependency added: %s</p>', primary_dependency.title)) |
54 | |
55 | - def validate(self, data): |
56 | - """Validate dependency configuration changes. |
57 | - |
58 | - Skip checks if no dependency candidate was sent in the form. |
59 | - |
60 | - Validate if the requested PPA dependency is sane (different than |
61 | - the context PPA and not yet registered). |
62 | - |
63 | - Also check if the dependency candidate is private, if so, it can |
64 | - only be set if the user has 'launchpad.View' permission on it and |
65 | - the context PPA is also private (this way P3A credentials will be |
66 | - sanitized from buildlogs). |
67 | - """ |
68 | - dependency_candidate = data.get('dependency_candidate') |
69 | - |
70 | - if dependency_candidate is None: |
71 | - return |
72 | - |
73 | - if dependency_candidate == self.context: |
74 | - self.setFieldError('dependency_candidate', |
75 | - "An archive should not depend on itself.") |
76 | - return |
77 | - |
78 | - if self.context.getArchiveDependency(dependency_candidate): |
79 | - self.setFieldError('dependency_candidate', |
80 | - "This dependency is already registered.") |
81 | - return |
82 | - |
83 | - if not check_permission('launchpad.View', dependency_candidate): |
84 | - self.setFieldError( |
85 | - 'dependency_candidate', |
86 | - "You don't have permission to use this dependency.") |
87 | - return |
88 | - |
89 | - if dependency_candidate.private and not self.context.private: |
90 | - self.setFieldError( |
91 | - 'dependency_candidate', |
92 | - "Public PPAs cannot depend on private ones.") |
93 | - |
94 | @action(_("Save"), name="save") |
95 | def save_action(self, action, data): |
96 | """Save dependency configuration changes. |
97 | @@ -1914,18 +1876,21 @@ |
98 | refreshing. And render a page notification with the summary of the |
99 | changes made. |
100 | """ |
101 | - # Redirect after POST. |
102 | - self.next_url = self.request.URL |
103 | - |
104 | # Process the form. |
105 | self._add_primary_dependencies(data) |
106 | - self._add_ppa_dependencies(data) |
107 | + try: |
108 | + self._add_ppa_dependencies(data) |
109 | + except ArchiveDependencyError as e: |
110 | + self.setFieldError('dependency_candidate', str(e)) |
111 | + return |
112 | self._remove_dependencies(data) |
113 | |
114 | # Issue a notification if anything was changed. |
115 | if len(self.messages) > 0: |
116 | self.request.response.addNotification( |
117 | structured(self.messages)) |
118 | + # Redirect after POST. |
119 | + self.next_url = self.request.URL |
120 | |
121 | |
122 | class ArchiveActivateView(LaunchpadFormView): |
123 | @@ -2128,7 +2093,7 @@ |
124 | # Check the external_dependencies field. |
125 | ext_deps = data.get('external_dependencies') |
126 | if ext_deps is not None: |
127 | - errors = self.validate_external_dependencies(ext_deps) |
128 | + errors = validate_external_dependencies(ext_deps) |
129 | if len(errors) != 0: |
130 | error_text = "\n".join(errors) |
131 | self.setFieldError('external_dependencies', error_text) |
132 | @@ -2138,31 +2103,6 @@ |
133 | 'commercial', |
134 | 'Can only set commericial for private archives.') |
135 | |
136 | - def validate_external_dependencies(self, ext_deps): |
137 | - """Validate the external_dependencies field. |
138 | - |
139 | - :param ext_deps: The dependencies form field to check. |
140 | - """ |
141 | - errors = [] |
142 | - # The field can consist of multiple entries separated by |
143 | - # newlines, so process each in turn. |
144 | - for dep in ext_deps.splitlines(): |
145 | - try: |
146 | - deb, url, suite, components = dep.split(" ", 3) |
147 | - except ValueError: |
148 | - errors.append( |
149 | - "'%s' is not a complete and valid sources.list entry" |
150 | - % dep) |
151 | - continue |
152 | - |
153 | - if deb != "deb": |
154 | - errors.append("%s: Must start with 'deb'" % dep) |
155 | - url_components = urlparse(url) |
156 | - if not url_components[0] or not url_components[1]: |
157 | - errors.append("%s: Invalid URL" % dep) |
158 | - |
159 | - return errors |
160 | - |
161 | @property |
162 | def owner_is_private_team(self): |
163 | """Is the owner a private team? |
164 | |
165 | === modified file 'lib/lp/soyuz/browser/tests/archive-views.txt' |
166 | --- lib/lp/soyuz/browser/tests/archive-views.txt 2011-06-06 02:00:42 +0000 |
167 | +++ lib/lp/soyuz/browser/tests/archive-views.txt 2011-06-09 17:59:33 +0000 |
168 | @@ -753,7 +753,8 @@ |
169 | mark/ppa |
170 | |
171 | >>> print dependency.title.escapedtext |
172 | - <a href="http://launchpad.dev/~mark/+archive/ppa">PPA for Mark Shuttleworth</a> |
173 | + <a href="http://launchpad.dev/~mark/+archive/ppa">PPA for Mark |
174 | + Shuttleworth</a> |
175 | |
176 | The form focus, now that we have a recorded dependencies, is set to the |
177 | first listed dependency. |
178 | @@ -1387,32 +1388,34 @@ |
179 | >>> ppa_archive_view = create_initialized_view( |
180 | ... cprov.archive, name="+admin") |
181 | |
182 | -The validate_external_dependencies() method is called when validating and will |
183 | -return a list of errors if the data dis not validate. A valid entry is of the |
184 | -form: |
185 | + >>> from lp.soyuz.interfaces.archive import validate_external_dependencies |
186 | + |
187 | +The validate_external_dependencies() function is called when validating and |
188 | +will return a list of errors if the data dis not validate. A valid entry is |
189 | +of the form: |
190 | deb scheme://domain/ suite component[s] |
191 | |
192 | - >>> print ppa_archive_view.validate_external_dependencies( |
193 | + >>> print validate_external_dependencies( |
194 | ... "deb http://example.com/ karmic main") |
195 | [] |
196 | |
197 | Multiple entries are valid, separated by newlines: |
198 | |
199 | - >>> print ppa_archive_view.validate_external_dependencies( |
200 | + >>> print validate_external_dependencies( |
201 | ... "deb http://example.com/ karmic main\n" |
202 | ... "deb http://example.com/ karmic restricted") |
203 | [] |
204 | |
205 | If the line does not start with the word "deb" it fails: |
206 | |
207 | - >>> print ppa_archive_view.validate_external_dependencies( |
208 | + >>> print validate_external_dependencies( |
209 | ... "deb http://example.com/ karmic universe\n" |
210 | ... "dab http://example.com/ karmic main") |
211 | ["dab http://example.com/ karmic main: Must start with 'deb'"] |
212 | |
213 | If the line has too few parts it fails. Here we're missing a suite: |
214 | |
215 | - >>> print ppa_archive_view.validate_external_dependencies( |
216 | + >>> print validate_external_dependencies( |
217 | ... "deb http://example.com/ karmic universe\n" |
218 | ... "deb http://example.com/ main") |
219 | ["'deb http://example.com/ main' |
220 | @@ -1420,7 +1423,7 @@ |
221 | |
222 | If the URL looks invalid, it fails: |
223 | |
224 | - >>> print ppa_archive_view.validate_external_dependencies( |
225 | + >>> print validate_external_dependencies( |
226 | ... "deb http://example.com/ karmic universe\n" |
227 | ... "deb example.com/ karmic main") |
228 | ['deb example.com/ karmic main: Invalid URL'] |
229 | |
230 | === modified file 'lib/lp/soyuz/browser/tests/test_archive_webservice.py' |
231 | --- lib/lp/soyuz/browser/tests/test_archive_webservice.py 2011-06-06 02:00:42 +0000 |
232 | +++ lib/lp/soyuz/browser/tests/test_archive_webservice.py 2011-06-09 17:59:33 +0000 |
233 | @@ -1,19 +1,30 @@ |
234 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
235 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
236 | # GNU Affero General Public License version 3 (see the file LICENSE). |
237 | |
238 | __metaclass__ = type |
239 | |
240 | import unittest |
241 | |
242 | -from lazr.restfulclient.errors import HTTPError |
243 | +from lazr.restfulclient.errors import ( |
244 | + BadRequest, |
245 | + HTTPError, |
246 | + Unauthorized as LRUnauthorized, |
247 | +) |
248 | +from testtools import ExpectedException |
249 | +import transaction |
250 | +from zope.component import getUtility |
251 | |
252 | from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller |
253 | from canonical.testing.layers import DatabaseFunctionalLayer |
254 | +from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
255 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
256 | from lp.soyuz.enums import ArchivePurpose |
257 | from lp.testing import ( |
258 | celebrity_logged_in, |
259 | launchpadlib_for, |
260 | + person_logged_in, |
261 | TestCaseWithFactory, |
262 | + WebServiceTestCase, |
263 | ) |
264 | |
265 | |
266 | @@ -67,5 +78,111 @@ |
267 | "in the 'DEVELOPMENT' state.", e.content) |
268 | |
269 | |
270 | +class TestExternalDependencies(WebServiceTestCase): |
271 | + |
272 | + def test_external_dependencies_random_user(self): |
273 | + """Normal users can look but not touch.""" |
274 | + archive = self.factory.makeArchive() |
275 | + transaction.commit() |
276 | + ws_archive = self.wsObject(archive) |
277 | + self.assertIs(None, ws_archive.external_dependencies) |
278 | + ws_archive.external_dependencies = "random" |
279 | + with ExpectedException(LRUnauthorized, '.*'): |
280 | + ws_archive.lp_save() |
281 | + |
282 | + def test_external_dependencies_owner(self): |
283 | + """Normal archive owners can look but not touch.""" |
284 | + archive = self.factory.makeArchive() |
285 | + transaction.commit() |
286 | + ws_archive = self.wsObject(archive, archive.owner) |
287 | + self.assertIs(None, ws_archive.external_dependencies) |
288 | + ws_archive.external_dependencies = "random" |
289 | + with ExpectedException(LRUnauthorized, '.*'): |
290 | + ws_archive.lp_save() |
291 | + |
292 | + def test_external_dependencies_commercial_owner_invalid(self): |
293 | + """Commercial admins can look and touch.""" |
294 | + commercial = getUtility(ILaunchpadCelebrities).commercial_admin |
295 | + owner = self.factory.makePerson(member_of=[commercial]) |
296 | + archive = self.factory.makeArchive(owner=owner) |
297 | + transaction.commit() |
298 | + ws_archive = self.wsObject(archive, archive.owner) |
299 | + self.assertIs(None, ws_archive.external_dependencies) |
300 | + ws_archive.external_dependencies = "random" |
301 | + regex = '(\n|.)*Invalid external dependencies(\n|.)*' |
302 | + with ExpectedException(BadRequest, regex): |
303 | + ws_archive.lp_save() |
304 | + |
305 | + def test_external_dependencies_commercial_owner_valid(self): |
306 | + """Commercial admins can look and touch.""" |
307 | + commercial = getUtility(ILaunchpadCelebrities).commercial_admin |
308 | + owner = self.factory.makePerson(member_of=[commercial]) |
309 | + archive = self.factory.makeArchive(owner=owner) |
310 | + transaction.commit() |
311 | + ws_archive = self.wsObject(archive, archive.owner) |
312 | + self.assertIs(None, ws_archive.external_dependencies) |
313 | + ws_archive.external_dependencies = ( |
314 | + "deb http://example.org suite components") |
315 | + ws_archive.lp_save() |
316 | + |
317 | + |
318 | +class TestArchiveDependencies(WebServiceTestCase): |
319 | + |
320 | + def test_addArchiveDependency_random_user(self): |
321 | + """Normal users cannot add archive dependencies.""" |
322 | + archive = self.factory.makeArchive() |
323 | + dependency = self.factory.makeArchive() |
324 | + transaction.commit() |
325 | + ws_archive = self.wsObject(archive) |
326 | + ws_dependency = self.wsObject(dependency) |
327 | + self.assertContentEqual([], ws_archive.dependencies) |
328 | + failure_regex = '(.|\n)*addArchiveDependency.*launchpad.Edit(.|\n)*' |
329 | + with ExpectedException(LRUnauthorized, failure_regex): |
330 | + dependency = ws_archive.addArchiveDependency( |
331 | + dependency=ws_dependency, pocket='Release', component='main') |
332 | + |
333 | + def test_addArchiveDependency_owner(self): |
334 | + """Normal users cannot add archive dependencies.""" |
335 | + archive = self.factory.makeArchive() |
336 | + dependency = self.factory.makeArchive() |
337 | + transaction.commit() |
338 | + ws_archive = self.wsObject(archive, archive.owner) |
339 | + ws_dependency = self.wsObject(dependency) |
340 | + self.assertContentEqual([], ws_archive.dependencies) |
341 | + with ExpectedException(BadRequest, '(.|\n)*asdf(.|\n)*'): |
342 | + ws_archive.addArchiveDependency( |
343 | + dependency=ws_dependency, pocket='Release', component='asdf') |
344 | + dependency = ws_archive.addArchiveDependency( |
345 | + dependency=ws_dependency, pocket='Release', component='main') |
346 | + self.assertContentEqual([dependency], ws_archive.dependencies) |
347 | + |
348 | + def test_removeArchiveDependency_random_user(self): |
349 | + """Normal users can remove archive dependencies.""" |
350 | + archive = self.factory.makeArchive() |
351 | + dependency = self.factory.makeArchive() |
352 | + with person_logged_in(archive.owner): |
353 | + archive.addArchiveDependency( |
354 | + dependency, PackagePublishingPocket.RELEASE) |
355 | + transaction.commit() |
356 | + ws_archive = self.wsObject(archive) |
357 | + ws_dependency = self.wsObject(dependency) |
358 | + failure_regex = '(.|\n)*remove.*Dependency.*launchpad.Edit(.|\n)*' |
359 | + with ExpectedException(LRUnauthorized, failure_regex): |
360 | + ws_archive.removeArchiveDependency(dependency=ws_dependency) |
361 | + |
362 | + def test_removeArchiveDependency_owner(self): |
363 | + """Normal users can remove archive dependencies.""" |
364 | + archive = self.factory.makeArchive() |
365 | + dependency = self.factory.makeArchive() |
366 | + with person_logged_in(archive.owner): |
367 | + archive.addArchiveDependency( |
368 | + dependency, PackagePublishingPocket.RELEASE) |
369 | + transaction.commit() |
370 | + ws_archive = self.wsObject(archive, archive.owner) |
371 | + ws_dependency = self.wsObject(dependency) |
372 | + ws_archive.removeArchiveDependency(dependency=ws_dependency) |
373 | + self.assertContentEqual([], ws_archive.dependencies) |
374 | + |
375 | + |
376 | def test_suite(): |
377 | return unittest.TestLoader().loadTestsFromName(__name__) |
378 | |
379 | === modified file 'lib/lp/soyuz/doc/archive-dependencies.txt' |
380 | --- lib/lp/soyuz/doc/archive-dependencies.txt 2011-06-07 00:16:31 +0000 |
381 | +++ lib/lp/soyuz/doc/archive-dependencies.txt 2011-06-09 17:59:33 +0000 |
382 | @@ -281,115 +281,17 @@ |
383 | deb http://archive.launchpad.dev/ubuntu hoary-updates |
384 | main restricted universe multiverse |
385 | |
386 | -The authentication information gets added for private PPA |
387 | -dependencies. We'll create a private PPA and then update cprov's PPA, |
388 | -removing the dependency on Mark's public PPA and adding one for the |
389 | -new private PPA. |
390 | - |
391 | - >>> private_ppa = factory.makeArchive( |
392 | - ... owner=mark, name='p3a', private=True, distribution=ubuntu) |
393 | - >>> pub_binaries = test_publisher.getPubBinaries( |
394 | - ... binaryname='dep-bin', archive=private_ppa, |
395 | - ... status=PackagePublishingStatus.PUBLISHED) |
396 | >>> cprov.archive.removeArchiveDependency(mark.archive) |
397 | - >>> archive_dependency = cprov.archive.addArchiveDependency( |
398 | - ... private_ppa, PackagePublishingPocket.RELEASE, |
399 | - ... getUtility(IComponentSet)['main']) |
400 | - |
401 | - >>> print_building_sources_list(a_build) |
402 | - deb http://ppa.launchpad.dev/cprov/ppa/ubuntu hoary main |
403 | - deb http://buildd:sekrit@private-ppa.launchpad.dev/mark/p3a/ubuntu |
404 | - hoary main |
405 | - deb http://archive.launchpad.dev/ubuntu hoary |
406 | - main restricted universe multiverse |
407 | - deb http://archive.launchpad.dev/ubuntu hoary-security |
408 | - main restricted universe multiverse |
409 | - deb http://archive.launchpad.dev/ubuntu hoary-updates |
410 | - main restricted universe multiverse |
411 | - |
412 | -Remove the private PPA dependency before continuing. |
413 | - |
414 | - >>> cprov.archive.removeArchiveDependency(private_ppa) |
415 | - |
416 | - >>> cprov.archive.external_dependencies is None |
417 | - True |
418 | - >>> cprov.archive.enabled |
419 | - True |
420 | - |
421 | -What happens when we fake external dependencies? |
422 | - |
423 | - >>> cprov.archive.external_dependencies = ( |
424 | - ... "Malformed format string here --> %(series)") |
425 | - |
426 | -The error caused by the malformed format string is caught and the other |
427 | -dependencies are returned as normal. |
428 | - |
429 | - >>> print_building_sources_list(a_build) |
430 | - ERROR:root:Exception during external dependency processing: |
431 | - Traceback (most recent call last): |
432 | - File ... in get_sources_list_for_building |
433 | - {'series': distroarchseries.distroseries.name}) |
434 | - ValueError: incomplete format |
435 | - <BLANKLINE> |
436 | - deb http://ppa.launchpad.dev/cprov/ppa/ubuntu hoary main |
437 | - deb http://archive.launchpad.dev/ubuntu hoary |
438 | - main restricted universe multiverse |
439 | - deb http://archive.launchpad.dev/ubuntu hoary-security |
440 | - main restricted universe multiverse |
441 | - deb http://archive.launchpad.dev/ubuntu hoary-updates |
442 | - main restricted universe multiverse |
443 | - |
444 | -However, in order to avoid the problem going forward (and to allow the PPA |
445 | -owner to correct the external dependencies) the PPA will be disabled. |
446 | - |
447 | - >>> cprov.archive.enabled |
448 | - False |
449 | - |
450 | -Restore the 'external_dependencies' property to its original state and |
451 | -re-enable the PPA. |
452 | - |
453 | - >>> cprov.archive.external_dependencies = None |
454 | - >>> cprov.archive.enable() |
455 | - |
456 | - >>> cprov.archive.external_dependencies is None |
457 | - True |
458 | - >>> cprov.archive.enabled |
459 | - True |
460 | - |
461 | -What happens when we fake external dependencies? |
462 | - |
463 | - >>> cprov.archive.external_dependencies = ( |
464 | - ... "Malformed format string here --> %(series)") |
465 | - |
466 | -The error caused by the malformed format string is caught and the other |
467 | -dependencies are returned as normal. |
468 | - |
469 | - >>> print_building_sources_list(a_build) |
470 | - ERROR:root:Exception during external dependency processing: |
471 | - Traceback (most recent call last): |
472 | - File ... in get_sources_list_for_building |
473 | - {'series': distroarchseries.distroseries.name}) |
474 | - ValueError: incomplete format |
475 | - <BLANKLINE> |
476 | - deb http://ppa.launchpad.dev/cprov/ppa/ubuntu hoary main |
477 | - deb http://archive.launchpad.dev/ubuntu hoary |
478 | - main restricted universe multiverse |
479 | - deb http://archive.launchpad.dev/ubuntu hoary-security |
480 | - main restricted universe multiverse |
481 | - deb http://archive.launchpad.dev/ubuntu hoary-updates |
482 | - main restricted universe multiverse |
483 | - |
484 | -However, in order to avoid the problem going forward (and to allow the PPA |
485 | -owner to correct the external dependencies) the PPA will be disabled. |
486 | - |
487 | - >>> cprov.archive.enabled |
488 | - False |
489 | - |
490 | -Restore the 'external_dependencies' property to its original state and |
491 | -re-enable the PPA. |
492 | - |
493 | - >>> cprov.archive.external_dependencies = None |
494 | - >>> cprov.archive.enable() |
495 | + |
496 | +What when we supply invalid dependencies, an exception is raised. |
497 | + |
498 | + >>> cprov.archive.external_dependencies = ( |
499 | + ... "Malformed format string here --> %(series)") |
500 | + Traceback (most recent call last): |
501 | + InvalidExternalDependencies: (InvalidExternalDependencies(...), |
502 | + "Invalid external dependencies:\nMalformed format string here |
503 | + --> %(series): Must start with 'deb'\nMalformed format string here |
504 | + --> %(series): Invalid URL\n") |
505 | |
506 | |
507 | == Overriding default primary archive dependencies == |
508 | |
509 | === modified file 'lib/lp/soyuz/doc/archive.txt' |
510 | --- lib/lp/soyuz/doc/archive.txt 2011-06-06 02:00:42 +0000 |
511 | +++ lib/lp/soyuz/doc/archive.txt 2011-06-09 17:59:33 +0000 |
512 | @@ -101,7 +101,7 @@ |
513 | |
514 | >>> login("admin@canonical.com") |
515 | >>> cprov_archive.relative_build_score = 100 |
516 | - >>> cprov_archive.external_dependencies = "test" |
517 | + >>> cprov_archive.external_dependencies = "deb http://foo hardy bar" |
518 | |
519 | The buildd_secret is used by the slave scanner when generating a |
520 | sources.list entry for the builder to access a private archive. It is |
521 | @@ -1043,6 +1043,7 @@ |
522 | >>> print_dependencies(no_priv.archive) |
523 | No dependencies recorded. |
524 | |
525 | + >>> login_person(no_priv) |
526 | >>> archive_dependency = no_priv.archive.addArchiveDependency( |
527 | ... cprov.archive, release_pocket, main_component) |
528 | |
529 | @@ -1058,8 +1059,7 @@ |
530 | ... cprov.archive, release_pocket, main_component) |
531 | Traceback (most recent call last): |
532 | ... |
533 | - ArchiveDependencyError: Only one dependency record per archive is |
534 | - supported. |
535 | + ArchiveDependencyError: This dependency is already registered. |
536 | |
537 | 'dependency' and target archive are the same. |
538 | |
539 | @@ -1099,8 +1099,7 @@ |
540 | ... getUtility(IComponentSet)['main']) |
541 | Traceback (most recent call last): |
542 | ... |
543 | - ArchiveDependencyError: Only one dependency record per archive is |
544 | - supported. |
545 | + ArchiveDependencyError: This dependency is already registered. |
546 | |
547 | Thus archive dependency removal can be performed simply by passing the |
548 | dependency target. |
549 | |
550 | === modified file 'lib/lp/soyuz/interfaces/archive.py' |
551 | --- lib/lp/soyuz/interfaces/archive.py 2011-06-07 00:16:31 +0000 |
552 | +++ lib/lp/soyuz/interfaces/archive.py 2011-06-09 17:59:33 +0000 |
553 | @@ -32,6 +32,7 @@ |
554 | 'IDistributionArchive', |
555 | 'InsufficientUploadRights', |
556 | 'InvalidComponent', |
557 | + 'InvalidExternalDependencies', |
558 | 'InvalidPocketForPartnerArchive', |
559 | 'InvalidPocketForPPA', |
560 | 'IPPA', |
561 | @@ -43,8 +44,12 @@ |
562 | 'PocketNotFound', |
563 | 'VersionRequiresName', |
564 | 'default_name_by_purpose', |
565 | + 'validate_external_dependencies', |
566 | ] |
567 | |
568 | + |
569 | +from urlparse import urlparse |
570 | + |
571 | from lazr.enum import DBEnumeratedType |
572 | from lazr.restful.declarations import ( |
573 | call_with, |
574 | @@ -54,6 +59,7 @@ |
575 | export_read_operation, |
576 | export_write_operation, |
577 | exported, |
578 | + operation_for_version, |
579 | operation_parameters, |
580 | operation_returns_collection_of, |
581 | operation_returns_entry, |
582 | @@ -114,59 +120,59 @@ |
583 | |
584 | class CannotCopy(Exception): |
585 | """Exception raised when a copy cannot be performed.""" |
586 | - webservice_error(400) #Bad request. |
587 | + webservice_error(400) # Bad request. |
588 | |
589 | |
590 | class CannotSwitchPrivacy(Exception): |
591 | """Raised when switching the privacy of an archive that has |
592 | publishing records.""" |
593 | - webservice_error(400) # Bad request. |
594 | + webservice_error(400) # Bad request. |
595 | |
596 | |
597 | class PocketNotFound(Exception): |
598 | """Invalid pocket.""" |
599 | - webservice_error(400) #Bad request. |
600 | + webservice_error(400) # Bad request. |
601 | |
602 | |
603 | class DistroSeriesNotFound(Exception): |
604 | """Invalid distroseries.""" |
605 | - webservice_error(400) #Bad request. |
606 | + webservice_error(400) # Bad request. |
607 | |
608 | |
609 | class AlreadySubscribed(Exception): |
610 | """Raised when creating a subscription for a subscribed person.""" |
611 | - webservice_error(400) # Bad request. |
612 | + webservice_error(400) # Bad request. |
613 | |
614 | |
615 | class ArchiveNotPrivate(Exception): |
616 | """Raised when creating an archive subscription for a public archive.""" |
617 | - webservice_error(400) # Bad request. |
618 | + webservice_error(400) # Bad request. |
619 | |
620 | |
621 | class NoTokensForTeams(Exception): |
622 | """Raised when creating a token for a team, rather than a person.""" |
623 | - webservice_error(400) # Bad request. |
624 | + webservice_error(400) # Bad request. |
625 | |
626 | |
627 | class ComponentNotFound(Exception): |
628 | """Invalid source name.""" |
629 | - webservice_error(400) #Bad request. |
630 | + webservice_error(400) # Bad request. |
631 | |
632 | |
633 | class InvalidComponent(Exception): |
634 | """Invalid component name.""" |
635 | - webservice_error(400) #Bad request. |
636 | + webservice_error(400) # Bad request. |
637 | |
638 | |
639 | class NoSuchPPA(NameLookupFailed): |
640 | """Raised when we try to look up an PPA that doesn't exist.""" |
641 | - webservice_error(400) #Bad request. |
642 | + webservice_error(400) # Bad request. |
643 | _message_prefix = "No such ppa" |
644 | |
645 | |
646 | class VersionRequiresName(Exception): |
647 | """Raised on some queries when version is specified but name is not.""" |
648 | - webservice_error(400) # Bad request. |
649 | + webservice_error(400) # Bad request. |
650 | |
651 | |
652 | class CannotRestrictArchitectures(Exception): |
653 | @@ -175,7 +181,7 @@ |
654 | |
655 | class CannotUploadToArchive(Exception): |
656 | """A reason for not being able to upload to an archive.""" |
657 | - webservice_error(403) # Forbidden. |
658 | + webservice_error(403) # Forbidden. |
659 | |
660 | _fmt = '%(person)s has no upload rights to %(archive)s.' |
661 | |
662 | @@ -192,7 +198,7 @@ |
663 | |
664 | class CannotUploadToPocket(Exception): |
665 | """Returned when a pocket is closed for uploads.""" |
666 | - webservice_error(403) # Forbidden. |
667 | + webservice_error(403) # Forbidden. |
668 | |
669 | def __init__(self, distroseries, pocket): |
670 | Exception.__init__(self, |
671 | @@ -248,6 +254,17 @@ |
672 | CannotUploadToArchive.__init__(self, archive_name=archive_name) |
673 | |
674 | |
675 | +class InvalidExternalDependencies(Exception): |
676 | + """Tried to set external dependencies to an invalid value.""" |
677 | + |
678 | + webservice_error(400) # Bad request. |
679 | + |
680 | + def __init__(self, errors): |
681 | + error_msg = 'Invalid external dependencies:\n%s\n' % '\n'.join(errors) |
682 | + super(Exception, self).__init__(self, error_msg) |
683 | + self.errors = errors |
684 | + |
685 | + |
686 | class IArchivePublic(IHasOwner, IPrivacy): |
687 | """An Archive interface for publicly available operations.""" |
688 | id = Attribute("The archive ID.") |
689 | @@ -332,7 +349,7 @@ |
690 | |
691 | distribution = exported( |
692 | Reference( |
693 | - Interface, # Redefined to IDistribution later. |
694 | + Interface, # Redefined to IDistribution later. |
695 | title=_("The distribution that uses or is used by this " |
696 | "archive."))) |
697 | |
698 | @@ -412,9 +429,9 @@ |
699 | "A delta to apply to all build scores for the archive. Builds " |
700 | "with a higher score will build sooner.")) |
701 | |
702 | - external_dependencies = Text( |
703 | - title=_("External dependencies"), required=False, readonly=False, |
704 | - description=_( |
705 | + external_dependencies = exported( |
706 | + Text(title=_("External dependencies"), required=False, |
707 | + readonly=False, description=_( |
708 | "Newline-separated list of repositories to be used to retrieve " |
709 | "any external build dependencies when building packages in the " |
710 | "archive, in the format:\n" |
711 | @@ -422,7 +439,7 @@ |
712 | "[components]\n" |
713 | "The series variable is replaced with the series name of the " |
714 | "context build.\n" |
715 | - "NOTE: This is for migration of OEM PPAs only!")) |
716 | + "NOTE: This is for migration of OEM PPAs only!"))) |
717 | |
718 | enabled_restricted_families = CollectionField( |
719 | title=_("Enabled restricted families"), |
720 | @@ -520,27 +537,6 @@ |
721 | records. |
722 | """ |
723 | |
724 | - def removeArchiveDependency(dependency): |
725 | - """Remove the `IArchiveDependency` record for the given dependency. |
726 | - |
727 | - :param dependency: is an `IArchive` object. |
728 | - """ |
729 | - |
730 | - def addArchiveDependency(dependency, pocket, component=None): |
731 | - """Record an archive dependency record for the context archive. |
732 | - |
733 | - :param dependency: is an `IArchive` object. |
734 | - :param pocket: is an `PackagePublishingPocket` enum. |
735 | - :param component: is an optional `IComponent` object, if not given |
736 | - the archive dependency will be tied to the component used |
737 | - for a corresponding source in primary archive. |
738 | - |
739 | - :raise: `ArchiveDependencyError` if given 'dependency' does not fit |
740 | - the context archive. |
741 | - :return: a `IArchiveDependency` object targeted to the context |
742 | - `IArchive` requiring 'dependency' `IArchive`. |
743 | - """ |
744 | - |
745 | def getPermissions(person, item, perm_type): |
746 | """Get the `IArchivePermission` record with the supplied details. |
747 | |
748 | @@ -892,7 +888,8 @@ |
749 | :return: True if the person is allowed to upload the source package. |
750 | """ |
751 | |
752 | - num_pkgs_building = Attribute("Tuple of packages building and waiting to build") |
753 | + num_pkgs_building = Attribute( |
754 | + "Tuple of packages building and waiting to build") |
755 | |
756 | def getSourcePackageReleases(build_status=None): |
757 | """Return the releases for this archive. |
758 | @@ -944,7 +941,8 @@ |
759 | dependencies = exported( |
760 | CollectionField( |
761 | title=_("Archive dependencies recorded for this archive."), |
762 | - value_type=Reference(schema=Interface), #Really IArchiveDependency |
763 | + value_type=Reference(schema=Interface), |
764 | + # Really IArchiveDependency |
765 | readonly=True)) |
766 | |
767 | description = exported( |
768 | @@ -1111,8 +1109,8 @@ |
769 | """ |
770 | |
771 | @operation_parameters( |
772 | - dependency=Reference(schema=Interface)) #Really IArchive. See below. |
773 | - @operation_returns_entry(schema=Interface) #Really IArchiveDependency. |
774 | + dependency=Reference(schema=Interface)) # Really IArchive. See below. |
775 | + @operation_returns_entry(schema=Interface) # Really IArchiveDependency. |
776 | @export_read_operation() |
777 | def getArchiveDependency(dependency): |
778 | """Return the `IArchiveDependency` object for the given dependency. |
779 | @@ -1233,7 +1231,8 @@ |
780 | source_names=List( |
781 | title=_("Source package names"), |
782 | value_type=TextLine()), |
783 | - from_archive=Reference(schema=Interface), #Really IArchive, see below |
784 | + from_archive=Reference(schema=Interface), |
785 | + #Really IArchive, see below |
786 | to_pocket=TextLine(title=_("Pocket name")), |
787 | to_series=TextLine(title=_("Distroseries name"), required=False), |
788 | include_binaries=Bool( |
789 | @@ -1275,7 +1274,8 @@ |
790 | @operation_parameters( |
791 | source_name=TextLine(title=_("Source package name")), |
792 | version=TextLine(title=_("Version")), |
793 | - from_archive=Reference(schema=Interface), #Really IArchive, see below |
794 | + from_archive=Reference(schema=Interface), |
795 | + # Really IArchive, see below |
796 | to_pocket=TextLine(title=_("Pocket name")), |
797 | to_series=TextLine(title=_("Distroseries name"), required=False), |
798 | include_binaries=Bool( |
799 | @@ -1314,7 +1314,7 @@ |
800 | |
801 | @call_with(registrant=REQUEST_USER) |
802 | @operation_parameters( |
803 | - subscriber = PublicPersonChoice( |
804 | + subscriber=PublicPersonChoice( |
805 | title=_("Subscriber"), |
806 | required=True, |
807 | vocabulary='ValidPersonOrTeam', |
808 | @@ -1458,6 +1458,61 @@ |
809 | processed. |
810 | """ |
811 | |
812 | + def addArchiveDependency(dependency, pocket, component=None): |
813 | + """Record an archive dependency record for the context archive. |
814 | + |
815 | + :param dependency: is an `IArchive` object. |
816 | + :param pocket: is an `PackagePublishingPocket` enum. |
817 | + :param component: is an optional `IComponent` object, if not given |
818 | + the archive dependency will be tied to the component used |
819 | + for a corresponding source in primary archive. |
820 | + |
821 | + :raise: `ArchiveDependencyError` if given 'dependency' does not fit |
822 | + the context archive. |
823 | + :return: a `IArchiveDependency` object targeted to the context |
824 | + `IArchive` requiring 'dependency' `IArchive`. |
825 | + """ |
826 | + |
827 | + @operation_parameters( |
828 | + dependency=Reference(schema=Interface, required=True), |
829 | + # Really IArchive |
830 | + pocket=Choice( |
831 | + title=_("Pocket"), |
832 | + description=_("The pocket into which this entry is published"), |
833 | + # Really PackagePublishingPocket. |
834 | + vocabulary=DBEnumeratedType, |
835 | + required=True), |
836 | + component=TextLine(title=_("Component"), required=False), |
837 | + ) |
838 | + @export_operation_as('addArchiveDependency') |
839 | + @export_factory_operation(Interface, []) # Really IArchiveDependency |
840 | + @operation_for_version('devel') |
841 | + def _addArchiveDependency(dependency, pocket, component=None): |
842 | + """Record an archive dependency record for the context archive. |
843 | + |
844 | + :param dependency: is an `IArchive` object. |
845 | + :param pocket: is an `PackagePublishingPocket` enum. |
846 | + :param component: is the name of a component. If not given, |
847 | + the archive dependency will be tied to the component used |
848 | + for a corresponding source in primary archive. |
849 | + |
850 | + :raise: `ArchiveDependencyError` if given 'dependency' does not fit |
851 | + the context archive. |
852 | + :return: a `IArchiveDependency` object targeted to the context |
853 | + `IArchive` requiring 'dependency' `IArchive`. |
854 | + """ |
855 | + @operation_parameters( |
856 | + dependency=Reference(schema=Interface, required=True), |
857 | + # Really IArchive |
858 | + ) |
859 | + @export_write_operation() |
860 | + @operation_for_version('devel') |
861 | + def removeArchiveDependency(dependency): |
862 | + """Remove the `IArchiveDependency` record for the given dependency. |
863 | + |
864 | + :param dependency: is an `IArchive` object. |
865 | + """ |
866 | + |
867 | |
868 | class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit, IArchiveView): |
869 | """Main Archive interface.""" |
870 | @@ -1690,3 +1745,29 @@ |
871 | ) |
872 | |
873 | # Circular dependency issues fixed in _schema_circular_imports.py |
874 | + |
875 | + |
876 | +def validate_external_dependencies(ext_deps): |
877 | + """Validate the external_dependencies field. |
878 | + |
879 | + :param ext_deps: The dependencies form field to check. |
880 | + """ |
881 | + errors = [] |
882 | + # The field can consist of multiple entries separated by |
883 | + # newlines, so process each in turn. |
884 | + for dep in ext_deps.splitlines(): |
885 | + try: |
886 | + deb, url, suite, components = dep.split(" ", 3) |
887 | + except ValueError: |
888 | + errors.append( |
889 | + "'%s' is not a complete and valid sources.list entry" |
890 | + % dep) |
891 | + continue |
892 | + |
893 | + if deb != "deb": |
894 | + errors.append("%s: Must start with 'deb'" % dep) |
895 | + url_components = urlparse(url) |
896 | + if not url_components[0] or not url_components[1]: |
897 | + errors.append("%s: Invalid URL" % dep) |
898 | + |
899 | + return errors |
900 | |
901 | === modified file 'lib/lp/soyuz/model/archive.py' |
902 | --- lib/lp/soyuz/model/archive.py 2011-06-07 00:16:31 +0000 |
903 | +++ lib/lp/soyuz/model/archive.py 2011-06-09 17:59:33 +0000 |
904 | @@ -68,6 +68,7 @@ |
905 | ISlaveStore, |
906 | IStore, |
907 | ) |
908 | +from canonical.launchpad.webapp.authorization import check_permission |
909 | from canonical.launchpad.webapp.interfaces import ( |
910 | DEFAULT_FLAVOR, |
911 | IStoreSelector, |
912 | @@ -125,6 +126,7 @@ |
913 | CannotSwitchPrivacy, |
914 | CannotUploadToPocket, |
915 | CannotUploadToPPA, |
916 | + ComponentNotFound, |
917 | default_name_by_purpose, |
918 | DistroSeriesNotFound, |
919 | FULL_COMPONENT_SUPPORT, |
920 | @@ -133,6 +135,7 @@ |
921 | IDistributionArchive, |
922 | InsufficientUploadRights, |
923 | InvalidComponent, |
924 | + InvalidExternalDependencies, |
925 | InvalidPocketForPartnerArchive, |
926 | InvalidPocketForPPA, |
927 | IPPA, |
928 | @@ -143,6 +146,7 @@ |
929 | NoTokensForTeams, |
930 | PocketNotFound, |
931 | VersionRequiresName, |
932 | + validate_external_dependencies, |
933 | ) |
934 | from lp.soyuz.interfaces.archivearch import IArchiveArchSet |
935 | from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthTokenSet |
936 | @@ -197,6 +201,14 @@ |
937 | from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease |
938 | |
939 | |
940 | +def storm_validate_external_dependencies(archive, attr, value): |
941 | + assert attr == 'external_dependencies' |
942 | + errors = validate_external_dependencies(value) |
943 | + if len(errors) > 0: |
944 | + raise InvalidExternalDependencies(errors) |
945 | + return value |
946 | + |
947 | + |
948 | class Archive(SQLBase): |
949 | implements(IArchive, IHasOwner, IHasBuildRecords) |
950 | _table = 'Archive' |
951 | @@ -306,7 +318,8 @@ |
952 | # Launchpad and should be re-examined in October 2010 to see if it |
953 | # is still relevant. |
954 | external_dependencies = StringCol( |
955 | - dbName='external_dependencies', notNull=False, default=None) |
956 | + dbName='external_dependencies', notNull=False, default=None, |
957 | + storm_validator=storm_validate_external_dependencies) |
958 | |
959 | commercial = BoolCol( |
960 | dbName='commercial', notNull=True, default=False) |
961 | @@ -483,7 +496,7 @@ |
962 | |
963 | if name is not None: |
964 | if exact_match: |
965 | - storm_clauses.append(SourcePackageName.name==name) |
966 | + storm_clauses.append(SourcePackageName.name == name) |
967 | else: |
968 | clauses.append( |
969 | "SourcePackageName.name LIKE '%%%%' || %s || '%%%%'" |
970 | @@ -494,7 +507,7 @@ |
971 | raise VersionRequiresName( |
972 | "The 'version' parameter can be used only together with" |
973 | " the 'name' parameter.") |
974 | - storm_clauses.append(SourcePackageRelease.version==version) |
975 | + storm_clauses.append(SourcePackageRelease.version == version) |
976 | else: |
977 | orderBy.insert(1, Desc(SourcePackageRelease.version)) |
978 | |
979 | @@ -514,7 +527,7 @@ |
980 | |
981 | if pocket is not None: |
982 | storm_clauses.append( |
983 | - SourcePackagePublishingHistory.pocket==pocket) |
984 | + SourcePackagePublishingHistory.pocket == pocket) |
985 | |
986 | if created_since_date is not None: |
987 | clauses.append( |
988 | @@ -529,6 +542,7 @@ |
989 | *orderBy) |
990 | if not eager_load: |
991 | return resultset |
992 | + |
993 | # Its not clear that this eager load is necessary or sufficient, it |
994 | # replaces a prejoin that had pathological query plans. |
995 | def eager_load(rows): |
996 | @@ -609,7 +623,7 @@ |
997 | clauseTables = ['SourcePackageRelease', 'SourcePackageName'] |
998 | |
999 | order_const = "SourcePackageRelease.version" |
1000 | - desc_version_order = SQLConstant(order_const+" DESC") |
1001 | + desc_version_order = SQLConstant(order_const + " DESC") |
1002 | orderBy = ['SourcePackageName.name', desc_version_order, |
1003 | '-SourcePackagePublishingHistory.id'] |
1004 | |
1005 | @@ -953,7 +967,14 @@ |
1006 | a_dependency = self.getArchiveDependency(dependency) |
1007 | if a_dependency is not None: |
1008 | raise ArchiveDependencyError( |
1009 | - "Only one dependency record per archive is supported.") |
1010 | + "This dependency is already registered.") |
1011 | + if not check_permission('launchpad.View', dependency): |
1012 | + raise ArchiveDependencyError( |
1013 | + "You don't have permission to use this dependency.") |
1014 | + return |
1015 | + if dependency.private and not self.private: |
1016 | + raise ArchiveDependencyError( |
1017 | + "Public PPAs cannot depend on private ones.") |
1018 | |
1019 | if dependency.is_ppa: |
1020 | if pocket is not PackagePublishingPocket.RELEASE: |
1021 | @@ -969,6 +990,15 @@ |
1022 | archive=self, dependency=dependency, pocket=pocket, |
1023 | component=component) |
1024 | |
1025 | + def _addArchiveDependency(self, dependency, pocket, component=None): |
1026 | + """See `IArchive`.""" |
1027 | + if isinstance(component, basestring): |
1028 | + try: |
1029 | + component = getUtility(IComponentSet)[component] |
1030 | + except NotFoundError as e: |
1031 | + raise ComponentNotFound(e) |
1032 | + return self.addArchiveDependency(dependency, pocket, component) |
1033 | + |
1034 | def getPermissions(self, user, item, perm_type): |
1035 | """See `IArchive`.""" |
1036 | permission_set = getUtility(IArchivePermissionSet) |
1037 | |
1038 | === modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt' |
1039 | --- lib/lp/soyuz/stories/webservice/xx-archive.txt 2011-06-06 02:00:42 +0000 |
1040 | +++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2011-06-09 17:59:33 +0000 |
1041 | @@ -21,6 +21,7 @@ |
1042 | description: u'packages to help my friends.' |
1043 | displayname: u'PPA for Celso Providelo' |
1044 | distribution_link: u'http://.../ubuntu' |
1045 | + external_dependencies: None |
1046 | name: u'ppa' |
1047 | owner_link: u'http://.../~cprov' |
1048 | private: False |
1049 | @@ -87,6 +88,7 @@ |
1050 | description: None |
1051 | displayname: u'Primary Archive for Ubuntu Test' |
1052 | distribution_link: u'http://.../ubuntutest' |
1053 | + external_dependencies: None |
1054 | name: u'primary' |
1055 | owner_link: u'http://.../~ubuntu-team' |
1056 | private: False |
1057 | @@ -823,6 +825,7 @@ |
1058 | description: u'tag:launchpad.net:2008:redacted' |
1059 | displayname: u'PPA named p3a for Celso Providelo' |
1060 | distribution_link: u'http://.../ubuntu' |
1061 | + external_dependencies: None |
1062 | name: u'p3a' |
1063 | owner_link: u'http://.../~cprov' |
1064 | private: True |
1065 | @@ -838,6 +841,7 @@ |
1066 | description: u'packages to help my friends.' |
1067 | displayname: u'PPA named p3a for Celso Providelo' |
1068 | distribution_link: u'http://.../ubuntu' |
1069 | + external_dependencies: None |
1070 | name: u'p3a' |
1071 | owner_link: u'http://.../~cprov' |
1072 | private: True |
1073 | |
1074 | === modified file 'lib/lp/soyuz/tests/test_archive.py' |
1075 | --- lib/lp/soyuz/tests/test_archive.py 2011-06-03 11:00:55 +0000 |
1076 | +++ lib/lp/soyuz/tests/test_archive.py 2011-06-09 17:59:33 +0000 |
1077 | @@ -10,7 +10,9 @@ |
1078 | ) |
1079 | import doctest |
1080 | |
1081 | -from testtools.matchers import DocTestMatches |
1082 | +from testtools.matchers import DocTestMatches, MatchesRegex |
1083 | +from testtools.testcase import ExpectedException |
1084 | + |
1085 | import transaction |
1086 | from zope.component import getUtility |
1087 | from zope.security.interfaces import Unauthorized |
1088 | @@ -48,6 +50,7 @@ |
1089 | PackagePublishingStatus, |
1090 | ) |
1091 | from lp.soyuz.interfaces.archive import ( |
1092 | + ArchiveDependencyError, |
1093 | ArchiveDisabled, |
1094 | CannotRestrictArchitectures, |
1095 | CannotUploadToPocket, |
1096 | @@ -223,7 +226,7 @@ |
1097 | # Calling series_with_sources returns all series with publishings. |
1098 | distribution = self.factory.makeDistribution() |
1099 | archive = self.factory.makeArchive(distribution=distribution) |
1100 | - series_with_no_sources = self.factory.makeDistroSeries( |
1101 | + self.factory.makeDistroSeries( |
1102 | distribution=distribution, version="0.5") |
1103 | series_with_sources1 = self.factory.makeDistroSeries( |
1104 | distribution=distribution, version="1") |
1105 | @@ -283,7 +286,7 @@ |
1106 | source_package_release=sourcepackagerelease, |
1107 | archive=archive, status=status) |
1108 | sprs.append(sourcepackagerelease) |
1109 | - unlinked_spr = self.factory.makeSourcePackageRelease() |
1110 | + self.factory.makeSourcePackageRelease() |
1111 | return archive, sprs |
1112 | |
1113 | def test_getSourcePackageReleases_with_no_params(self): |
1114 | @@ -425,7 +428,7 @@ |
1115 | other_spn = self.factory.makeSourcePackageName(name="bar") |
1116 | archive = self.factory.makeArchive() |
1117 | self.makePublishedSources(archive, |
1118 | - [PackagePublishingStatus.PUBLISHED]*3, |
1119 | + [PackagePublishingStatus.PUBLISHED] * 3, |
1120 | ["1.0", "1.1", "2.0"], |
1121 | [sourcepackagename, sourcepackagename, other_spn]) |
1122 | pubs = removeSecurityProxy(archive)._collectLatestPublishedSources( |
1123 | @@ -1299,6 +1302,69 @@ |
1124 | self.assertTrue(self.archive.build_debug_symbols) |
1125 | |
1126 | |
1127 | +class TestAddArchiveDependencies(TestCaseWithFactory): |
1128 | + |
1129 | + layer = DatabaseFunctionalLayer |
1130 | + |
1131 | + def test_add_hidden_dependency(self): |
1132 | + # The user cannot add a dependency on an archive they cannot see. |
1133 | + archive = self.factory.makeArchive(private=True) |
1134 | + dependency = self.factory.makeArchive(private=True) |
1135 | + with person_logged_in(archive.owner): |
1136 | + with ExpectedException( |
1137 | + ArchiveDependencyError, |
1138 | + "You don't have permission to use this dependency."): |
1139 | + archive.addArchiveDependency(dependency, 'foo') |
1140 | + |
1141 | + def test_private_dependency_public_archive(self): |
1142 | + # A public archive may not depend on a private archive. |
1143 | + archive = self.factory.makeArchive() |
1144 | + dependency = self.factory.makeArchive( |
1145 | + private=True, owner=archive.owner) |
1146 | + with person_logged_in(archive.owner): |
1147 | + with ExpectedException( |
1148 | + ArchiveDependencyError, |
1149 | + "Public PPAs cannot depend on private ones."): |
1150 | + archive.addArchiveDependency(dependency, 'foo') |
1151 | + |
1152 | + def test_add_private_dependency(self): |
1153 | + # The user can add a dependency on private archive they can see. |
1154 | + archive = self.factory.makeArchive(private=True) |
1155 | + dependency = self.factory.makeArchive( |
1156 | + private=True, owner=archive.owner) |
1157 | + with person_logged_in(archive.owner): |
1158 | + archive_dependency = archive.addArchiveDependency(dependency, |
1159 | + PackagePublishingPocket.RELEASE) |
1160 | + self.assertContentEqual( |
1161 | + archive.dependencies, [archive_dependency]) |
1162 | + |
1163 | + |
1164 | +class TestArchiveDependencies(TestCaseWithFactory): |
1165 | + |
1166 | + layer = LaunchpadZopelessLayer |
1167 | + |
1168 | + def test_private_sources_list(self): |
1169 | + """Entries for private dependencies include credentials.""" |
1170 | + p3a = self.factory.makeArchive(name='p3a', private=True) |
1171 | + dependency = self.factory.makeArchive( |
1172 | + name='dependency', private=True, owner=p3a.owner) |
1173 | + with person_logged_in(p3a.owner): |
1174 | + bpph = self.factory.makeBinaryPackagePublishingHistory( |
1175 | + archive=dependency, status=PackagePublishingStatus.PUBLISHED) |
1176 | + p3a.addArchiveDependency(dependency, |
1177 | + PackagePublishingPocket.RELEASE) |
1178 | + build = self.factory.makeBinaryPackageBuild(archive=p3a, |
1179 | + distroarchseries=bpph.distroarchseries) |
1180 | + sources_list = get_sources_list_for_building( |
1181 | + build, build.distro_arch_series, |
1182 | + build.source_package_release.name) |
1183 | + sources_list_str = '\n'.join(sources_list) |
1184 | + matches = MatchesRegex( |
1185 | + "deb http://buildd:sekrit@private-ppa.launchpad.dev/" |
1186 | + "person-name-.*/dependency/ubuntu distroseries-.* main") |
1187 | + self.assertThat(sources_list[0], matches) |
1188 | + |
1189 | + |
1190 | class TestFindDepCandidates(TestCaseWithFactory): |
1191 | """Tests for Archive.findDepCandidates.""" |
1192 | |
1193 | @@ -1346,7 +1412,7 @@ |
1194 | |
1195 | def test_does_not_find_pending_publication(self): |
1196 | # A pending candidate in the same archive should not be found. |
1197 | - bins = self.publisher.getPubBinaries( |
1198 | + self.publisher.getPubBinaries( |
1199 | binaryname='foo', archive=self.archive) |
1200 | self.assertDep('i386', 'foo', []) |
1201 | |
1202 | @@ -1572,7 +1638,7 @@ |
1203 | def test_two_ppas_with_team(self): |
1204 | team = self.factory.makeTeam( |
1205 | subscription_policy=TeamSubscriptionPolicy.MODERATED) |
1206 | - ppa = self.factory.makeArchive(owner=team, name='ppa') |
1207 | + self.factory.makeArchive(owner=team, name='ppa') |
1208 | self.assertEqual("%s already has a PPA named 'ppa'." % ( |
1209 | team.displayname), Archive.validatePPA(team, 'ppa')) |
1210 | |
1211 | |
1212 | === modified file 'lib/lp/testing/factory.py' |
1213 | --- lib/lp/testing/factory.py 2011-06-09 08:07:52 +0000 |
1214 | +++ lib/lp/testing/factory.py 2011-06-09 17:59:33 +0000 |
1215 | @@ -595,7 +595,7 @@ |
1216 | self, email=None, name=None, password=None, |
1217 | email_address_status=None, hide_email_addresses=False, |
1218 | displayname=None, time_zone=None, latitude=None, longitude=None, |
1219 | - selfgenerated_bugnotifications=False): |
1220 | + selfgenerated_bugnotifications=False, member_of=()): |
1221 | """Create and return a new, arbitrary Person. |
1222 | |
1223 | :param email: The email address for the new person. |
1224 | @@ -660,6 +660,10 @@ |
1225 | |
1226 | self.makeOpenIdIdentifier(person.account) |
1227 | |
1228 | + for team in member_of: |
1229 | + with person_logged_in(team.teamowner): |
1230 | + team.addMember(person, team.teamowner) |
1231 | + |
1232 | # Ensure updated ValidPersonCache |
1233 | flush_database_updates() |
1234 | return person |
Looks good :)
[1]
+ with ExpectedExcepti on(LRUnauthoriz ed, '.*'): lp_save( )
+ ws_archive.
Oh wow, that's neat.
[2]
+class InvalidExternal Dependencies( Exception) : error(400) # Bad request. \n%s\n' % '\n'.join(errors) __init_ _(self, error_msg)
+ """Tried to set external dependencies to an invalid value."""
+
+ webservice_
+
+ def __init__(self, errors):
+ error_msg = 'Invalid external dependencies:
+ Exception.
Although it's a single line of inheritance, Exception is a new style
class, so please use super() here.