Merge lp:~rvb/launchpad/change-perm-sync2 into lp:launchpad
- change-perm-sync2
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Raphaël Badin |
Approved revision: | no longer in the source branch. |
Merged at revision: | 12988 |
Proposed branch: | lp:~rvb/launchpad/change-perm-sync2 |
Merge into: | lp:launchpad |
Diff against target: |
1462 lines (+448/-239) 12 files modified
lib/lp/registry/browser/distroseries.py (+7/-2) lib/lp/registry/browser/tests/test_distroseries.py (+218/-157) lib/lp/registry/templates/distroseries-localdifferences.pt (+3/-2) lib/lp/soyuz/browser/archive.py (+9/-3) lib/lp/soyuz/doc/archive.txt (+23/-13) lib/lp/soyuz/interfaces/archive.py (+23/-6) lib/lp/soyuz/model/archive.py (+26/-8) lib/lp/soyuz/model/packagecopyjob.py (+1/-1) lib/lp/soyuz/scripts/packagecopier.py (+66/-23) lib/lp/soyuz/scripts/tests/test_copypackage.py (+51/-23) lib/lp/soyuz/stories/ppa/xx-ppa-files.txt (+2/-1) lib/lp/soyuz/tests/test_archive.py (+19/-0) |
To merge this branch: | bzr merge lp:~rvb/launchpad/change-perm-sync2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
j.c.sackett (community) | Approve | ||
Review via email: mp+60077@code.launchpad.net |
Commit message
[r=jcsackett][bug=773261] Fix the permission check used to sync packages (It previously used lp.edit on the series and now checks upload permissions for every synced package).
Description of the change
This branch fixes the permission check used to sync packages. It previously used lp.edit on the series and now checks upload permissions for every synced package.
This is a follow-up branch on the reverted branch https:/
= Details =
The tricky part is that this perm check cannot be performed by a security adapter because the check needs more information than what is available inside a security adapter.
- Added the permission check inside checkCopy (lib/lp/
- Removed the perm check against lp.edit on the series.
- Added new tests and refactored existing ones in lib/lp/
- Drive-by documentation fix: added documentation for parameter strict_component of checkUpload (lib/lp/
- Drive-by style fix in lib/lp/
== Pre-implementation notes ==
Spoke with William Grant.
= Tests =
(modified tests)
./bin/test -cvv test_distroseries test_sync_
./bin/test -cvv test_distroseries test_sync_
./bin/test -cvv test_distroseries test_sync_
(added tests)
./bin/test -cvv test_distroseries test_canPerform
./bin/test -cvv test_distroseries test_canPerform
./bin/test -cvv test_distroseries test_sync_
./bin/test -cvv test_distroseries test_sync_
./bin/test -cvv test_distroseries test_sync_
./bin/test -cvv test_distroseries test_sync_
./bin/test -cvv test_distroseries test_sync_
./bin/test -cvv test_archive test_hasAnyPerm
= QA =
On DF: https:/
As anon user: the sync button should not be displayed.
As non privileged user: syncing packages should return an error.
As a user with perms on destination component A:
- syncing a package already existing in component B should fail.
- syncing a package already existing in component A should work.
- syncing a package not existing in the destination series should work.
(https:/
As a member of the security team: syncing package should work.
Raphaël Badin (rvb) wrote : | # |
> derived_series, _, _ = self._setUpDSD() scans a little weirdly. You might try
> derived_series = self._setUpDSD()[0] or
> (derived_series, ignore1, ignore2) = self._setUpDSD()
Done.
> I think the period was actually the right punctuation here.
Done.
> It looks to me like syncSource isn't necessary; couldn't you just
> pass one source_name to syncSources? Obviously this isn't a problem
> in your branch, but it may be worth filing a bug to capture the issue?
I'll look into that. Thanks a lot for the review.
Preview Diff
1 | === modified file 'lib/lp/registry/browser/distroseries.py' |
2 | --- lib/lp/registry/browser/distroseries.py 2011-05-05 00:47:27 +0000 |
3 | +++ lib/lp/registry/browser/distroseries.py 2011-05-05 20:54:43 +0000 |
4 | @@ -784,7 +784,8 @@ |
5 | if self.do_copy( |
6 | 'selected_differences', sources, self.context.main_archive, |
7 | self.context, destination_pocket, include_binaries=False, |
8 | - dest_url=series_url, dest_display_name=series_title): |
9 | + dest_url=series_url, dest_display_name=series_title, |
10 | + person=self.user): |
11 | # The copy worked so we can redirect back to the page to |
12 | # show the results. |
13 | self.next_url = self.request.URL |
14 | @@ -803,7 +804,11 @@ |
15 | This method is used as a condition for the above sync action, as |
16 | well as directly in the template. |
17 | """ |
18 | - return (check_permission('launchpad.Edit', self.context) and |
19 | + archive = self.context.main_archive |
20 | + has_perm = (self.user is not None and ( |
21 | + archive.hasAnyPermission(self.user) or |
22 | + check_permission('launchpad.Append', archive))) |
23 | + return (has_perm and |
24 | self.cached_differences.batch.total() > 0) |
25 | |
26 | @property |
27 | |
28 | === modified file 'lib/lp/registry/browser/tests/test_distroseries.py' |
29 | --- lib/lp/registry/browser/tests/test_distroseries.py 2011-05-05 18:23:26 +0000 |
30 | +++ lib/lp/registry/browser/tests/test_distroseries.py 2011-05-05 20:54:43 +0000 |
31 | @@ -27,10 +27,10 @@ |
32 | from zope.security.proxy import removeSecurityProxy |
33 | |
34 | from canonical.config import config |
35 | -from canonical.database.constants import UTC_NOW |
36 | from canonical.database.sqlbase import flush_database_caches |
37 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
38 | from canonical.launchpad.testing.pages import find_tag_by_id |
39 | +from canonical.launchpad.webapp.authorization import check_permission |
40 | from canonical.launchpad.webapp.batching import BatchNavigator |
41 | from canonical.launchpad.webapp.publisher import canonical_url |
42 | from canonical.testing.layers import ( |
43 | @@ -48,8 +48,7 @@ |
44 | DistroSeriesDifferenceStatus, |
45 | DistroSeriesDifferenceType, |
46 | ) |
47 | -from lp.registry.interfaces.pocket import PackagePublishingPocket |
48 | -from lp.registry.interfaces.series import SeriesStatus |
49 | +from lp.registry.interfaces.person import IPersonSet |
50 | from lp.services.features import ( |
51 | get_relevant_feature_controller, |
52 | getFeatureFlag, |
53 | @@ -61,15 +60,18 @@ |
54 | getFeatureStore, |
55 | ) |
56 | from lp.soyuz.enums import ( |
57 | + ArchivePermissionType, |
58 | PackagePublishingStatus, |
59 | SourcePackageFormat, |
60 | ) |
61 | +from lp.soyuz.interfaces.component import IComponentSet |
62 | from lp.soyuz.interfaces.distributionjob import ( |
63 | IInitialiseDistroSeriesJobSource, |
64 | ) |
65 | from lp.soyuz.interfaces.sourcepackageformat import ( |
66 | ISourcePackageFormatSelectionSet, |
67 | ) |
68 | +from lp.soyuz.model.archivepermission import ArchivePermission |
69 | from lp.testing import ( |
70 | celebrity_logged_in, |
71 | feature_flags, |
72 | @@ -176,11 +178,11 @@ |
73 | derived_series, |
74 | '+index', |
75 | principal=self.simple_user) |
76 | - html_text = view() |
77 | + html_content = view() |
78 | |
79 | self.assertEqual( |
80 | None, getFeatureFlag('soyuz.derived-series-ui.enabled')) |
81 | - self.assertThat(html_text, Not(portlet_header)) |
82 | + self.assertThat(html_content, Not(portlet_header)) |
83 | |
84 | def test_differences_portlet_all_differences(self): |
85 | # The difference portlet shows the differences with the parent |
86 | @@ -212,9 +214,9 @@ |
87 | # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides |
88 | # self.features to NullFeatureController. |
89 | view.request.features = get_relevant_feature_controller() |
90 | - html_text = view() |
91 | + html_content = view() |
92 | |
93 | - self.assertThat(html_text, portlet_display) |
94 | + self.assertThat(html_content, portlet_display) |
95 | |
96 | def test_differences_portlet_no_differences(self): |
97 | # The difference portlet displays 'No differences' if there is no |
98 | @@ -238,9 +240,9 @@ |
99 | # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides |
100 | # self.features to NullFeatureController. |
101 | view.request.features = get_relevant_feature_controller() |
102 | - html_text = view() |
103 | + html_content = view() |
104 | |
105 | - self.assertThat(html_text, portlet_display) |
106 | + self.assertThat(html_content, portlet_display) |
107 | |
108 | def test_differences_portlet_initialising(self): |
109 | # The difference portlet displays 'The series is initialising.' if |
110 | @@ -266,10 +268,10 @@ |
111 | # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides |
112 | # self.features to NullFeatureController. |
113 | view.request.features = get_relevant_feature_controller() |
114 | - html_text = view() |
115 | + html_content = view() |
116 | |
117 | self.assertTrue(derived_series.is_initialising) |
118 | - self.assertThat(html_text, portlet_display) |
119 | + self.assertThat(html_content, portlet_display) |
120 | |
121 | |
122 | class TestMilestoneBatchNavigatorAttribute(TestCaseWithFactory): |
123 | @@ -404,7 +406,7 @@ |
124 | class DistroSeriesDifferenceMixin: |
125 | """A helper class for testing differences pages""" |
126 | |
127 | - def _test_packagesets(self, html_text, packageset_text, |
128 | + def _test_packagesets(self, html_content, packageset_text, |
129 | packageset_class, msg_text): |
130 | parent_packagesets = soupmatchers.HTMLContains( |
131 | soupmatchers.Tag( |
132 | @@ -412,7 +414,7 @@ |
133 | attrs={'class': packageset_class}, |
134 | text=packageset_text)) |
135 | |
136 | - self.assertThat(html_text, parent_packagesets) |
137 | + self.assertThat(html_content, parent_packagesets) |
138 | |
139 | |
140 | class TestDistroSeriesLocalDifferences( |
141 | @@ -474,11 +476,11 @@ |
142 | ds_diff.derived_series, |
143 | '+localpackagediffs', |
144 | principal=self.simple_user) |
145 | - html_text = view() |
146 | + html_content = view() |
147 | |
148 | packageset_text = re.compile('\s*' + ps.name) |
149 | self._test_packagesets( |
150 | - html_text, packageset_text, 'parent-packagesets', |
151 | + html_content, packageset_text, 'parent-packagesets', |
152 | 'Parent packagesets') |
153 | |
154 | def test_parent_packagesets_localpackagediffs_sorts(self): |
155 | @@ -497,22 +499,25 @@ |
156 | ds_diff.derived_series, |
157 | '+localpackagediffs', |
158 | principal=self.simple_user) |
159 | - html_text = view() |
160 | + html = view() |
161 | |
162 | packageset_text = re.compile( |
163 | '\s*' + ', '.join(sorted(unsorted_names))) |
164 | self._test_packagesets( |
165 | - html_text, packageset_text, 'parent-packagesets', |
166 | - 'Parent packagesets') |
167 | + html, packageset_text, 'parent-packagesets', 'Parent packagesets') |
168 | |
169 | def test_queries(self): |
170 | # With no DistroSeriesDifferences the query count should be low and |
171 | # fairly static. However, with some DistroSeriesDifferences the query |
172 | # count will be higher, but it should remain the same no matter how |
173 | # many differences there are. |
174 | - login_person(self.simple_user) |
175 | derived_series = self.factory.makeDistroSeries( |
176 | parent_series=self.factory.makeDistroSeries()) |
177 | + ArchivePermission( |
178 | + archive=derived_series.main_archive, person=self.simple_user, |
179 | + component=getUtility(IComponentSet)["main"], |
180 | + permission=ArchivePermissionType.QUEUE_ADMIN) |
181 | + login_person(self.simple_user) |
182 | |
183 | def add_differences(num): |
184 | for index in xrange(num): |
185 | @@ -1007,12 +1012,14 @@ |
186 | [resolved_diff], filtered_view.cached_differences.batch) |
187 | |
188 | def _setUpDSD(self, src_name='src-name', versions=None, |
189 | - difference_type=None): |
190 | + difference_type=None, distribution=None): |
191 | # Helper to create a derived series with fixed names and proper |
192 | # source package format selection along with a DSD. |
193 | parent_series = self.factory.makeDistroSeries(name='warty') |
194 | + if distribution == None: |
195 | + distribution = self.factory.makeDistribution('deribuntu') |
196 | derived_series = self.factory.makeDistroSeries( |
197 | - distribution=self.factory.makeDistribution(name='deribuntu'), |
198 | + distribution=distribution, |
199 | name='derilucid', parent_series=parent_series) |
200 | self._set_source_selection(derived_series) |
201 | self.factory.makeDistroSeriesDifference( |
202 | @@ -1024,33 +1031,45 @@ |
203 | set_derived_series_ui_feature_flag(self) |
204 | return derived_series, parent_series, sourcepackagename |
205 | |
206 | - def test_canPerformSync_non_editor(self): |
207 | - # Non-editors do not see options to sync. |
208 | - derived_series = self.factory.makeDistroSeries( |
209 | - name='derilucid', parent_series=self.factory.makeDistroSeries( |
210 | - name='lucid')) |
211 | - self.factory.makeDistroSeriesDifference( |
212 | - derived_series=derived_series) |
213 | - |
214 | - set_derived_series_ui_feature_flag(self) |
215 | + def test_canPerformSync_anon(self): |
216 | + # Anonymous users cannot sync packages. |
217 | + derived_series = self._setUpDSD()[0] |
218 | + view = create_initialized_view( |
219 | + derived_series, '+localpackagediffs') |
220 | + |
221 | + self.assertFalse(view.canPerformSync()) |
222 | + |
223 | + def test_canPerformSync_non_anon_no_perm_dest_archive(self): |
224 | + # Logged-in users with no permission on the destination archive |
225 | + # are not presented with options to perform syncs. |
226 | + derived_series = self._setUpDSD()[0] |
227 | with person_logged_in(self.factory.makePerson()): |
228 | view = create_initialized_view( |
229 | derived_series, '+localpackagediffs') |
230 | |
231 | - self.assertFalse(view.canPerformSync()) |
232 | - |
233 | - def test_canPerformSync_editor(self): |
234 | - # Editors are presented with options to perform syncs. |
235 | - derived_series = self.factory.makeDistroSeries( |
236 | - name='derilucid', parent_series=self.factory.makeDistroSeries( |
237 | - name='lucid')) |
238 | - self.factory.makeDistroSeriesDifference( |
239 | - derived_series=derived_series) |
240 | - |
241 | - set_derived_series_ui_feature_flag(self) |
242 | - with person_logged_in(derived_series.owner): |
243 | + self.assertFalse(view.canPerformSync()) |
244 | + |
245 | + def _setUpPersonWithPerm(self, derived_series): |
246 | + # Helper to create a person with an upload permission on the |
247 | + # series' archive. |
248 | + person = self.factory.makePerson() |
249 | + ArchivePermission( |
250 | + archive=derived_series.main_archive, person=person, |
251 | + component=getUtility(IComponentSet)["main"], |
252 | + permission=ArchivePermissionType.QUEUE_ADMIN) |
253 | + return person |
254 | + |
255 | + def test_canPerformSync_non_anon(self): |
256 | + # Logged-in users with a permission on the destination archive |
257 | + # are presented with options to perform syncs. |
258 | + # Note that a more fine-grained perm check is done on each |
259 | + # synced package. |
260 | + derived_series = self._setUpDSD()[0] |
261 | + person = self._setUpPersonWithPerm(derived_series) |
262 | + with person_logged_in(person): |
263 | view = create_initialized_view( |
264 | derived_series, '+localpackagediffs') |
265 | + |
266 | self.assertTrue(view.canPerformSync()) |
267 | |
268 | def _syncAndGetView(self, derived_series, person, sync_differences, |
269 | @@ -1065,47 +1084,87 @@ |
270 | }) |
271 | return view |
272 | |
273 | - def test_sync_notification_on_success(self): |
274 | - # Syncing one or more diffs results in a stub notification. |
275 | - versions = { |
276 | - 'base': '1.0', |
277 | - 'derived': '1.0derived1', |
278 | - 'parent': '1.0-1', |
279 | - } |
280 | - parent_series = self.factory.makeDistroSeries(name='warty') |
281 | - derived_distro = self.factory.makeDistribution(name='deribuntu') |
282 | - derived_series = self.factory.makeDistroSeries( |
283 | - distribution=derived_distro, name='derilucid', |
284 | - parent_series=parent_series) |
285 | - self._set_source_selection(derived_series) |
286 | - difference = self.factory.makeDistroSeriesDifference( |
287 | - source_package_name_str='my-src-name', |
288 | - derived_series=derived_series, versions=versions) |
289 | - |
290 | - # The inital state is that 1.0-1 is not in the derived series. |
291 | - pubs = derived_series.main_archive.getPublishedSources( |
292 | - name='my-src-name', version=versions['parent'], |
293 | - distroseries=derived_series).any() |
294 | - self.assertIs(None, pubs) |
295 | - |
296 | - # Now, sync the source from the parent using the form. |
297 | - set_derived_series_ui_feature_flag(self) |
298 | - with person_logged_in(derived_series.owner): |
299 | - view = create_initialized_view( |
300 | - derived_series, '+localpackagediffs', |
301 | - method='POST', form={ |
302 | - 'field.selected_differences': [ |
303 | - difference.source_package_name.name, |
304 | - ], |
305 | - 'field.actions.sync': 'Sync', |
306 | - }) |
307 | - |
308 | - # The parent's version should now be in the derived series: |
309 | - pub = derived_series.main_archive.getPublishedSources( |
310 | - name='my-src-name', version=versions['parent'], |
311 | - distroseries=derived_series).one() |
312 | + def test_sync_error_nothing_selected(self): |
313 | + # An error is raised when a sync is requested without any selection. |
314 | + derived_series = self._setUpDSD()[0] |
315 | + person = self._setUpPersonWithPerm(derived_series) |
316 | + view = self._syncAndGetView(derived_series, person, []) |
317 | + |
318 | + self.assertEqual(1, len(view.errors)) |
319 | + self.assertEqual( |
320 | + 'No differences selected.', view.errors[0]) |
321 | + |
322 | + def test_sync_error_invalid_selection(self): |
323 | + # An error is raised when an invalid difference is selected. |
324 | + derived_series = self._setUpDSD('my-src-name')[0] |
325 | + person = self._setUpPersonWithPerm(derived_series) |
326 | + view = self._syncAndGetView( |
327 | + derived_series, person, ['some-other-name']) |
328 | + |
329 | + self.assertEqual(2, len(view.errors)) |
330 | + self.assertEqual( |
331 | + 'No differences selected.', view.errors[0]) |
332 | + self.assertEqual( |
333 | + 'Invalid value', view.errors[1].error_name) |
334 | + |
335 | + def test_sync_error_no_perm_dest_archive(self): |
336 | + # A user without upload rights on the destination archive cannot |
337 | + # sync packages. |
338 | + derived_series = self._setUpDSD('my-src-name')[0] |
339 | + person = self._setUpPersonWithPerm(derived_series) |
340 | + view = self._syncAndGetView( |
341 | + derived_series, person, ['my-src-name']) |
342 | + |
343 | + self.assertEqual(1, len(view.errors)) |
344 | + self.assertTrue( |
345 | + "The signer of this package has no upload rights to this " |
346 | + "distribution's primary archive" in view.errors[0]) |
347 | + |
348 | + def makePersonWithComponentPermission(self, archive, component=None): |
349 | + person = self.factory.makePerson() |
350 | + if component is None: |
351 | + component = self.factory.makeComponent() |
352 | + removeSecurityProxy(archive).newComponentUploader( |
353 | + person, component) |
354 | + return person, component |
355 | + |
356 | + def test_sync_success_perm_component(self): |
357 | + # A user with upload rights on the destination component |
358 | + # can sync packages. |
359 | + derived_series, parent_series, sourcepackagename = self._setUpDSD( |
360 | + 'my-src-name') |
361 | + person, _ = self.makePersonWithComponentPermission( |
362 | + derived_series.main_archive, |
363 | + derived_series.getSourcePackage( |
364 | + sourcepackagename).latest_published_component) |
365 | + view = self._syncAndGetView( |
366 | + derived_series, person, ['my-src-name']) |
367 | + |
368 | + self.assertEqual(0, len(view.errors)) |
369 | + |
370 | + def test_sync_error_no_perm_component(self): |
371 | + # A user without upload rights on the destination component |
372 | + # will get an error when he syncs packages to this component. |
373 | + derived_series, parent_series, sourcepackagename = self._setUpDSD( |
374 | + 'my-src-name') |
375 | + person, another_component = self.makePersonWithComponentPermission( |
376 | + derived_series.main_archive) |
377 | + view = self._syncAndGetView( |
378 | + derived_series, person, ['my-src-name']) |
379 | + |
380 | + self.assertEqual(1, len(view.errors)) |
381 | + self.assertTrue( |
382 | + "Signer is not permitted to upload to the " |
383 | + "component" in view.errors[0]) |
384 | + |
385 | + def assertPackageCopied(self, series, src_name, version, view): |
386 | + # Helper to check that a package has been copied. |
387 | + # The new version should now be in the destination series. |
388 | + pub = series.main_archive.getPublishedSources( |
389 | + name=src_name, version=version, |
390 | + distroseries=series).one() |
391 | self.assertIsNot(None, pub) |
392 | - self.assertEqual(versions['parent'], pub.sourcepackagerelease.version) |
393 | + self.assertEqual(version, pub.sourcepackagerelease.version) |
394 | |
395 | # The view should show no errors, and the notification should |
396 | # confirm the sync worked. |
397 | @@ -1114,87 +1173,90 @@ |
398 | self.assertEqual(1, len(notifications)) |
399 | self.assertEqual( |
400 | '<p>Packages copied to ' |
401 | - '<a href="http://launchpad.dev/deribuntu/derilucid"' |
402 | + '<a href="http://launchpad.dev/%s/%s"' |
403 | '>Derilucid</a>:</p>\n<ul>\n<li>my-src-name 1.0-1 in ' |
404 | - 'derilucid</li>\n</ul>', |
405 | + 'derilucid</li>\n</ul>' % (series.parent.name, series.name), |
406 | notifications[0].message) |
407 | # 302 is a redirect back to the same page. |
408 | self.assertEqual(302, view.request.response.getStatus()) |
409 | |
410 | - def test_sync_error_nothing_selected(self): |
411 | - # An error is raised when a sync is requested without any selection. |
412 | - derived_series = self.factory.makeDistroSeries( |
413 | - name='derilucid', parent_series=self.factory.makeDistroSeries( |
414 | - name='lucid')) |
415 | - self.factory.makeDistroSeriesDifference( |
416 | - source_package_name_str='my-src-name', |
417 | - derived_series=derived_series) |
418 | - |
419 | - set_derived_series_ui_feature_flag(self) |
420 | - with person_logged_in(derived_series.owner): |
421 | - view = create_initialized_view( |
422 | - derived_series, '+localpackagediffs', |
423 | - method='POST', form={ |
424 | - 'field.selected_differences': [], |
425 | - 'field.actions.sync': 'Sync', |
426 | - }) |
427 | - |
428 | - self.assertEqual(1, len(view.errors)) |
429 | - self.assertEqual( |
430 | - 'No differences selected.', view.errors[0]) |
431 | - |
432 | - def test_sync_error_invalid_selection(self): |
433 | - # An error is raised when an invalid difference is selected. |
434 | - derived_series = self.factory.makeDistroSeries( |
435 | - name='derilucid', parent_series=self.factory.makeDistroSeries( |
436 | - name='lucid')) |
437 | - self._set_source_selection(derived_series) |
438 | - self.factory.makeDistroSeriesDifference( |
439 | - source_package_name_str='my-src-name', |
440 | - derived_series=derived_series) |
441 | - |
442 | - set_derived_series_ui_feature_flag(self) |
443 | - with person_logged_in(derived_series.owner): |
444 | - view = create_initialized_view( |
445 | - derived_series, '+localpackagediffs', |
446 | - method='POST', form={ |
447 | - 'field.selected_differences': ['some-other-name'], |
448 | - 'field.actions.sync': 'Sync', |
449 | - }) |
450 | - |
451 | - self.assertEqual(2, len(view.errors)) |
452 | - self.assertEqual( |
453 | - 'No differences selected.', view.errors[0]) |
454 | - self.assertEqual( |
455 | - 'Invalid value', view.errors[1].error_name) |
456 | - |
457 | - def test_sync_in_released_series_in_updates(self): |
458 | - # If the destination series is released, the sync packages end |
459 | - # up in the updates pocket. |
460 | + def test_sync_notification_on_success(self): |
461 | + # A user with upload rights on the destination archive can |
462 | + # sync packages. Notifications about the synced packages are |
463 | + # displayed and the packages are copied inside the destination |
464 | + # series. |
465 | versions = { |
466 | + 'base': '1.0', |
467 | + 'derived': '1.0derived1', |
468 | 'parent': '1.0-1', |
469 | } |
470 | derived_series, parent_series, sourcepackagename = self._setUpDSD( |
471 | 'my-src-name', versions=versions) |
472 | |
473 | - # Update destination series status to current and update |
474 | - # daterelease. |
475 | - with celebrity_logged_in('admin'): |
476 | - derived_series.status = SeriesStatus.CURRENT |
477 | - derived_series.datereleased = UTC_NOW |
478 | - |
479 | - self._syncAndGetView( |
480 | - derived_series, derived_series.owner, ['my-src-name']) |
481 | - |
482 | - parent_pub = parent_series.main_archive.getPublishedSources( |
483 | - name='my-src-name', version=versions['parent'], |
484 | - distroseries=parent_series).one() |
485 | - pub = derived_series.main_archive.getPublishedSources( |
486 | - name='my-src-name', version=versions['parent'], |
487 | - distroseries=derived_series).one() |
488 | - |
489 | - self.assertEqual(self.factory.getAnyPocket(), parent_pub.pocket) |
490 | - self.assertEqual(PackagePublishingPocket.UPDATES, pub.pocket) |
491 | + # Setup a user with upload rights. |
492 | + person = self.factory.makePerson() |
493 | + removeSecurityProxy(derived_series.main_archive).newPackageUploader( |
494 | + person, sourcepackagename) |
495 | + |
496 | + # The inital state is that 1.0-1 is not in the derived series. |
497 | + pubs = derived_series.main_archive.getPublishedSources( |
498 | + name='my-src-name', version=versions['parent'], |
499 | + distroseries=derived_series).any() |
500 | + self.assertIs(None, pubs) |
501 | + |
502 | + # Now, sync the source from the parent using the form. |
503 | + view = self._syncAndGetView( |
504 | + derived_series, person, ['my-src-name']) |
505 | + |
506 | + # The parent's version should now be in the derived series and |
507 | + # the notifications displayed: |
508 | + self.assertPackageCopied( |
509 | + derived_series, 'my-src-name', versions['parent'], view) |
510 | + |
511 | + def test_sync_success_not_yet_in_derived_series(self): |
512 | + # If the package to sync does not exist yet in the derived series, |
513 | + # upload right to any component inside the destination series will be |
514 | + # enough to sync the package. |
515 | + versions = { |
516 | + 'parent': '1.0-1', |
517 | + } |
518 | + missing = DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES |
519 | + derived_series, parent_series, sourcepackagename = self._setUpDSD( |
520 | + 'my-src-name', difference_type=missing, versions=versions) |
521 | + person, another_component = self.makePersonWithComponentPermission( |
522 | + derived_series.main_archive) |
523 | + view = self._syncAndGetView( |
524 | + derived_series, person, ['my-src-name'], |
525 | + view_name='+missingpackages') |
526 | + |
527 | + self.assertPackageCopied( |
528 | + derived_series, 'my-src-name', versions['parent'], view) |
529 | + |
530 | + def test_sync_append_main_archive(self): |
531 | + # A user with lp.Append on the main archive (e.g. members of |
532 | + # ubuntu-security on an ubuntu series) can sync packages. |
533 | + # XXX: rvb 2011-05-05 bug=777911: This check should be refactored |
534 | + # and moved to lib/lp/soyuz/scripts/tests/test_copypackage.py. |
535 | + versions = { |
536 | + 'base': '1.0', |
537 | + 'derived': '1.0derived1', |
538 | + 'parent': '1.0-1', |
539 | + } |
540 | + ubuntu = getUtility(ILaunchpadCelebrities).ubuntu |
541 | + derived_series, parent_series, sourcepackagename = self._setUpDSD( |
542 | + 'my-src-name', distribution=ubuntu, versions=versions) |
543 | + ubuntu_security = getUtility(IPersonSet).getByName('ubuntu-security') |
544 | + with person_logged_in(ubuntu_security): |
545 | + self.assertTrue( |
546 | + check_permission( |
547 | + 'launchpad.Append', derived_series.main_archive)) |
548 | + view = self._syncAndGetView( |
549 | + derived_series, ubuntu_security, ['my-src-name']) |
550 | + |
551 | + self.assertTrue(view.canPerformSync()) |
552 | + |
553 | + self.assertPackageCopied( |
554 | + derived_series, 'my-src-name', versions['parent'], view) |
555 | |
556 | |
557 | class TestDistroSeriesNeedsPackagesView(TestCaseWithFactory): |
558 | @@ -1291,12 +1353,11 @@ |
559 | self.ds_diff.derived_series, |
560 | '+missingpackages', |
561 | principal=self.simple_user) |
562 | - html_text = view() |
563 | + html = view() |
564 | |
565 | packageset_text = re.compile('\s*' + ps.name) |
566 | self._test_packagesets( |
567 | - html_text, packageset_text, 'parent-packagesets', |
568 | - 'Parent packagesets') |
569 | + html, packageset_text, 'parent-packagesets', 'Parent packagesets') |
570 | |
571 | |
572 | class DistroSerieUniquePackageDiffsTestCase(TestCaseWithFactory): |
573 | @@ -1378,8 +1439,8 @@ |
574 | self.ds_diff.derived_series, |
575 | '+uniquepackages', |
576 | principal=self.simple_user) |
577 | - html_text = view() |
578 | + html = view() |
579 | |
580 | packageset_text = re.compile('\s*' + ps.name) |
581 | self._test_packagesets( |
582 | - html_text, packageset_text, 'packagesets', 'Packagesets') |
583 | + html, packageset_text, 'packagesets', 'Packagesets') |
584 | |
585 | === modified file 'lib/lp/registry/templates/distroseries-localdifferences.pt' |
586 | --- lib/lp/registry/templates/distroseries-localdifferences.pt 2011-05-05 00:47:27 +0000 |
587 | +++ lib/lp/registry/templates/distroseries-localdifferences.pt 2011-05-05 20:54:43 +0000 |
588 | @@ -23,7 +23,8 @@ |
589 | <div class="top-portlet" metal:fill-slot="main" |
590 | tal:define="differences view/cached_differences; |
591 | series_name context/displayname; |
592 | - parent_name context/parent_series/displayname;"> |
593 | + parent_name context/parent_series/displayname; |
594 | + can_perform_sync view/canPerformSync;"> |
595 | <p><tal:replace replace="structure view/explanation/escapedtext" /></p> |
596 | |
597 | <tal:distroseries_localdiff_search_form |
598 | @@ -64,7 +65,7 @@ |
599 | tal:attributes="class src_name"> |
600 | |
601 | <td> |
602 | - <input tal:condition="view/canPerformSync" |
603 | + <input tal:condition="can_perform_sync" |
604 | name="field.selected_differences" type="checkbox" |
605 | tal:attributes=" |
606 | value src_name; |
607 | |
608 | === modified file 'lib/lp/soyuz/browser/archive.py' |
609 | --- lib/lp/soyuz/browser/archive.py 2011-05-05 00:47:27 +0000 |
610 | +++ lib/lp/soyuz/browser/archive.py 2011-05-05 20:54:43 +0000 |
611 | @@ -1219,7 +1219,8 @@ |
612 | |
613 | def do_copy(self, sources_field_name, source_pubs, dest_archive, |
614 | dest_series, dest_pocket, include_binaries, |
615 | - dest_url=None, dest_display_name=None): |
616 | + dest_url=None, dest_display_name=None, person=None, |
617 | + check_permissions=True): |
618 | """Copy packages and add appropriate feedback to the browser page. |
619 | |
620 | :param sources_field_name: The name of the form field to set errors |
621 | @@ -1236,13 +1237,17 @@ |
622 | :param dest_display_name: The text to use for the dest_url link. |
623 | Defaults to the target archive's display name and will be |
624 | automatically escaped for inclusion in the output. |
625 | + :param person: The person requesting the copy. |
626 | + :param: check_permissions: boolean indicating whether or not the |
627 | + requester's permissions to copy should be checked. |
628 | |
629 | :return: True if the copying worked, False otherwise. |
630 | """ |
631 | try: |
632 | copies = do_copy( |
633 | source_pubs, dest_archive, dest_series, |
634 | - dest_pocket, include_binaries) |
635 | + dest_pocket, include_binaries, allow_delayed_copies=True, |
636 | + person=person, check_permissions=check_permissions) |
637 | except CannotCopy, error: |
638 | messages = [] |
639 | error_lines = str(error).splitlines() |
640 | @@ -1462,7 +1467,8 @@ |
641 | # setting up on-page notifications. |
642 | if self.do_copy( |
643 | 'selected_sources', selected_sources, destination_archive, |
644 | - destination_series, destination_pocket, include_binaries): |
645 | + destination_series, destination_pocket, include_binaries, |
646 | + person=self.user): |
647 | # The copy worked so we can redirect back to the page to |
648 | # show the result. |
649 | self.setNextURL() |
650 | |
651 | === modified file 'lib/lp/soyuz/doc/archive.txt' |
652 | --- lib/lp/soyuz/doc/archive.txt 2011-05-05 00:47:27 +0000 |
653 | +++ lib/lp/soyuz/doc/archive.txt 2011-05-05 20:54:43 +0000 |
654 | @@ -2275,7 +2275,8 @@ |
655 | release pocket, but to do so we must have edit permissions on the archive. |
656 | |
657 | >>> sources = ["package1", "package2"] |
658 | - >>> mark.archive.syncSources(sources, cprov.archive, "release") |
659 | + >>> mark.archive.syncSources( |
660 | + ... sources, cprov.archive, "release", person=None) |
661 | Traceback (most recent call last): |
662 | ... |
663 | Unauthorized... |
664 | @@ -2284,7 +2285,8 @@ |
665 | |
666 | >>> login("mark@example.com") |
667 | |
668 | - >>> mark.archive.syncSources(sources, cprov.archive, "release") |
669 | + >>> mark.archive.syncSources( |
670 | + ... sources, cprov.archive, "release", person=mark) |
671 | |
672 | >>> mark_one = mark.archive.getPublishedSources(name="package1").one() |
673 | >>> print mark_one.sourcepackagerelease.version |
674 | @@ -2298,7 +2300,8 @@ |
675 | |
676 | Repeating this source copy gives an error: |
677 | |
678 | - >>> mark.archive.syncSources(sources, cprov.archive, "release") |
679 | + >>> mark.archive.syncSources( |
680 | + ... sources, cprov.archive, "release", person=mark) |
681 | Traceback (most recent call last): |
682 | ... |
683 | CannotCopy: |
684 | @@ -2310,7 +2313,8 @@ |
685 | Repeating this copy with binaries also gives an error: |
686 | |
687 | >>> mark.archive.syncSources( |
688 | - ... sources, cprov.archive, "release", include_binaries=True) |
689 | + ... sources, cprov.archive, "release", include_binaries=True, |
690 | + ... person=mark) |
691 | Traceback (most recent call last): |
692 | ... |
693 | CannotCopy: |
694 | @@ -2320,18 +2324,21 @@ |
695 | Specifying non-existent source names, pocket names or distroseries names |
696 | all result in a NotFound exception: |
697 | |
698 | - >>> mark.archive.syncSources(["bogus"], cprov.archive, "release") |
699 | + >>> mark.archive.syncSources(["bogus"], cprov.archive, "release", |
700 | + ... person=mark) |
701 | Traceback (most recent call last): |
702 | ... |
703 | NoSuchSourcePackageName: No such source package: 'bogus'. |
704 | |
705 | - >>> mark.archive.syncSources(sources, cprov.archive, "badpocket") |
706 | + >>> mark.archive.syncSources(sources, cprov.archive, "badpocket", |
707 | + ... person=mark) |
708 | Traceback (most recent call last): |
709 | ... |
710 | PocketNotFound: 'BADPOCKET' |
711 | |
712 | >>> mark.archive.syncSources( |
713 | - ... sources, cprov.archive, "release", to_series="badseries") |
714 | + ... sources, cprov.archive, "release", to_series="badseries", |
715 | + ... person=mark) |
716 | Traceback (most recent call last): |
717 | ... |
718 | DistroSeriesNotFound: badseries |
719 | @@ -2360,7 +2367,8 @@ |
720 | As with syncSources() you need to have edit permission on the archive. |
721 | |
722 | >>> login(ANONYMOUS) |
723 | - >>> mark.archive.syncSource("pack", "1.0", cprov.archive, "release") |
724 | + >>> mark.archive.syncSource("pack", "1.0", cprov.archive, "release", |
725 | + ... person=None) |
726 | Traceback (most recent call last): |
727 | ... |
728 | Unauthorized... |
729 | @@ -2368,7 +2376,8 @@ |
730 | Login as mark to continue. |
731 | |
732 | >>> login("mark@example.com") |
733 | - >>> mark.archive.syncSource("pack", "1.0", cprov.archive, "release") |
734 | + >>> mark.archive.syncSource("pack", "1.0", cprov.archive, "release", |
735 | + ... person=mark) |
736 | >>> pack = mark.archive.getPublishedSources( |
737 | ... name="pack", exact_match=True).one() |
738 | >>> print pack.sourcepackagerelease.version |
739 | @@ -2376,7 +2385,8 @@ |
740 | |
741 | Copy package3 1.0 explicitly: |
742 | |
743 | - >>> mark.archive.syncSource("package3", "1.0", cprov.archive, "release") |
744 | + >>> mark.archive.syncSource("package3", "1.0", cprov.archive, |
745 | + ... "release", person=mark) |
746 | >>> mark_three = mark.archive.getPublishedSources(name="package3").one() |
747 | >>> print mark_three.sourcepackagerelease.version |
748 | 1.0 |
749 | @@ -2403,7 +2413,7 @@ |
750 | |
751 | >>> mark.archive.syncSource( |
752 | ... "built-source", "1.0", cprov.archive, "release", |
753 | - ... include_binaries=True) |
754 | + ... include_binaries=True, person=mark) |
755 | |
756 | Now, Mark's PPA has 'built-source' source and it's 2 binaries. |
757 | |
758 | @@ -2415,7 +2425,7 @@ |
759 | or a CannotCopy exception is thrown. |
760 | |
761 | >>> mark.archive.syncSource( |
762 | - ... "package3", "1.2", cprov.archive, "updates") |
763 | + ... "package3", "1.2", cprov.archive, "updates", person=mark) |
764 | Traceback (most recent call last): |
765 | ... |
766 | CannotCopy: Destination pocket must be 'release' for a PPA. |
767 | @@ -2446,7 +2456,7 @@ |
768 | |
769 | >>> mark.archive.syncSource( |
770 | ... source_name='overridden', version='1.0', |
771 | - ... from_archive=source_archive, to_pocket='release') |
772 | + ... from_archive=source_archive, to_pocket='release', person=mark) |
773 | |
774 | >>> copy = mark.archive.getPublishedSources(name="overridden").one() |
775 | >>> print copy.section.name |
776 | |
777 | === modified file 'lib/lp/soyuz/interfaces/archive.py' |
778 | --- lib/lp/soyuz/interfaces/archive.py 2011-05-05 00:47:27 +0000 |
779 | +++ lib/lp/soyuz/interfaces/archive.py 2011-05-05 20:54:43 +0000 |
780 | @@ -616,6 +616,9 @@ |
781 | :param component: The `Component` being uploaded to. |
782 | :param pocket: The `PackagePublishingPocket` of 'distroseries' being |
783 | uploaded to. |
784 | + :param strict_component: True if access to the specific component for |
785 | + the package is needed to upload to it. If False, then access to |
786 | + any component will do. |
787 | :return: The reason for not being able to upload, None otherwise. |
788 | """ |
789 | |
790 | @@ -632,7 +635,7 @@ |
791 | :param distroseries: The upload's target distro series. |
792 | :param strict_component: True if access to the specific component for |
793 | the package is needed to upload to it. If False, then access to |
794 | - any package will do. |
795 | + any component will do. |
796 | :return: CannotUploadToArchive if 'person' cannot upload to the |
797 | archive, |
798 | None otherwise. |
799 | @@ -1173,12 +1176,22 @@ |
800 | @operation_returns_collection_of(Interface) |
801 | @export_read_operation() |
802 | def getComponentsForQueueAdmin(person): |
803 | - """Return `IArchivePermission` for the person's queue admin components |
804 | + """Return `IArchivePermission` for the person's queue admin |
805 | + components. |
806 | |
807 | - :param person: An `IPerson` |
808 | + :param person: An `IPerson`. |
809 | :return: A list of `IArchivePermission` records. |
810 | """ |
811 | |
812 | + def hasAnyPermission(person): |
813 | + """Whether or not this person has any permission at all on this |
814 | + archive. |
815 | + |
816 | + :param person: The `IPerson` for whom the check is performed. |
817 | + :return: A boolean indicating if the person has any permission on this |
818 | + archive at all. |
819 | + """ |
820 | + |
821 | def getPackageDownloadCount(bpr, day, country): |
822 | """Get the `IBinaryPackageDownloadCount` with the given key.""" |
823 | |
824 | @@ -1212,6 +1225,7 @@ |
825 | class IArchiveAppend(Interface): |
826 | """Archive interface for operations restricted by append privilege.""" |
827 | |
828 | + @call_with(person=REQUEST_USER) |
829 | @operation_parameters( |
830 | source_names=List( |
831 | title=_("Source package names"), |
832 | @@ -1227,8 +1241,8 @@ |
833 | @export_write_operation() |
834 | # Source_names is a string because exporting a SourcePackageName is |
835 | # rather nonsensical as it only has id and name columns. |
836 | - def syncSources(source_names, from_archive, to_pocket, |
837 | - to_series=None, include_binaries=False): |
838 | + def syncSources(source_names, from_archive, to_pocket, to_series=None, |
839 | + include_binaries=False, person=None): |
840 | """Synchronise (copy) named sources into this archive from another. |
841 | |
842 | It will copy the most recent PUBLISHED versions of the named |
843 | @@ -1246,6 +1260,7 @@ |
844 | :param include_binaries: optional boolean, controls whether or not |
845 | the published binaries for each given source should also be |
846 | copied along with the source. |
847 | + :param person: the `IPerson` who requests the sync. |
848 | |
849 | :raises NoSuchSourcePackageName: if the source name is invalid |
850 | :raises PocketNotFound: if the pocket name is invalid |
851 | @@ -1253,6 +1268,7 @@ |
852 | :raises CannotCopy: if there is a problem copying. |
853 | """ |
854 | |
855 | + @call_with(person=REQUEST_USER) |
856 | @operation_parameters( |
857 | source_name=TextLine(title=_("Source package name")), |
858 | version=TextLine(title=_("Version")), |
859 | @@ -1271,7 +1287,7 @@ |
860 | # we should consider either changing this method or adding a new one |
861 | # that takes that object instead. |
862 | def syncSource(source_name, version, from_archive, to_pocket, |
863 | - to_series=None, include_binaries=False): |
864 | + to_series=None, include_binaries=False, person=None): |
865 | """Synchronise (copy) a single named source into this archive. |
866 | |
867 | Copy a specific version of a named source to the destination |
868 | @@ -1285,6 +1301,7 @@ |
869 | :param include_binaries: optional boolean, controls whether or not |
870 | the published binaries for each given source should also be |
871 | copied along with the source. |
872 | + :param person: the `IPerson` who requests the sync. |
873 | |
874 | :raises NoSuchSourcePackageName: if the source name is invalid |
875 | :raises PocketNotFound: if the pocket name is invalid |
876 | |
877 | === modified file 'lib/lp/soyuz/model/archive.py' |
878 | --- lib/lp/soyuz/model/archive.py 2011-05-05 00:47:27 +0000 |
879 | +++ lib/lp/soyuz/model/archive.py 2011-05-05 20:54:43 +0000 |
880 | @@ -28,8 +28,8 @@ |
881 | Desc, |
882 | Or, |
883 | Select, |
884 | + SQL, |
885 | Sum, |
886 | - SQL, |
887 | ) |
888 | from storm.locals import ( |
889 | Count, |
890 | @@ -1001,6 +1001,19 @@ |
891 | permission_set = getUtility(IArchivePermissionSet) |
892 | return permission_set.componentsForQueueAdmin(self, person) |
893 | |
894 | + def hasAnyPermission(self, person): |
895 | + """See `IArchive`.""" |
896 | + # Avoiding circular imports. |
897 | + from lp.soyuz.model.archivepermission import ArchivePermission |
898 | + |
899 | + any_perm_on_archive = Store.of(self).find( |
900 | + TeamParticipation, |
901 | + ArchivePermission.archive == self.id, |
902 | + TeamParticipation.person == person.id, |
903 | + TeamParticipation.teamID == ArchivePermission.personID, |
904 | + ) |
905 | + return not any_perm_on_archive.is_empty() |
906 | + |
907 | def getBuildCounters(self, include_needsbuild=True): |
908 | """See `IArchiveSet`.""" |
909 | |
910 | @@ -1176,7 +1189,7 @@ |
911 | strict_component) |
912 | |
913 | def verifyUpload(self, person, sourcepackagename, component, |
914 | - distroseries, strict_component=True): |
915 | + distroseries, strict_component=True): |
916 | """See `IArchive`.""" |
917 | if not self.enabled: |
918 | return ArchiveDisabled(self.displayname) |
919 | @@ -1435,15 +1448,17 @@ |
920 | reason) |
921 | |
922 | def syncSources(self, source_names, from_archive, to_pocket, |
923 | - to_series=None, include_binaries=False): |
924 | + to_series=None, include_binaries=False, person=None): |
925 | """See `IArchive`.""" |
926 | # Find and validate the source package names in source_names. |
927 | sources = self._collectLatestPublishedSources( |
928 | from_archive, source_names) |
929 | - self._copySources(sources, to_pocket, to_series, include_binaries) |
930 | + self._copySources( |
931 | + sources, to_pocket, to_series, include_binaries, |
932 | + person=person) |
933 | |
934 | def syncSource(self, source_name, version, from_archive, to_pocket, |
935 | - to_series=None, include_binaries=False): |
936 | + to_series=None, include_binaries=False, person=None): |
937 | """See `IArchive`.""" |
938 | # Check to see if the source package exists, and raise a useful error |
939 | # if it doesn't. |
940 | @@ -1452,7 +1467,9 @@ |
941 | source = from_archive.getPublishedSources( |
942 | name=source_name, version=version, exact_match=True).first() |
943 | |
944 | - self._copySources([source], to_pocket, to_series, include_binaries) |
945 | + self._copySources( |
946 | + [source], to_pocket, to_series, include_binaries, |
947 | + person=person) |
948 | |
949 | def _collectLatestPublishedSources(self, from_archive, source_names): |
950 | """Private helper to collect the latest published sources for an |
951 | @@ -1478,7 +1495,7 @@ |
952 | return sources |
953 | |
954 | def _copySources(self, sources, to_pocket, to_series=None, |
955 | - include_binaries=False): |
956 | + include_binaries=False, person=None): |
957 | """Private helper function to copy sources to this archive. |
958 | |
959 | It takes a list of SourcePackagePublishingHistory but the other args |
960 | @@ -1507,7 +1524,8 @@ |
961 | series = None |
962 | |
963 | # Perform the copy, may raise CannotCopy. |
964 | - do_copy(sources, self, series, pocket, include_binaries) |
965 | + do_copy( |
966 | + sources, self, series, pocket, include_binaries, person=person) |
967 | |
968 | def getAuthToken(self, person): |
969 | """See `IArchive`.""" |
970 | |
971 | === modified file 'lib/lp/soyuz/model/packagecopyjob.py' |
972 | --- lib/lp/soyuz/model/packagecopyjob.py 2011-05-05 00:47:27 +0000 |
973 | +++ lib/lp/soyuz/model/packagecopyjob.py 2011-05-05 20:54:43 +0000 |
974 | @@ -127,4 +127,4 @@ |
975 | do_copy( |
976 | sources=source_packages, archive=self.target_archive, |
977 | series=self.target_distroseries, pocket=self.target_pocket, |
978 | - include_binaries=self.include_binaries) |
979 | + include_binaries=self.include_binaries, check_permissions=False) |
980 | |
981 | === modified file 'lib/lp/soyuz/scripts/packagecopier.py' |
982 | --- lib/lp/soyuz/scripts/packagecopier.py 2011-05-05 00:47:27 +0000 |
983 | +++ lib/lp/soyuz/scripts/packagecopier.py 2011-05-05 20:54:43 +0000 |
984 | @@ -24,14 +24,16 @@ |
985 | from zope.component import getUtility |
986 | |
987 | from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet |
988 | +from canonical.launchpad.webapp.authorization import check_permission |
989 | from canonical.librarian.utils import copy_and_close |
990 | from lp.app.errors import NotFoundError |
991 | from lp.buildmaster.enums import BuildStatus |
992 | from lp.soyuz.adapters.packagelocation import build_package_location |
993 | -from lp.soyuz.enums import ArchivePurpose |
994 | -from lp.soyuz.interfaces.archive import ( |
995 | - CannotCopy, |
996 | +from lp.soyuz.enums import ( |
997 | + ArchivePurpose, |
998 | + SourcePackageFormat, |
999 | ) |
1000 | +from lp.soyuz.interfaces.archive import CannotCopy |
1001 | from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus |
1002 | from lp.soyuz.interfaces.publishing import ( |
1003 | active_publishing_status, |
1004 | @@ -44,7 +46,6 @@ |
1005 | IPackageUploadCustom, |
1006 | IPackageUploadSet, |
1007 | ) |
1008 | -from lp.soyuz.enums import SourcePackageFormat |
1009 | from lp.soyuz.scripts.ftpmasterbase import ( |
1010 | SoyuzScript, |
1011 | SoyuzScriptError, |
1012 | @@ -374,7 +375,8 @@ |
1013 | "%s already exists in destination archive with " |
1014 | "different contents." % lf.libraryfile.filename) |
1015 | |
1016 | - def checkCopy(self, source, series, pocket): |
1017 | + def checkCopy(self, source, series, pocket, person=None, |
1018 | + check_permissions=True): |
1019 | """Check if the source can be copied to the given location. |
1020 | |
1021 | Check possible conflicting publications in the destination archive. |
1022 | @@ -384,13 +386,49 @@ |
1023 | higher than any version of the same source present in the |
1024 | destination suite (series + pocket). |
1025 | |
1026 | + If person is not None, check that this person has upload rights to |
1027 | + the destination (archive, component, pocket). |
1028 | + |
1029 | :param source: copy candidate, `ISourcePackagePublishingHistory`. |
1030 | :param series: destination `IDistroSeries`. |
1031 | :param pocket: destination `PackagePublishingPocket`. |
1032 | + :param person: requester `IPerson`. |
1033 | + :param check_permissions: boolean indicating whether or not the |
1034 | + requester's permissions to copy should be checked. |
1035 | |
1036 | :raise CannotCopy when a copy is not allowed to be performed |
1037 | containing the reason of the error. |
1038 | """ |
1039 | + # If there is a requester, check that he has upload permission |
1040 | + # into the destination (archive, component, pocket). This check |
1041 | + # is done here rather than in the security adapter because it |
1042 | + # requires more info than is available in the security adapter. |
1043 | + if check_permissions: |
1044 | + if person is None: |
1045 | + raise CannotCopy( |
1046 | + 'Cannot check copy permissions (no requester).') |
1047 | + else: |
1048 | + sourcepackagerelease = source.sourcepackagerelease |
1049 | + sourcepackagename = sourcepackagerelease.sourcepackagename |
1050 | + destination_component = series.getSourcePackage( |
1051 | + sourcepackagename).latest_published_component |
1052 | + # If destination_component is not None, make sure the person |
1053 | + # has upload permission for this component. Otherwise, any |
1054 | + # upload permission on this archive will do. |
1055 | + strict_component = destination_component is not None |
1056 | + reason = self.archive.checkUpload( |
1057 | + person, series, sourcepackagename, destination_component, |
1058 | + pocket, strict_component=strict_component) |
1059 | + if reason is not None: |
1060 | + # launchpad.Append on the main archive is sufficient |
1061 | + # to copy arbitrary packages. This allows for |
1062 | + # ubuntu's security team to sync sources into the |
1063 | + # primary archive (bypassing the queue and |
1064 | + # annoncements). |
1065 | + if not check_permission( |
1066 | + 'launchpad.Append', series.main_archive): |
1067 | + raise CannotCopy(reason) |
1068 | + |
1069 | if series not in self.archive.distribution.series: |
1070 | raise CannotCopy( |
1071 | "No such distro series %s in distribution %s." % |
1072 | @@ -459,7 +497,7 @@ |
1073 | |
1074 | |
1075 | def do_copy(sources, archive, series, pocket, include_binaries=False, |
1076 | - allow_delayed_copies=True): |
1077 | + allow_delayed_copies=True, person=None, check_permissions=True): |
1078 | """Perform the complete copy of the given sources incrementally. |
1079 | |
1080 | Verifies if each copy can be performed using `CopyChecker` and |
1081 | @@ -470,17 +508,20 @@ |
1082 | |
1083 | Wrapper for `do_direct_copy`. |
1084 | |
1085 | - :param: sources: a list of `ISourcePackagePublishingHistory`. |
1086 | - :param: archive: the target `IArchive`. |
1087 | - :param: series: the target `IDistroSeries`, if None is given the same |
1088 | + :param sources: a list of `ISourcePackagePublishingHistory`. |
1089 | + :param archive: the target `IArchive`. |
1090 | + :param series: the target `IDistroSeries`, if None is given the same |
1091 | current source distroseries will be used as destination. |
1092 | - :param: pocket: the target `PackagePublishingPocket`. |
1093 | - :param: include_binaries: optional boolean, controls whether or |
1094 | + :param pocket: the target `PackagePublishingPocket`. |
1095 | + :param include_binaries: optional boolean, controls whether or |
1096 | not the published binaries for each given source should be also |
1097 | copied along with the source. |
1098 | :param allow_delayed_copies: boolean indicating whether or not private |
1099 | sources can be copied to public archives using delayed_copies. |
1100 | Defaults to True, only set as False in the UnembargoPackage context. |
1101 | + :param person: the requester `IPerson`. |
1102 | + :param check_permissions: boolean indicating whether or not the |
1103 | + requester's permissions to copy should be checked. |
1104 | |
1105 | :raise CannotCopy when one or more copies were not allowed. The error |
1106 | will contain the reason why each copy was denied. |
1107 | @@ -500,7 +541,8 @@ |
1108 | else: |
1109 | destination_series = series |
1110 | try: |
1111 | - copy_checker.checkCopy(source, destination_series, pocket) |
1112 | + copy_checker.checkCopy( |
1113 | + source, destination_series, pocket, person, check_permissions) |
1114 | except CannotCopy, reason: |
1115 | errors.append("%s (%s)" % (source.displayname, reason)) |
1116 | continue |
1117 | @@ -536,12 +578,12 @@ |
1118 | Also copy published binaries for each source if requested to. Again, |
1119 | only copy binaries that were not yet copied before. |
1120 | |
1121 | - :param: source: an `ISourcePackagePublishingHistory`. |
1122 | - :param: archive: the target `IArchive`. |
1123 | - :param: series: the target `IDistroSeries`, if None is given the same |
1124 | + :param source: an `ISourcePackagePublishingHistory`. |
1125 | + :param archive: the target `IArchive`. |
1126 | + :param series: the target `IDistroSeries`, if None is given the same |
1127 | current source distroseries will be used as destination. |
1128 | - :param: pocket: the target `PackagePublishingPocket`. |
1129 | - :param: include_binaries: optional boolean, controls whether or |
1130 | + :param pocket: the target `PackagePublishingPocket`. |
1131 | + :param include_binaries: optional boolean, controls whether or |
1132 | not the published binaries for each given source should be also |
1133 | copied along with the source. |
1134 | |
1135 | @@ -608,11 +650,11 @@ |
1136 | |
1137 | Also include published builds for each source if requested to. |
1138 | |
1139 | - :param: source: an `ISourcePackagePublishingHistory`. |
1140 | - :param: archive: the target `IArchive`. |
1141 | - :param: series: the target `IDistroSeries`. |
1142 | - :param: pocket: the target `PackagePublishingPocket`. |
1143 | - :param: include_binaries: optional boolean, controls whether or |
1144 | + :param source: an `ISourcePackagePublishingHistory`. |
1145 | + :param archive: the target `IArchive`. |
1146 | + :param series: the target `IDistroSeries`. |
1147 | + :param pocket: the target `PackagePublishingPocket`. |
1148 | + :param include_binaries: optional boolean, controls whether or |
1149 | not the published binaries for each given source should be also |
1150 | copied along with the source. |
1151 | |
1152 | @@ -778,7 +820,8 @@ |
1153 | copies = do_copy( |
1154 | sources, self.destination.archive, |
1155 | self.destination.distroseries, self.destination.pocket, |
1156 | - self.options.include_binaries, self.allow_delayed_copies) |
1157 | + self.options.include_binaries, self.allow_delayed_copies, |
1158 | + check_permissions=False) |
1159 | except CannotCopy, error: |
1160 | self.logger.error(str(error)) |
1161 | return [] |
1162 | |
1163 | === modified file 'lib/lp/soyuz/scripts/tests/test_copypackage.py' |
1164 | --- lib/lp/soyuz/scripts/tests/test_copypackage.py 2011-05-05 00:47:27 +0000 |
1165 | +++ lib/lp/soyuz/scripts/tests/test_copypackage.py 2011-05-05 20:54:43 +0000 |
1166 | @@ -450,7 +450,9 @@ |
1167 | copy_checker = CopyChecker(self.archive, include_binaries=False) |
1168 | self.assertIs( |
1169 | None, |
1170 | - copy_checker.checkCopy(self.source, self.series, self.pocket)) |
1171 | + copy_checker.checkCopy( |
1172 | + self.source, self.series, self.pocket, |
1173 | + check_permissions=False)) |
1174 | checked_copies = list(copy_checker.getCheckedCopies()) |
1175 | self.assertEquals(1, len(checked_copies)) |
1176 | [checked_copy] = checked_copies |
1177 | @@ -476,7 +478,9 @@ |
1178 | copy_checker = CopyChecker(self.archive, include_binaries=True) |
1179 | self.assertIs( |
1180 | None, |
1181 | - copy_checker.checkCopy(self.source, self.series, self.pocket)) |
1182 | + copy_checker.checkCopy( |
1183 | + self.source, self.series, self.pocket, |
1184 | + check_permissions=False)) |
1185 | checked_copies = list(copy_checker.getCheckedCopies()) |
1186 | self.assertEquals(1, len(checked_copies)) |
1187 | [checked_copy] = checked_copies |
1188 | @@ -485,7 +489,8 @@ |
1189 | BuildSetStatus.FULLYBUILT_PENDING) |
1190 | self.assertEquals(delayed, checked_copy.delayed) |
1191 | |
1192 | - def assertCannotCopySourceOnly(self, msg): |
1193 | + def assertCannotCopySourceOnly(self, msg, person=None, |
1194 | + check_permissions=False): |
1195 | """`CopyChecker.checkCopy()` for source-only copy raises CannotCopy. |
1196 | |
1197 | No `CheckedCopy` is stored. |
1198 | @@ -493,7 +498,8 @@ |
1199 | copy_checker = CopyChecker(self.archive, include_binaries=False) |
1200 | self.assertRaisesWithContent( |
1201 | CannotCopy, msg, |
1202 | - copy_checker.checkCopy, self.source, self.series, self.pocket) |
1203 | + copy_checker.checkCopy, self.source, self.series, self.pocket, |
1204 | + person, check_permissions) |
1205 | checked_copies = list(copy_checker.getCheckedCopies()) |
1206 | self.assertEquals(0, len(checked_copies)) |
1207 | |
1208 | @@ -505,7 +511,8 @@ |
1209 | copy_checker = CopyChecker(self.archive, include_binaries=True) |
1210 | self.assertRaisesWithContent( |
1211 | CannotCopy, msg, |
1212 | - copy_checker.checkCopy, self.source, self.series, self.pocket) |
1213 | + copy_checker.checkCopy, self.source, self.series, self.pocket, |
1214 | + None, False) |
1215 | checked_copies = list(copy_checker.getCheckedCopies()) |
1216 | self.assertEquals(0, len(checked_copies)) |
1217 | |
1218 | @@ -514,6 +521,14 @@ |
1219 | self.assertCannotCopyBinaries( |
1220 | 'source has no binaries to be copied') |
1221 | |
1222 | + def test_cannot_copy_check_perm_no_person(self): |
1223 | + # If check_permissions=True and person=None is passed to |
1224 | + # checkCopy, raise an error (cannot check permissions for a |
1225 | + # 'None' person). |
1226 | + self.assertCannotCopySourceOnly( |
1227 | + 'Cannot check copy permissions (no requester).', |
1228 | + person=None, check_permissions=True) |
1229 | + |
1230 | def test_cannot_copy_binaries_from_FTBFS(self): |
1231 | [build] = self.source.createMissingBuilds() |
1232 | build.status = BuildStatus.FAILEDTOBUILD |
1233 | @@ -699,10 +714,14 @@ |
1234 | # At this point copy is allowed with or without binaries. |
1235 | copy_checker = CopyChecker(archive, include_binaries=False) |
1236 | self.assertIs( |
1237 | - None, copy_checker.checkCopy(source, series, pocket)) |
1238 | + None, |
1239 | + copy_checker.checkCopy( |
1240 | + source, series, pocket, check_permissions=False)) |
1241 | copy_checker = CopyChecker(archive, include_binaries=True) |
1242 | self.assertIs( |
1243 | - None, copy_checker.checkCopy(source, series, pocket)) |
1244 | + None, |
1245 | + copy_checker.checkCopy( |
1246 | + source, series, pocket, check_permissions=False)) |
1247 | |
1248 | # Set the expiration date of one of the testing binary files. |
1249 | utc = pytz.timezone('UTC') |
1250 | @@ -713,14 +732,15 @@ |
1251 | # Now source-only copies are allowed. |
1252 | copy_checker = CopyChecker(archive, include_binaries=False) |
1253 | self.assertIs( |
1254 | - None, copy_checker.checkCopy(source, series, pocket)) |
1255 | + None, copy_checker.checkCopy( |
1256 | + source, series, pocket, check_permissions=False)) |
1257 | |
1258 | # Copies with binaries are denied. |
1259 | copy_checker = CopyChecker(archive, include_binaries=True) |
1260 | self.assertRaisesWithContent( |
1261 | CannotCopy, |
1262 | 'source has expired binaries', |
1263 | - copy_checker.checkCopy, source, series, pocket) |
1264 | + copy_checker.checkCopy, source, series, pocket, None, False) |
1265 | |
1266 | def test_checkCopy_cannot_copy_expired_sources(self): |
1267 | # checkCopy() raises CannotCopy if the copy requested includes |
1268 | @@ -745,7 +765,7 @@ |
1269 | self.assertRaisesWithContent( |
1270 | CannotCopy, |
1271 | 'source contains expired files', |
1272 | - copy_checker.checkCopy, source, series, pocket) |
1273 | + copy_checker.checkCopy, source, series, pocket, None, False) |
1274 | |
1275 | def test_checkCopy_allows_copies_from_other_distributions(self): |
1276 | # It is possible to copy packages between distributions, |
1277 | @@ -768,7 +788,8 @@ |
1278 | # Copy of sources to series in another distribution can be |
1279 | # performed. |
1280 | copy_checker = CopyChecker(archive, include_binaries=False) |
1281 | - copy_checker.checkCopy(source, series, pocket) |
1282 | + copy_checker.checkCopy( |
1283 | + source, series, pocket, check_permissions=False) |
1284 | |
1285 | def test_checkCopy_forbids_copies_to_unknown_distroseries(self): |
1286 | # We currently deny copies to series that are not for the Archive |
1287 | @@ -794,7 +815,7 @@ |
1288 | self.assertRaisesWithContent( |
1289 | CannotCopy, |
1290 | 'No such distro series sid in distribution debian.', |
1291 | - copy_checker.checkCopy, source, sid, pocket) |
1292 | + copy_checker.checkCopy, source, sid, pocket, None, False) |
1293 | |
1294 | def test_checkCopy_respects_sourceformatselection(self): |
1295 | # A source copy should be denied if the source's dsc_format is |
1296 | @@ -820,7 +841,8 @@ |
1297 | self.assertRaisesWithContent( |
1298 | CannotCopy, |
1299 | "Source format '3.0 (quilt)' not supported by target series " |
1300 | - "warty.", copy_checker.checkCopy, source, series, pocket) |
1301 | + "warty.", copy_checker.checkCopy, source, series, pocket, None, |
1302 | + False) |
1303 | |
1304 | def test_checkCopy_identifies_conflicting_copy_candidates(self): |
1305 | # checkCopy() is able to identify conflicting candidates within |
1306 | @@ -850,7 +872,8 @@ |
1307 | self.assertIs( |
1308 | None, |
1309 | copy_checker.checkCopy( |
1310 | - source, source.distroseries, source.pocket)) |
1311 | + source, source.distroseries, source.pocket, |
1312 | + check_permissions=False)) |
1313 | |
1314 | # The second source-only copy, for hoary-test, fails, since it |
1315 | # conflicts with the just-approved copy. |
1316 | @@ -859,7 +882,8 @@ |
1317 | 'same version already building in the destination archive ' |
1318 | 'for Breezy Badger Autotest', |
1319 | copy_checker.checkCopy, |
1320 | - copied_source, copied_source.distroseries, copied_source.pocket) |
1321 | + copied_source, copied_source.distroseries, copied_source.pocket, |
1322 | + None, False) |
1323 | |
1324 | def test_checkCopy_identifies_delayed_copies_conflicts(self): |
1325 | # checkCopy() detects copy conflicts in the upload queue for |
1326 | @@ -882,14 +906,16 @@ |
1327 | # Commit so the just-created files are accessible and perform |
1328 | # the delayed-copy. |
1329 | self.layer.txn.commit() |
1330 | - do_copy([source], archive, series, pocket, include_binaries=False) |
1331 | + do_copy( |
1332 | + [source], archive, series, pocket, include_binaries=False, |
1333 | + check_permissions=False) |
1334 | |
1335 | # Repeating the copy is denied. |
1336 | copy_checker = CopyChecker(archive, include_binaries=False) |
1337 | self.assertRaisesWithContent( |
1338 | CannotCopy, |
1339 | 'same version already uploaded and waiting in ACCEPTED queue', |
1340 | - copy_checker.checkCopy, source, series, pocket) |
1341 | + copy_checker.checkCopy, source, series, pocket, None, False) |
1342 | |
1343 | def test_checkCopy_suppressing_delayed_copies(self): |
1344 | # `CopyChecker` by default will request delayed-copies when it's |
1345 | @@ -917,7 +943,8 @@ |
1346 | # this operation, since restricted files are being copied to |
1347 | # public archives. |
1348 | copy_checker = CopyChecker(archive, include_binaries=False) |
1349 | - copy_checker.checkCopy(source, series, pocket) |
1350 | + copy_checker.checkCopy( |
1351 | + source, series, pocket, check_permissions=False) |
1352 | [checked_copy] = list(copy_checker.getCheckedCopies()) |
1353 | self.assertTrue(checked_copy.delayed) |
1354 | |
1355 | @@ -925,7 +952,8 @@ |
1356 | # scheduled. |
1357 | copy_checker = CopyChecker( |
1358 | archive, include_binaries=False, allow_delayed_copies=False) |
1359 | - copy_checker.checkCopy(source, series, pocket) |
1360 | + copy_checker.checkCopy( |
1361 | + source, series, pocket, check_permissions=False) |
1362 | [checked_copy] = list(copy_checker.getCheckedCopies()) |
1363 | self.assertFalse(checked_copy.delayed) |
1364 | |
1365 | @@ -2683,7 +2711,7 @@ |
1366 | self.assertIs( |
1367 | None, |
1368 | checker.checkCopy(proposed_source, warty, |
1369 | - PackagePublishingPocket.UPDATES)) |
1370 | + PackagePublishingPocket.UPDATES, check_permissions=False)) |
1371 | |
1372 | def testCopySourceWithConflictingFilesInPPAs(self): |
1373 | """We can copy source if the source files match, both in name and |
1374 | @@ -2728,7 +2756,7 @@ |
1375 | "test-source_1.0.orig.tar.gz already exists in destination " |
1376 | "archive with different contents.", |
1377 | checker.checkCopy, test2_source, warty, |
1378 | - PackagePublishingPocket.RELEASE) |
1379 | + PackagePublishingPocket.RELEASE, None, False) |
1380 | |
1381 | def testCopySourceWithSameFilenames(self): |
1382 | """We can copy source if the source files match, both in name and |
1383 | @@ -2771,7 +2799,7 @@ |
1384 | self.assertIs( |
1385 | None, |
1386 | checker.checkCopy(test2_source, warty, |
1387 | - PackagePublishingPocket.RELEASE)) |
1388 | + PackagePublishingPocket.RELEASE, check_permissions=False)) |
1389 | |
1390 | def testCopySourceWithExpiredSourcesInDestination(self): |
1391 | """We can also copy sources if the destination archive has expired |
1392 | @@ -2820,4 +2848,4 @@ |
1393 | self.assertIs( |
1394 | None, |
1395 | checker.checkCopy(test2_source, warty, |
1396 | - PackagePublishingPocket.RELEASE)) |
1397 | + PackagePublishingPocket.RELEASE, check_permissions=False)) |
1398 | |
1399 | === modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-files.txt' |
1400 | --- lib/lp/soyuz/stories/ppa/xx-ppa-files.txt 2011-05-05 00:47:27 +0000 |
1401 | +++ lib/lp/soyuz/stories/ppa/xx-ppa-files.txt 2011-05-05 20:54:43 +0000 |
1402 | @@ -268,7 +268,8 @@ |
1403 | ... no_priv_private_ppa.getPublishedSources(name='test-pkg'), |
1404 | ... no_priv.archive, series=ubuntu['warty'], |
1405 | ... pocket=PackagePublishingPocket.RELEASE, |
1406 | - ... include_binaries=True, allow_delayed_copies=False) |
1407 | + ... include_binaries=True, allow_delayed_copies=False, |
1408 | + ... person=no_priv) |
1409 | |
1410 | >>> from zope.security.proxy import removeSecurityProxy |
1411 | >>> removeSecurityProxy(dsc_file).restricted = False |
1412 | |
1413 | === modified file 'lib/lp/soyuz/tests/test_archive.py' |
1414 | --- lib/lp/soyuz/tests/test_archive.py 2011-05-05 00:47:27 +0000 |
1415 | +++ lib/lp/soyuz/tests/test_archive.py 2011-05-05 20:54:43 +0000 |
1416 | @@ -37,6 +37,7 @@ |
1417 | from lp.services.propertycache import clear_property_cache |
1418 | from lp.services.worlddata.interfaces.country import ICountrySet |
1419 | from lp.soyuz.enums import ( |
1420 | + ArchivePermissionType, |
1421 | ArchivePurpose, |
1422 | ArchiveStatus, |
1423 | PackagePublishingStatus, |
1424 | @@ -60,6 +61,7 @@ |
1425 | from lp.soyuz.interfaces.component import IComponentSet |
1426 | from lp.soyuz.interfaces.processor import IProcessorFamilySet |
1427 | from lp.soyuz.model.archive import Archive |
1428 | +from lp.soyuz.model.archivepermission import ArchivePermission |
1429 | from lp.soyuz.model.binarypackagerelease import ( |
1430 | BinaryPackageReleaseDownloadCount, |
1431 | ) |
1432 | @@ -67,6 +69,7 @@ |
1433 | from lp.soyuz.tests.test_publishing import SoyuzTestPublisher |
1434 | from lp.testing import ( |
1435 | ANONYMOUS, |
1436 | + celebrity_logged_in, |
1437 | login, |
1438 | login_person, |
1439 | person_logged_in, |
1440 | @@ -824,6 +827,22 @@ |
1441 | False, |
1442 | archive.canUploadSuiteSourcePackage(person, suitesourcepackage)) |
1443 | |
1444 | + def test_hasAnyPermission(self): |
1445 | + # hasAnyPermission returns true if the person is the member of a |
1446 | + # team with any kind of permission on the archive. |
1447 | + archive = self.factory.makeArchive() |
1448 | + person = self.factory.makePerson() |
1449 | + team = self.factory.makeTeam() |
1450 | + main = getUtility(IComponentSet)["main"] |
1451 | + ArchivePermission( |
1452 | + archive=archive, person=team, component=main, |
1453 | + permission=ArchivePermissionType.UPLOAD) |
1454 | + |
1455 | + self.assertFalse(archive.hasAnyPermission(person)) |
1456 | + with celebrity_logged_in('admin'): |
1457 | + team.addMember(person, team.teamowner) |
1458 | + self.assertTrue(archive.hasAnyPermission(person)) |
1459 | + |
1460 | |
1461 | class TestUpdatePackageDownloadCount(TestCaseWithFactory): |
1462 | """Ensure that updatePackageDownloadCount works as expected.""" |
Looks fine, I have a few comments in the diff below.
> === modified file 'lib/lp/ registry/ browser/ tests/test_ distroseries. py' registry/ browser/ tests/test_ distroseries. py 2011-05-05 05:22:20 +0000 registry/ browser/ tests/test_ distroseries. py 2011-05-05 15:21:22 +0000 database. constants import UTC_NOW database. sqlbase import flush_database_ caches launchpad. interfaces. launchpad import ILaunchpadCeleb rities launchpad. testing. pages import find_tag_by_id launchpad. webapp. authorization import check_permission launchpad. webapp. batching import BatchNavigator launchpad. webapp. publisher import canonical_url testing. layers import ( ferenceStatus, ferenceType, interfaces. pocket import PackagePublishi ngPocket interfaces. series import SeriesStatus interfaces. person import IPersonSet features import ( feature_ controller, onType, ngStatus, rmat, interfaces. component import IComponentSet interfaces. distributionjob import ( roSeriesJobSour ce, interfaces. sourcepackagefo rmat import ( ormatSelectionS et, model.archivepe rmission import ArchivePermission logged_ in, makeDistroSerie s( series= self.factory. makeDistroSerie s()) makeDistroSerie sDifference( makeDistroSerie sDifference( series= distroseries, difference_ type=difference _type) initialized_ view(distroseri es, '+index') self.simple_ user) 'soyuz. derived- series- ui.enabled' )) (html, Not(portlet_ header) ) (html_content, Not(portlet_ header) ) s_portlet_ all_differences (self): quest overrides roller. features = get_relevant_ feature_ controller( )
> --- lib/lp/
> +++ lib/lp/
> @@ -27,10 +27,10 @@
> from zope.security.proxy import removeSecurityProxy
>
> from canonical.config import config
> -from canonical.
> from canonical.
> from canonical.
> from canonical.
> +from canonical.
> from canonical.
> from canonical.
> from canonical.
> @@ -48,8 +48,7 @@
> DistroSeriesDif
> DistroSeriesDif
> )
> -from lp.registry.
> -from lp.registry.
> +from lp.registry.
> from lp.services.
> get_relevant_
> getFeatureFlag,
> @@ -61,15 +60,18 @@
> getFeatureStore,
> )
> from lp.soyuz.enums import (
> + ArchivePermissi
> PackagePublishi
> SourcePackageFo
> )
> +from lp.soyuz.
> from lp.soyuz.
> IInitialiseDist
> )
> from lp.soyuz.
> ISourcePackageF
> )
> +from lp.soyuz.
> from lp.testing import (
> celebrity_
> feature_flags,
> @@ -113,7 +115,7 @@
> # Helper function to create a valid DSD.
> distroseries = self.factory.
> parent_
> - ds_diff = self.factory.
> + self.factory.
> derived_
> return create_
>
> @@ -176,11 +178,11 @@
> derived_series,
> '+index',
> principal=
> - html = view()
> + html_content = view()
>
> self.assertEqual(
> None, getFeatureFlag(
> - self.assertThat
> + self.assertThat
>
> def test_difference
> # The difference portlet shows the differences with the parent
> @@ -212,9 +214,9 @@
> # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRe
> # self.features to NullFeatureCont
> view.request.
> - html = view()
> + html_conte...