Merge lp:~stevenk/launchpad/workitems-delete-series into lp:launchpad

Proposed by Steve Kowalik
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
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.

IHasSpecifications._all_specifications has been renamed to visible_specifications since it filters by the current logged in user via ILaunchBag, and left exported as all_specifications.

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.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/_schema_circular_imports.py'
2--- lib/lp/_schema_circular_imports.py 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',