Merge lp:~stevenk/launchpad/workitems-delete-series into lp:launchpad
- workitems-delete-series
- Merge into devel
Status: | Merged |
---|---|
Approved by: | William Grant |
Approved revision: | no longer in the source branch. |
Merged at revision: | 16549 |
Proposed branch: | lp:~stevenk/launchpad/workitems-delete-series |
Merge into: | lp:launchpad |
Diff against target: |
667 lines (+108/-92) 22 files modified
lib/lp/_schema_circular_imports.py (+1/-1) lib/lp/blueprints/browser/specificationtarget.py (+1/-1) lib/lp/blueprints/doc/specification.txt (+0/-3) lib/lp/blueprints/interfaces/specification.py (+0/-3) lib/lp/blueprints/interfaces/specificationtarget.py (+1/-1) lib/lp/blueprints/model/specification.py (+1/-9) lib/lp/blueprints/tests/test_hasspecifications.py (+6/-8) lib/lp/bugs/model/tests/test_bugtask.py (+1/-2) lib/lp/registry/browser/__init__.py (+11/-15) lib/lp/registry/browser/milestone.py (+1/-1) lib/lp/registry/browser/productseries.py (+3/-3) lib/lp/registry/browser/tests/test_milestone.py (+31/-0) lib/lp/registry/browser/tests/test_productseries_views.py (+9/-12) lib/lp/registry/configure.zcml (+2/-1) lib/lp/registry/doc/milestone.txt (+2/-2) lib/lp/registry/doc/projectgroup.txt (+10/-10) lib/lp/registry/interfaces/milestone.py (+3/-1) lib/lp/registry/interfaces/productseries.py (+6/-7) lib/lp/registry/model/milestone.py (+12/-10) lib/lp/registry/model/productseries.py (+5/-0) lib/lp/registry/tests/test_product.py (+1/-1) lib/lp/registry/tests/test_productseries.py (+1/-1) |
To merge this branch: | bzr merge lp:~stevenk/launchpad/workitems-delete-series |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+156457@code.launchpad.net |
Commit message
Clean up ProductSeries and Milestone deletion to not be so OOPS-friendly when faced with deleted workitems or invisible specifications.
Description of the change
This branch is sadly now misnamed, since it has grown in scope quite a bit.
ProductSeries and Milestone deletion would not handle a specification that was invisible to the currently logged in user, which would lead to an OOPS in the milestone case, or a specification targeted to the now obsolete-junk series in the series case.
Worse, workitems were not handled at all in the milestone case, which makes deleting a milestone that contained workitems fraught with peril, since the code might miss a workitem, and then you get an easily reproduced OOPS.
IHasSpecificati
IMilestone and IProductSeries have grown a all_specifications property which returns all specifications, irregardless if the current logged in user can see them or not.
I have performed a little bit of clean-up, but not enough to force this branch to be net-negative.
William Grant (wgrant) : | # |
Preview Diff
1 | === modified file 'lib/lp/_schema_circular_imports.py' |
2 | --- lib/lp/_schema_circular_imports.py 2013-02-05 22:56:33 +0000 |
3 | +++ lib/lp/_schema_circular_imports.py 2013-04-03 04:02:25 +0000 |
4 | @@ -734,7 +734,7 @@ |
5 | |
6 | # IHasSpecifications |
7 | patch_collection_property( |
8 | - IHasSpecifications, '_all_specifications', ISpecification) |
9 | + IHasSpecifications, 'visible_specifications', ISpecification) |
10 | patch_collection_property( |
11 | IHasSpecifications, '_valid_specifications', ISpecification) |
12 | |
13 | |
14 | === modified file 'lib/lp/blueprints/browser/specificationtarget.py' |
15 | --- lib/lp/blueprints/browser/specificationtarget.py 2013-01-22 05:07:31 +0000 |
16 | +++ lib/lp/blueprints/browser/specificationtarget.py 2013-04-03 04:02:25 +0000 |
17 | @@ -259,7 +259,7 @@ |
18 | |
19 | @cachedproperty |
20 | def has_any_specifications(self): |
21 | - return not self.context._all_specifications.is_empty() |
22 | + return not self.context.visible_specifications.is_empty() |
23 | |
24 | @cachedproperty |
25 | def all_specifications(self): |
26 | |
27 | === modified file 'lib/lp/blueprints/doc/specification.txt' |
28 | --- lib/lp/blueprints/doc/specification.txt 2013-01-25 03:30:08 +0000 |
29 | +++ lib/lp/blueprints/doc/specification.txt 2013-04-03 04:02:25 +0000 |
30 | @@ -99,9 +99,6 @@ |
31 | |
32 | SpecificationSet implements the ISpecificationSet interface |
33 | |
34 | - >>> ubuspec in specset._all_specifications |
35 | - True |
36 | - |
37 | >>> from lp.testing import verifyObject |
38 | >>> verifyObject(ISpecificationSet, specset) |
39 | True |
40 | |
41 | === modified file 'lib/lp/blueprints/interfaces/specification.py' |
42 | --- lib/lp/blueprints/interfaces/specification.py 2013-01-25 03:30:08 +0000 |
43 | +++ lib/lp/blueprints/interfaces/specification.py 2013-04-03 04:02:25 +0000 |
44 | @@ -698,9 +698,6 @@ |
45 | :return: A list of tuples containing (status_id, count). |
46 | """ |
47 | |
48 | - def __iter__(): |
49 | - """Iterate over all specifications.""" |
50 | - |
51 | def getByURL(url): |
52 | """Return the specification with the given url.""" |
53 | |
54 | |
55 | === modified file 'lib/lp/blueprints/interfaces/specificationtarget.py' |
56 | --- lib/lp/blueprints/interfaces/specificationtarget.py 2013-01-07 02:40:55 +0000 |
57 | +++ lib/lp/blueprints/interfaces/specificationtarget.py 2013-04-03 04:02:25 +0000 |
58 | @@ -37,7 +37,7 @@ |
59 | associated with them, and you can use this interface to query those. |
60 | """ |
61 | |
62 | - _all_specifications = exported(doNotSnapshot( |
63 | + visible_specifications = exported(doNotSnapshot( |
64 | CollectionField( |
65 | title=_("All specifications"), |
66 | value_type=Reference(schema=Interface), # ISpecification, really. |
67 | |
68 | === modified file 'lib/lp/blueprints/model/specification.py' |
69 | --- lib/lp/blueprints/model/specification.py 2013-01-30 05:31:20 +0000 |
70 | +++ lib/lp/blueprints/model/specification.py 2013-04-03 04:02:25 +0000 |
71 | @@ -968,7 +968,7 @@ |
72 | return (Desc(Specification.datecreated), Specification.id) |
73 | |
74 | @property |
75 | - def _all_specifications(self): |
76 | + def visible_specifications(self): |
77 | """See IHasSpecifications.""" |
78 | user = getUtility(ILaunchBag).user |
79 | return self.specifications(user, filter=[SpecificationFilter.ALL]) |
80 | @@ -1013,14 +1013,6 @@ |
81 | cur.execute(query) |
82 | return cur.fetchall() |
83 | |
84 | - @property |
85 | - def _all_specifications(self): |
86 | - return Specification.select() |
87 | - |
88 | - def __iter__(self): |
89 | - """See ISpecificationSet.""" |
90 | - return iter(self.all_specifications) |
91 | - |
92 | def specifications(self, user, sort=None, quantity=None, filter=None, |
93 | prejoin_people=True): |
94 | from lp.blueprints.model.specificationsearch import ( |
95 | |
96 | === modified file 'lib/lp/blueprints/tests/test_hasspecifications.py' |
97 | --- lib/lp/blueprints/tests/test_hasspecifications.py 2013-01-22 05:07:31 +0000 |
98 | +++ lib/lp/blueprints/tests/test_hasspecifications.py 2013-04-03 04:02:25 +0000 |
99 | @@ -25,7 +25,7 @@ |
100 | self.factory.makeSpecification(product=product, name="spec1") |
101 | self.factory.makeSpecification(product=product, name="spec2") |
102 | self.assertNamesOfSpecificationsAre( |
103 | - ["spec1", "spec2"], product._all_specifications) |
104 | + ["spec1", "spec2"], product.visible_specifications) |
105 | |
106 | def test_product_valid_specifications(self): |
107 | product = self.factory.makeProduct() |
108 | @@ -43,7 +43,7 @@ |
109 | self.factory.makeSpecification( |
110 | distribution=distribution, name="spec2") |
111 | self.assertNamesOfSpecificationsAre( |
112 | - ["spec1", "spec2"], distribution._all_specifications) |
113 | + ["spec1", "spec2"], distribution.visible_specifications) |
114 | |
115 | def test_distribution_valid_specifications(self): |
116 | distribution = self.factory.makeDistribution() |
117 | @@ -67,8 +67,7 @@ |
118 | self.factory.makeSpecification( |
119 | distribution=distribution, name="spec3") |
120 | self.assertNamesOfSpecificationsAre( |
121 | - ["spec1", "spec2"], |
122 | - distroseries._all_specifications) |
123 | + ["spec1", "spec2"], distroseries.visible_specifications) |
124 | |
125 | # XXX: salgado, 2010-11-25, bug=681432: Test disabled because |
126 | # DistroSeries._valid_specifications is broken. |
127 | @@ -101,7 +100,7 @@ |
128 | product=product, name="spec2", goal=productseries) |
129 | self.factory.makeSpecification(product=product, name="spec3") |
130 | self.assertNamesOfSpecificationsAre( |
131 | - ["spec1", "spec2"], productseries._all_specifications) |
132 | + ["spec1", "spec2"], productseries.visible_specifications) |
133 | |
134 | def test_productseries_valid_specifications(self): |
135 | product = self.factory.makeProduct() |
136 | @@ -132,8 +131,7 @@ |
137 | self.factory.makeSpecification( |
138 | product=product3, name="spec3") |
139 | self.assertNamesOfSpecificationsAre( |
140 | - ["spec1", "spec2"], |
141 | - projectgroup._all_specifications) |
142 | + ["spec1", "spec2"], projectgroup.visible_specifications) |
143 | |
144 | def test_projectgroup_valid_specifications(self): |
145 | projectgroup = self.factory.makeProject() |
146 | @@ -160,7 +158,7 @@ |
147 | self.factory.makeSpecification( |
148 | product=product, name="spec3") |
149 | self.assertNamesOfSpecificationsAre( |
150 | - ["spec1", "spec2"], person._all_specifications) |
151 | + ["spec1", "spec2"], person.visible_specifications) |
152 | |
153 | def test_person_valid_specifications(self): |
154 | person = self.factory.makePerson(name="james-w") |
155 | |
156 | === modified file 'lib/lp/bugs/model/tests/test_bugtask.py' |
157 | --- lib/lp/bugs/model/tests/test_bugtask.py 2013-02-21 06:29:24 +0000 |
158 | +++ lib/lp/bugs/model/tests/test_bugtask.py 2013-04-03 04:02:25 +0000 |
159 | @@ -27,7 +27,6 @@ |
160 | ServiceUsage, |
161 | ) |
162 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
163 | -from lp.blueprints.interfaces.specification import ISpecificationSet |
164 | from lp.bugs.interfaces.bug import ( |
165 | CreateBugParams, |
166 | IBug, |
167 | @@ -505,7 +504,7 @@ |
168 | ' has_specification: False']) |
169 | |
170 | # a specification gets linked... |
171 | - spec = getUtility(ISpecificationSet)._all_specifications[0] |
172 | + spec = self.factory.makeSpecification() |
173 | spec.linkBug(bug_two) |
174 | |
175 | # or a branch gets linked to the bug... |
176 | |
177 | === modified file 'lib/lp/registry/browser/__init__.py' |
178 | --- lib/lp/registry/browser/__init__.py 2012-10-19 14:22:36 +0000 |
179 | +++ lib/lp/registry/browser/__init__.py 2013-04-03 04:02:25 +0000 |
180 | @@ -1,4 +1,4 @@ |
181 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
182 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
183 | # GNU Affero General Public License version 3 (see the file LICENSE). |
184 | |
185 | """Common registry browser helpers and mixins.""" |
186 | @@ -30,11 +30,11 @@ |
187 | LaunchpadEditFormView, |
188 | ) |
189 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
190 | +from lp.blueprints.model.specificationworkitem import SpecificationWorkItem |
191 | from lp.bugs.interfaces.bugtask import IBugTaskSet |
192 | from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams |
193 | from lp.registry.interfaces.productseries import IProductSeries |
194 | from lp.registry.interfaces.series import SeriesStatus |
195 | -from lp.services.webapp.interfaces import ILaunchBag |
196 | from lp.services.webapp.publisher import ( |
197 | canonical_url, |
198 | DataDownloadView, |
199 | @@ -171,14 +171,6 @@ |
200 | bugtasks = getUtility(IBugTaskSet).search(params) |
201 | return list(bugtasks) |
202 | |
203 | - def _getSpecifications(self, target): |
204 | - """Return the list `ISpecification`s associated to the target.""" |
205 | - if IProductSeries.providedBy(target): |
206 | - return list(target._all_specifications) |
207 | - else: |
208 | - user = getUtility(ILaunchBag).user |
209 | - return list(target.getSpecifications(user)) |
210 | - |
211 | def _getProductRelease(self, milestone): |
212 | """The `IProductRelease` associated with the milestone.""" |
213 | return milestone.product_release |
214 | @@ -200,9 +192,11 @@ |
215 | subscription.delete() |
216 | |
217 | def _remove_series_bugs_and_specifications(self, series): |
218 | - """Untarget the associated bugs and subscriptions.""" |
219 | - for spec in self._getSpecifications(series): |
220 | - spec.proposeGoal(None, self.user) |
221 | + """Untarget the associated bugs and specifications.""" |
222 | + for spec in series.all_specifications: |
223 | + # The logged in user may have no permission to see the spec, so |
224 | + # make use of removeSecurityProxy to force it. |
225 | + removeSecurityProxy(spec).proposeGoal(None, self.user) |
226 | for bugtask in self._getBugtasks(series): |
227 | # Bugtasks cannot be deleted directly. In this case, the bugtask |
228 | # is already reported on the product, so the series bugtask has |
229 | @@ -246,8 +240,10 @@ |
230 | Store.of(bugtask).remove(nb.conjoined_master) |
231 | else: |
232 | nb.milestone = None |
233 | - for spec in self._getSpecifications(milestone): |
234 | - spec.milestone = None |
235 | + removeSecurityProxy(milestone.all_specifications).set(milestoneID=None) |
236 | + Store.of(milestone).find( |
237 | + SpecificationWorkItem, milestone_id=milestone.id).set( |
238 | + milestone_id=None) |
239 | self._deleteRelease(milestone.product_release) |
240 | milestone.destroySelf() |
241 | |
242 | |
243 | === modified file 'lib/lp/registry/browser/milestone.py' |
244 | --- lib/lp/registry/browser/milestone.py 2013-03-26 02:56:48 +0000 |
245 | +++ lib/lp/registry/browser/milestone.py 2013-04-03 04:02:25 +0000 |
246 | @@ -569,7 +569,7 @@ |
247 | @cachedproperty |
248 | def specifications(self): |
249 | """The list `ISpecification`s targeted to the milestone.""" |
250 | - return self._getSpecifications(self.context) |
251 | + return self.context.getSpecifications(self.user) |
252 | |
253 | @cachedproperty |
254 | def product_release(self): |
255 | |
256 | === modified file 'lib/lp/registry/browser/productseries.py' |
257 | --- lib/lp/registry/browser/productseries.py 2012-11-26 08:40:20 +0000 |
258 | +++ lib/lp/registry/browser/productseries.py 2013-04-03 04:02:25 +0000 |
259 | @@ -1,4 +1,4 @@ |
260 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
261 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
262 | # GNU Affero General Public License version 3 (see the file LICENSE). |
263 | |
264 | """View classes for `IProductSeries`.""" |
265 | @@ -696,9 +696,9 @@ |
266 | @cachedproperty |
267 | def specifications(self): |
268 | """A list of all `ISpecification`s targeted to this series.""" |
269 | - all_specifications = self._getSpecifications(self.context) |
270 | + all_specifications = list(self.context.visible_specifications) |
271 | for milestone in self.milestones: |
272 | - all_specifications.extend(self._getSpecifications(milestone)) |
273 | + all_specifications.extend(milestone.getSpecifications(self.user)) |
274 | return all_specifications |
275 | |
276 | @cachedproperty |
277 | |
278 | === modified file 'lib/lp/registry/browser/tests/test_milestone.py' |
279 | --- lib/lp/registry/browser/tests/test_milestone.py 2013-03-25 05:53:38 +0000 |
280 | +++ lib/lp/registry/browser/tests/test_milestone.py 2013-04-03 04:02:25 +0000 |
281 | @@ -6,6 +6,7 @@ |
282 | __metaclass__ = type |
283 | |
284 | import soupmatchers |
285 | +from storm.store import Store |
286 | from testtools.matchers import LessThan |
287 | from zope.component import getUtility |
288 | |
289 | @@ -309,6 +310,36 @@ |
290 | BugTaskSearchParams(user=None)) |
291 | self.assertEqual(0, tasks.count()) |
292 | |
293 | + def test_delete_milestone_with_deleted_workitems(self): |
294 | + milestone = self.factory.makeMilestone() |
295 | + specification = self.factory.makeSpecification( |
296 | + product=milestone.product) |
297 | + workitem = self.factory.makeSpecificationWorkItem( |
298 | + specification=specification, milestone=milestone, deleted=True) |
299 | + form = {'field.actions.delete': 'Delete Milestone'} |
300 | + owner = milestone.product.owner |
301 | + with person_logged_in(owner): |
302 | + view = create_initialized_view(milestone, '+delete', form=form) |
303 | + Store.of(workitem).flush() |
304 | + self.assertEqual([], view.errors) |
305 | + self.assertIs(None, workitem.milestone) |
306 | + |
307 | + def test_delete_milestone_with_private_specification(self): |
308 | + policy = SpecificationSharingPolicy.PROPRIETARY |
309 | + product = self.factory.makeProduct(specification_sharing_policy=policy) |
310 | + milestone = self.factory.makeMilestone(product=product) |
311 | + specification = self.factory.makeSpecification( |
312 | + information_type=InformationType.PROPRIETARY, milestone=milestone) |
313 | + ap = getUtility(IAccessPolicySource).find( |
314 | + [(product, InformationType.PROPRIETARY)]) |
315 | + getUtility(IAccessPolicyGrantSource).revokeByPolicy(ap) |
316 | + form = {'field.actions.delete': 'Delete Milestone'} |
317 | + with person_logged_in(product.owner): |
318 | + view = create_initialized_view(milestone, '+delete', form=form) |
319 | + Store.of(specification).flush() |
320 | + self.assertEqual([], view.errors) |
321 | + self.assertIs(None, specification.milestone) |
322 | + |
323 | |
324 | class TestQueryCountBase(TestCaseWithFactory): |
325 | |
326 | |
327 | === modified file 'lib/lp/registry/browser/tests/test_productseries_views.py' |
328 | --- lib/lp/registry/browser/tests/test_productseries_views.py 2012-10-18 17:16:49 +0000 |
329 | +++ lib/lp/registry/browser/tests/test_productseries_views.py 2013-04-03 04:02:25 +0000 |
330 | @@ -1,4 +1,4 @@ |
331 | -# Copyright 2011-2012 Canonical Ltd. This software is licensed under the |
332 | +# Copyright 2011-2013 Canonical Ltd. This software is licensed under the |
333 | # GNU Affero General Public License version 3 (see the file LICENSE). |
334 | |
335 | """View tests for ProductSeries pages.""" |
336 | @@ -104,9 +104,9 @@ |
337 | """The displayed branch name should include the unique name.""" |
338 | branch = self.factory.makeProductBranch() |
339 | series = self.factory.makeProductSeries(branch=branch) |
340 | - tag = soupmatchers.Tag('series-branch', 'a', |
341 | - attrs={'id': 'series-branch'}, |
342 | - text='lp://dev/' + branch.unique_name) |
343 | + tag = soupmatchers.Tag( |
344 | + 'series-branch', 'a', attrs={'id': 'series-branch'}, |
345 | + text='lp://dev/' + branch.unique_name) |
346 | browser = self.getViewBrowser(series) |
347 | self.assertThat(browser.contents, soupmatchers.HTMLContains(tag)) |
348 | |
349 | @@ -121,8 +121,8 @@ |
350 | information_type=InformationType.PROPRIETARY) |
351 | productseries = self.factory.makeProductSeries(product=product) |
352 | ubuntu_series = self.factory.makeUbuntuDistroSeries() |
353 | - sp = self.factory.makeSourcePackage(distroseries=ubuntu_series, |
354 | - publish=True) |
355 | + sp = self.factory.makeSourcePackage( |
356 | + distroseries=ubuntu_series, publish=True) |
357 | browser = self.getBrowser(productseries, '+ubuntupkg') |
358 | browser.getControl('Source Package Name').value = ( |
359 | sp.sourcepackagename.name) |
360 | @@ -156,11 +156,9 @@ |
361 | series = self.factory.makeProductSeries(product=product) |
362 | for status in BugTaskStatusSearch.items: |
363 | self.factory.makeBug( |
364 | - series=series, status=status, |
365 | - owner=product.owner) |
366 | + series=series, status=status, owner=product.owner) |
367 | self.factory.makeBug( |
368 | - series=series, status=BugTaskStatus.UNKNOWN, |
369 | - owner=product.owner) |
370 | + series=series, status=BugTaskStatus.UNKNOWN, owner=product.owner) |
371 | expected = [ |
372 | (BugTaskStatus.NEW, 1), |
373 | (BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE, 1), |
374 | @@ -177,8 +175,7 @@ |
375 | (BugTaskStatus.INPROGRESS, 1), |
376 | (BugTaskStatus.FIXCOMMITTED, 1), |
377 | (BugTaskStatus.FIXRELEASED, 1), |
378 | - (BugTaskStatus.UNKNOWN, 1), |
379 | - ] |
380 | + (BugTaskStatus.UNKNOWN, 1)] |
381 | with person_logged_in(product.owner): |
382 | view = create_initialized_view(series, '+status') |
383 | observed = [ |
384 | |
385 | === modified file 'lib/lp/registry/configure.zcml' |
386 | --- lib/lp/registry/configure.zcml 2013-03-12 03:18:18 +0000 |
387 | +++ lib/lp/registry/configure.zcml 2013-04-03 04:02:25 +0000 |
388 | @@ -1055,6 +1055,7 @@ |
389 | productseries |
390 | series_target |
391 | summary |
392 | + all_specifications |
393 | "/> |
394 | <require |
395 | permission="launchpad.LimitedView" |
396 | @@ -1458,7 +1459,7 @@ |
397 | <require |
398 | permission="launchpad.View" |
399 | attributes=" |
400 | - _all_specifications |
401 | + visible_specifications |
402 | _valid_specifications |
403 | getAllowedSpecificationInformationTypes |
404 | getDefaultSpecificationInformationType |
405 | |
406 | === modified file 'lib/lp/registry/doc/milestone.txt' |
407 | --- lib/lp/registry/doc/milestone.txt 2012-10-19 14:22:36 +0000 |
408 | +++ lib/lp/registry/doc/milestone.txt 2013-04-03 04:02:25 +0000 |
409 | @@ -289,7 +289,7 @@ |
410 | the Gnome project has yet any specifications. |
411 | |
412 | >>> for product in gnome.products: |
413 | - ... print product.name, list(product._all_specifications) |
414 | + ... print product.name, list(product.visible_specifications) |
415 | evolution [] |
416 | gnome-terminal [] |
417 | applets [] |
418 | @@ -303,7 +303,7 @@ |
419 | milestone, it is "inheritied" by the project milestone. |
420 | |
421 | >>> spec = test_helper.createSpecification('1.1', 'applets') |
422 | - >>> [spec.name for spec in applets._all_specifications] |
423 | + >>> [spec.name for spec in applets.visible_specifications] |
424 | [u'applets-specification'] |
425 | |
426 | >>> specs = gnome.getMilestone('1.1').getSpecifications(None) |
427 | |
428 | === modified file 'lib/lp/registry/doc/projectgroup.txt' |
429 | --- lib/lp/registry/doc/projectgroup.txt 2012-12-26 01:32:19 +0000 |
430 | +++ lib/lp/registry/doc/projectgroup.txt 2013-04-03 04:02:25 +0000 |
431 | @@ -200,10 +200,10 @@ |
432 | >>> firefox.active = True |
433 | >>> flush_database_updates() |
434 | |
435 | -We can get all the specifications via the _all_specifications property, |
436 | +We can get all the specifications via the visible_specifications property, |
437 | and all valid specifications via the _valid_specifications property: |
438 | |
439 | - >>> for spec in mozilla._all_specifications: |
440 | + >>> for spec in mozilla.visible_specifications: |
441 | ... print spec.name |
442 | svg-support |
443 | canvas |
444 | @@ -238,30 +238,30 @@ |
445 | >>> print mozilla.getSeries('nonsense') |
446 | None |
447 | |
448 | -IProjectGroupSeries._all_specifications lists all specifications |
449 | +IProjectGroupSeries.visible_specifications lists all specifications |
450 | assigned to a series. Currently, no specifications are assigned to the |
451 | Mozilla series 1.0. |
452 | |
453 | - >>> specs = mozilla_series_1_0._all_specifications |
454 | + >>> specs = mozilla_series_1_0.visible_specifications |
455 | >>> specs.count() |
456 | 0 |
457 | |
458 | If a specification is assigned to series 1.0, it appears in |
459 | -mozilla_1_0_series._all_specifications. |
460 | +mozilla_1_0_series.visible_specifications. |
461 | |
462 | >>> filter = [SpecificationFilter.INFORMATIONAL] |
463 | >>> extension_manager_upgrades = mozilla.specifications( |
464 | ... None, filter=filter)[0] |
465 | >>> series_1_0 = firefox.getSeries('1.0') |
466 | >>> extension_manager_upgrades.proposeGoal(series_1_0, no_priv) |
467 | - >>> for spec in mozilla_series_1_0._all_specifications: |
468 | + >>> for spec in mozilla_series_1_0.visible_specifications: |
469 | ... print spec.name |
470 | extension-manager-upgrades |
471 | |
472 | This specification is not listed for other series. |
473 | |
474 | >>> mozilla_trunk = mozilla.getSeries('trunk') |
475 | - >>> print mozilla_trunk._all_specifications.count() |
476 | + >>> print mozilla_trunk.visible_specifications.count() |
477 | 0 |
478 | |
479 | Filtered lists of project series related specifications are generated |
480 | @@ -273,7 +273,7 @@ |
481 | |
482 | If all existing specifications are assigned to the 1.0 series,... |
483 | |
484 | - >>> for spec in mozilla._all_specifications: |
485 | + >>> for spec in mozilla.visible_specifications: |
486 | ... spec.proposeGoal(series_1_0, no_priv) |
487 | |
488 | we have the save five incomplete specs in the series 1.0 as we have for the |
489 | @@ -312,10 +312,10 @@ |
490 | |
491 | >>> firefox.active = True |
492 | |
493 | -We can get all the specifications via the _all_specifications property, |
494 | +We can get all the specifications via the visible_specifications property, |
495 | and all valid specifications via the _valid_specifications property: |
496 | |
497 | - >>> for spec in mozilla_series_1_0._all_specifications: |
498 | + >>> for spec in mozilla_series_1_0.visible_specifications: |
499 | ... print spec.name |
500 | svg-support |
501 | canvas |
502 | |
503 | === modified file 'lib/lp/registry/interfaces/milestone.py' |
504 | --- lib/lp/registry/interfaces/milestone.py 2013-01-07 02:40:55 +0000 |
505 | +++ lib/lp/registry/interfaces/milestone.py 2013-04-03 04:02:25 +0000 |
506 | @@ -1,4 +1,4 @@ |
507 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
508 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
509 | # GNU Affero General Public License version 3 (see the file LICENSE). |
510 | |
511 | """Milestone interfaces.""" |
512 | @@ -135,6 +135,8 @@ |
513 | title = exported( |
514 | TextLine(title=_("A context title for pages."), |
515 | readonly=True)) |
516 | + all_specifications = doNotSnapshot( |
517 | + Attribute('All specifications linked to this milestone.')) |
518 | |
519 | def bugtasks(user): |
520 | """Get a list of non-conjoined bugtasks visible to this user.""" |
521 | |
522 | === modified file 'lib/lp/registry/interfaces/productseries.py' |
523 | --- lib/lp/registry/interfaces/productseries.py 2013-01-07 02:40:55 +0000 |
524 | +++ lib/lp/registry/interfaces/productseries.py 2013-04-03 04:02:25 +0000 |
525 | @@ -168,9 +168,7 @@ |
526 | parent = Attribute('The structural parent of this series - the product') |
527 | |
528 | datecreated = exported( |
529 | - Datetime(title=_('Date Registered'), |
530 | - required=True, |
531 | - readonly=True), |
532 | + Datetime(title=_('Date Registered'), required=True, readonly=True), |
533 | exported_as='date_created') |
534 | |
535 | owner = exported( |
536 | @@ -239,10 +237,8 @@ |
537 | |
538 | branch = exported( |
539 | ReferenceChoice( |
540 | - title=_('Branch'), |
541 | - vocabulary='BranchRestrictedOnProduct', |
542 | - schema=IBranch, |
543 | - required=False, |
544 | + title=_('Branch'), vocabulary='BranchRestrictedOnProduct', |
545 | + schema=IBranch, required=False, |
546 | description=_("The Bazaar branch for this series. Leave blank " |
547 | "if this series is not maintained in Bazaar."))) |
548 | |
549 | @@ -269,6 +265,9 @@ |
550 | "A Bazaar branch to commit translation snapshots to. " |
551 | "Leave blank to disable.")) |
552 | |
553 | + all_specifications = doNotSnapshot( |
554 | + Attribute('All specifications linked to this series.')) |
555 | + |
556 | def getCachedReleases(): |
557 | """Gets a cached copy of this series' releases. |
558 | |
559 | |
560 | === modified file 'lib/lp/registry/model/milestone.py' |
561 | --- lib/lp/registry/model/milestone.py 2013-03-25 05:53:38 +0000 |
562 | +++ lib/lp/registry/model/milestone.py 2013-04-03 04:02:25 +0000 |
563 | @@ -15,6 +15,7 @@ |
564 | |
565 | import datetime |
566 | import httplib |
567 | +from operator import itemgetter |
568 | |
569 | from lazr.restful.declarations import error_status |
570 | from sqlobject import ( |
571 | @@ -67,7 +68,6 @@ |
572 | from lp.services.database.lpstorm import IStore |
573 | from lp.services.database.sqlbase import SQLBase |
574 | from lp.services.propertycache import get_property_cache |
575 | -from lp.services.webapp.interfaces import ILaunchBag |
576 | from lp.services.webapp.sorting import expand_numbers |
577 | |
578 | |
579 | @@ -159,6 +159,11 @@ |
580 | def title(self): |
581 | raise NotImplementedError |
582 | |
583 | + @property |
584 | + def all_specifications(self): |
585 | + return Store.of(self).find( |
586 | + Specification, Specification.milestoneID == self.id) |
587 | + |
588 | def getSpecifications(self, user): |
589 | """See `IMilestoneData`""" |
590 | from lp.registry.model.person import Person |
591 | @@ -186,13 +191,11 @@ |
592 | SpecificationWorkItem.deleted == False)), |
593 | all=True)), |
594 | *clauses) |
595 | - ordered_results = results.order_by(Desc(Specification.priority), |
596 | - Specification.definition_status, |
597 | - Specification.implementation_status, |
598 | - Specification.title) |
599 | + ordered_results = results.order_by( |
600 | + Desc(Specification.priority), Specification.definition_status, |
601 | + Specification.implementation_status, Specification.title) |
602 | ordered_results.config(distinct=True) |
603 | - mapper = lambda row: row[0] |
604 | - return DecoratedResultSet(ordered_results, mapper) |
605 | + return DecoratedResultSet(ordered_results, itemgetter(0)) |
606 | |
607 | def bugtasks(self, user): |
608 | """The list of non-conjoined bugtasks targeted to this milestone.""" |
609 | @@ -307,14 +310,13 @@ |
610 | params = BugTaskSearchParams(milestone=self, user=None) |
611 | bugtasks = getUtility(IBugTaskSet).search(params) |
612 | subscriptions = IResultSet(self.getSubscriptions()) |
613 | - user = getUtility(ILaunchBag).user |
614 | assert subscriptions.is_empty(), ( |
615 | "You cannot delete a milestone which has structural " |
616 | "subscriptions.") |
617 | - assert bugtasks.count() == 0, ( |
618 | + assert bugtasks.is_empty(), ( |
619 | "You cannot delete a milestone which has bugtasks targeted " |
620 | "to it.") |
621 | - assert self.getSpecifications(user).count() == 0, ( |
622 | + assert self.all_specifications.is_empty(), ( |
623 | "You cannot delete a milestone which has specifications targeted " |
624 | "to it.") |
625 | assert self.product_release is None, ( |
626 | |
627 | === modified file 'lib/lp/registry/model/productseries.py' |
628 | --- lib/lp/registry/model/productseries.py 2013-01-22 05:07:31 +0000 |
629 | +++ lib/lp/registry/model/productseries.py 2013-04-03 04:02:25 +0000 |
630 | @@ -332,6 +332,11 @@ |
631 | self, base_clauses, user, sort, quantity, filter, prejoin_people, |
632 | default_acceptance=True) |
633 | |
634 | + @property |
635 | + def all_specifications(self): |
636 | + return Store.of(self).find( |
637 | + Specification, Specification.productseriesID == self.id) |
638 | + |
639 | def _customizeSearchParams(self, search_params): |
640 | """Customize `search_params` for this product series.""" |
641 | search_params.setProductSeries(self) |
642 | |
643 | === modified file 'lib/lp/registry/tests/test_product.py' |
644 | --- lib/lp/registry/tests/test_product.py 2013-02-13 03:50:06 +0000 |
645 | +++ lib/lp/registry/tests/test_product.py 2013-04-03 04:02:25 +0000 |
646 | @@ -812,7 +812,7 @@ |
647 | 'official_blueprints', 'official_codehosting', 'official_malone', |
648 | 'owner', 'parent_subscription_target', 'project', 'title', )), |
649 | 'launchpad.View': set(( |
650 | - '_getOfficialTagClause', '_all_specifications', |
651 | + '_getOfficialTagClause', 'visible_specifications', |
652 | '_valid_specifications', 'active_or_packaged_series', |
653 | 'aliases', 'all_milestones', |
654 | 'allowsTranslationEdits', 'allowsTranslationSuggestions', |
655 | |
656 | === modified file 'lib/lp/registry/tests/test_productseries.py' |
657 | --- lib/lp/registry/tests/test_productseries.py 2012-12-05 09:55:39 +0000 |
658 | +++ lib/lp/registry/tests/test_productseries.py 2013-04-03 04:02:25 +0000 |
659 | @@ -626,7 +626,7 @@ |
660 | 'bugtargetdisplayname', 'bugtarget_parent', 'name', |
661 | 'parent_subscription_target', 'product', 'productID', 'series')), |
662 | 'launchpad.View': set(( |
663 | - '_all_specifications', '_getOfficialTagClause', |
664 | + 'visible_specifications', '_getOfficialTagClause', |
665 | '_valid_specifications', 'active', 'all_milestones', |
666 | 'answers_usage', 'blueprints_usage', 'branch', |
667 | 'bug_reported_acknowledgement', 'bug_reporting_guidelines', |