Merge lp:~wgrant/launchpad/retarget-bugtask-model into lp:launchpad

Proposed by William Grant
Status: Merged
Approved by: William Grant
Approved revision: no longer in the source branch.
Merged at revision: 13494
Proposed branch: lp:~wgrant/launchpad/retarget-bugtask-model
Merge into: lp:launchpad
Prerequisite: lp:~wgrant/launchpad/destroy-bugtask-markers
Diff against target: 445 lines (+257/-58)
5 files modified
lib/lp/bugs/model/bugtask.py (+61/-23)
lib/lp/bugs/model/tests/test_bugtask.py (+194/-1)
lib/lp/bugs/scripts/bugtasktargetnamecaches.py (+2/-2)
lib/lp/bugs/stories/webservice/xx-bug.txt (+0/-31)
lib/lp/bugs/tests/test_bugnotification.py (+0/-1)
To merge this branch: bzr merge lp:~wgrant/launchpad/retarget-bugtask-model
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code Approve
Review via email: mp+68761@code.launchpad.net

Commit message

[r=stevenk][bug=80902][incr] Allow BugTask.transitionToTarget to transition between all non-series targets.

Description of the change

This branch extends BugTask.transitionToTarget to permit most changes. It was previously restricted to not changing the type of a task (eg. product->product, distribution->distributionsourcepackage) because of the IUpstreamBugTask, IDistroBugTask, etc. marker interfaces. But the prerequisite branches remove those, so changing task types is fine now.

transitionToTarget now uses a new bug_target_to_key function, which converts an IBugTarget to a dict of column values. This is the inverse of the existing determine_target, which has been renamed to bug_target_from_key.

I've added tests for the various cases. There are only three that are disallowed now: transition to or from a productseries; transition to or from a distroseries; and transition to or from a sourcepackage, except where it is to or from another sourcepackage in the same distroseries. Transitioning between series is forbidden, as that's what nominations are for.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) :
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/bugs/model/bugtask.py'
2--- lib/lp/bugs/model/bugtask.py 2011-07-22 04:36:14 +0000
3+++ lib/lp/bugs/model/bugtask.py 2011-07-22 04:53:42 +0000
4@@ -13,7 +13,8 @@
5 'BugTask',
6 'BugTaskSet',
7 'bugtask_sort_key',
8- 'determine_target',
9+ 'bug_target_from_key',
10+ 'bug_target_to_key',
11 'get_bug_privacy_filter',
12 'get_related_bugtasks_search_params',
13 'search_value_to_where_condition',
14@@ -266,9 +267,9 @@
15 return search_params
16
17
18-def determine_target(product, productseries, distribution, distroseries,
19- sourcepackagename):
20- """Returns the IBugTarget defined by the given arguments."""
21+def bug_target_from_key(product, productseries, distribution, distroseries,
22+ sourcepackagename):
23+ """Returns the IBugTarget defined by the given DB column values."""
24 if product:
25 return product
26 elif productseries:
27@@ -289,6 +290,34 @@
28 raise AssertionError("Unable to determine bugtask target.")
29
30
31+def bug_target_to_key(target):
32+ """Returns the DB column values for an IBugTarget."""
33+ values = dict(
34+ product=None,
35+ productseries=None,
36+ distribution=None,
37+ distroseries=None,
38+ sourcepackagename=None,
39+ )
40+ if IProduct.providedBy(target):
41+ values['product'] = target
42+ elif IProductSeries.providedBy(target):
43+ values['productseries'] = target
44+ elif IDistribution.providedBy(target):
45+ values['distribution'] = target
46+ elif IDistroSeries.providedBy(target):
47+ values['distroseries'] = target
48+ elif IDistributionSourcePackage.providedBy(target):
49+ values['distribution'] = target.distribution
50+ values['sourcepackagename'] = target.sourcepackagename
51+ elif ISourcePackage.providedBy(target):
52+ values['distroseries'] = target.distroseries
53+ values['sourcepackagename'] = target.sourcepackagename
54+ else:
55+ raise AssertionError("Not an IBugTarget.")
56+ return values
57+
58+
59 class BugTaskDelta:
60 """See `IBugTaskDelta`."""
61
62@@ -317,7 +346,7 @@
63 def validate_target_attribute(self, attr, value):
64 """Update the targetnamecache."""
65 # Don't update targetnamecache during _init().
66- if self._SO_creating:
67+ if self._SO_creating or self._inhibit_target_check:
68 return value
69 # Determine the new target attributes.
70 target_params = dict(
71@@ -342,7 +371,7 @@
72 # Update the target name cache with the potential new target. The
73 # attribute changes haven't been made yet, so we need to calculate the
74 # target manually.
75- self.updateTargetNameCache(determine_target(**target_params))
76+ self.updateTargetNameCache(bug_target_from_key(**target_params))
77
78 return value
79
80@@ -424,6 +453,8 @@
81 "date_left_closed")
82 _NON_CONJOINED_STATUSES = (BugTaskStatus.WONTFIX, )
83
84+ _inhibit_target_check = False
85+
86 bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
87 product = ForeignKey(
88 dbName='product', foreignKey='Product',
89@@ -523,7 +554,7 @@
90 @property
91 def target(self):
92 """See `IBugTask`."""
93- return determine_target(
94+ return bug_target_from_key(
95 self.product, self.productseries, self.distribution,
96 self.distroseries, self.sourcepackagename)
97
98@@ -1068,22 +1099,29 @@
99 # current target, or reset it to None
100 self.milestone = None
101
102- if self.product:
103- if IProduct.providedBy(target):
104- self.product = target
105- else:
106- raise IllegalTarget(
107- "Upstream bug tasks may only be re-targeted "
108- "to another project.")
109- else:
110- if (IDistributionSourcePackage.providedBy(target) and
111- (target.distribution == self.target or
112- target.distribution == self.target.distribution)):
113- self.sourcepackagename = target.sourcepackagename
114- else:
115- raise IllegalTarget(
116- "Distribution bug tasks may only be re-targeted "
117- "to a package in the same distribution.")
118+ # Check if any series are involved. You can't retarget series
119+ # tasks. Except for SourcePackage tasks, which can only be
120+ # retargetted to another SourcePackage in the same DistroSeries.
121+ interfaces = set(providedBy(target))
122+ interfaces.update(providedBy(self.target))
123+ if interfaces.intersection((IProductSeries, IDistroSeries)):
124+ raise IllegalTarget(
125+ "Series tasks may only be created by approving nominations.")
126+ elif ISourcePackage in interfaces:
127+ if (not ISourcePackage.providedBy(target) or
128+ not ISourcePackage.providedBy(self.target) or
129+ target.distroseries != self.target.distroseries):
130+ raise IllegalTarget(
131+ "Series source package tasks may only be retargetted "
132+ "to another source package in the same series.")
133+
134+ # Inhibit validate_target_attribute, as we can't set them all
135+ # atomically, but we know the final result is correct.
136+ self._inhibit_target_check = True
137+ for name, value in bug_target_to_key(target).iteritems():
138+ setattr(self, name, value)
139+ self._inhibit_target_check = False
140+ self.updateTargetNameCache()
141
142 # After the target has changed, we need to recalculate the maximum bug
143 # heat for the new and old targets.
144
145=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
146--- lib/lp/bugs/model/tests/test_bugtask.py 2011-07-22 04:34:34 +0000
147+++ lib/lp/bugs/model/tests/test_bugtask.py 2011-07-22 04:53:42 +0000
148@@ -6,9 +6,11 @@
149 from datetime import timedelta
150 import unittest
151
152+from lazr.lifecycle.event import ObjectModifiedEvent
153 from lazr.lifecycle.snapshot import Snapshot
154 from testtools.matchers import Equals
155 from zope.component import getUtility
156+from zope.event import notify
157 from zope.interface import providedBy
158 from zope.security.proxy import removeSecurityProxy
159
160@@ -35,7 +37,12 @@
161 UNRESOLVED_BUGTASK_STATUSES,
162 )
163 from lp.bugs.interfaces.bugwatch import IBugWatchSet
164-from lp.bugs.model.bugtask import build_tag_search_clause
165+from lp.bugs.model.bugtask import (
166+ bug_target_from_key,
167+ bug_target_to_key,
168+ build_tag_search_clause,
169+ IllegalTarget,
170+ )
171 from lp.bugs.tests.bug import (
172 create_old_bug,
173 )
174@@ -1638,3 +1645,189 @@
175 bug_task.maybeConfirm()
176 self.assertEqual(BugTaskStatus.CONFIRMED, bug_task.status)
177 self.assertEqual(0, len(recorder.events))
178+
179+
180+class TestTransitionToTarget(TestCaseWithFactory):
181+ """Tests for BugTask.transitionToTarget."""
182+
183+ layer = DatabaseFunctionalLayer
184+
185+ def makeAndTransition(self, old, new):
186+ task = self.factory.makeBugTask(target=old)
187+ p = self.factory.makePerson()
188+ self.assertEqual(old, task.target)
189+ old_state = Snapshot(task, providing=providedBy(task))
190+ with person_logged_in(task.owner):
191+ task.bug.subscribe(p, p)
192+ task.transitionToTarget(new)
193+ notify(ObjectModifiedEvent(task, old_state, ["target"]))
194+ return task
195+
196+ def assertTransitionWorks(self, a, b):
197+ """Check that a transition between two targets works both ways."""
198+ self.assertEqual(b, self.makeAndTransition(a, b).target)
199+ self.assertEqual(a, self.makeAndTransition(b, a).target)
200+
201+ def assertTransitionForbidden(self, a, b):
202+ """Check that a transition between two targets fails both ways."""
203+ self.assertRaises(IllegalTarget, self.makeAndTransition, a, b)
204+ self.assertRaises(IllegalTarget, self.makeAndTransition, b, a)
205+
206+ def test_product_to_product_works(self):
207+ self.assertTransitionWorks(
208+ self.factory.makeProduct(),
209+ self.factory.makeProduct())
210+
211+ def test_product_to_distribution_works(self):
212+ self.assertTransitionWorks(
213+ self.factory.makeProduct(),
214+ self.factory.makeDistributionSourcePackage())
215+
216+ def test_product_to_package_works(self):
217+ self.assertTransitionWorks(
218+ self.factory.makeProduct(),
219+ self.factory.makeDistributionSourcePackage())
220+
221+ def test_distribution_to_distribution_works(self):
222+ self.assertTransitionWorks(
223+ self.factory.makeDistribution(),
224+ self.factory.makeDistribution())
225+
226+ def test_distribution_to_package_works(self):
227+ distro = self.factory.makeDistribution()
228+ dsp = self.factory.makeDistributionSourcePackage(distribution=distro)
229+ self.assertEquals(dsp.distribution, distro)
230+ self.assertTransitionWorks(distro, dsp)
231+
232+ def test_package_to_package_works(self):
233+ distro = self.factory.makeDistribution()
234+ self.assertTransitionWorks(
235+ self.factory.makeDistributionSourcePackage(distribution=distro),
236+ self.factory.makeDistributionSourcePackage(distribution=distro))
237+
238+ def test_sourcepackage_to_sourcepackage_in_same_series_works(self):
239+ sp1 = self.factory.makeSourcePackage()
240+ sp2 = self.factory.makeSourcePackage(distroseries=sp1.distroseries)
241+ self.assertTransitionWorks(sp1, sp2)
242+
243+ def test_different_distros_works(self):
244+ self.assertTransitionWorks(
245+ self.factory.makeDistributionSourcePackage(),
246+ self.factory.makeDistributionSourcePackage())
247+
248+ def test_cannot_transition_to_productseries(self):
249+ product = self.factory.makeProduct()
250+ self.assertTransitionForbidden(
251+ product,
252+ self.factory.makeProductSeries(product=product))
253+
254+ def test_cannot_transition_to_distroseries(self):
255+ distro = self.factory.makeDistribution()
256+ series = self.factory.makeDistroSeries(distribution=distro)
257+ self.assertTransitionForbidden(distro, series)
258+
259+ def test_cannot_transition_to_sourcepackage(self):
260+ dsp = self.factory.makeDistributionSourcePackage()
261+ series = self.factory.makeDistroSeries(distribution=dsp.distribution)
262+ sp = self.factory.makeSourcePackage(
263+ distroseries=series, sourcepackagename=dsp.sourcepackagename)
264+ self.assertTransitionForbidden(dsp, sp)
265+
266+ def test_cannot_transition_to_sourcepackage_in_different_series(self):
267+ distro = self.factory.makeDistribution()
268+ ds1 = self.factory.makeDistroSeries(distribution=distro)
269+ sp1 = self.factory.makeSourcePackage(distroseries=ds1)
270+ ds2 = self.factory.makeDistroSeries(distribution=distro)
271+ sp2 = self.factory.makeSourcePackage(distroseries=ds2)
272+ self.assertTransitionForbidden(sp1, sp2)
273+
274+
275+class TestBugTargetKeys(TestCaseWithFactory):
276+ """Tests for bug_target_to_key and bug_target_from_key."""
277+
278+ layer = DatabaseFunctionalLayer
279+
280+ def assertTargetKeyWorks(self, target, flat):
281+ """Check that a target flattens to the dict and back."""
282+ self.assertEqual(flat, bug_target_to_key(target))
283+ self.assertEqual(target, bug_target_from_key(**flat))
284+
285+ def test_product(self):
286+ product = self.factory.makeProduct()
287+ self.assertTargetKeyWorks(
288+ product,
289+ dict(
290+ product=product,
291+ productseries=None,
292+ distribution=None,
293+ distroseries=None,
294+ sourcepackagename=None,
295+ ))
296+
297+ def test_productseries(self):
298+ series = self.factory.makeProductSeries()
299+ self.assertTargetKeyWorks(
300+ series,
301+ dict(
302+ product=None,
303+ productseries=series,
304+ distribution=None,
305+ distroseries=None,
306+ sourcepackagename=None,
307+ ))
308+
309+ def test_distribution(self):
310+ distro = self.factory.makeDistribution()
311+ self.assertTargetKeyWorks(
312+ distro,
313+ dict(
314+ product=None,
315+ productseries=None,
316+ distribution=distro,
317+ distroseries=None,
318+ sourcepackagename=None,
319+ ))
320+
321+ def test_distroseries(self):
322+ distroseries = self.factory.makeDistroSeries()
323+ self.assertTargetKeyWorks(
324+ distroseries,
325+ dict(
326+ product=None,
327+ productseries=None,
328+ distribution=None,
329+ distroseries=distroseries,
330+ sourcepackagename=None,
331+ ))
332+
333+ def test_distributionsourcepackage(self):
334+ dsp = self.factory.makeDistributionSourcePackage()
335+ self.assertTargetKeyWorks(
336+ dsp,
337+ dict(
338+ product=None,
339+ productseries=None,
340+ distribution=dsp.distribution,
341+ distroseries=None,
342+ sourcepackagename=dsp.sourcepackagename,
343+ ))
344+
345+ def test_sourcepackage(self):
346+ sp = self.factory.makeSourcePackage()
347+ self.assertTargetKeyWorks(
348+ sp,
349+ dict(
350+ product=None,
351+ productseries=None,
352+ distribution=None,
353+ distroseries=sp.distroseries,
354+ sourcepackagename=sp.sourcepackagename,
355+ ))
356+
357+ def test_no_key_for_non_targets(self):
358+ self.assertRaises(
359+ AssertionError, bug_target_to_key, self.factory.makePerson())
360+
361+ def test_no_target_for_bad_keys(self):
362+ self.assertRaises(
363+ AssertionError, bug_target_from_key, None, None, None, None, None)
364
365=== modified file 'lib/lp/bugs/scripts/bugtasktargetnamecaches.py'
366--- lib/lp/bugs/scripts/bugtasktargetnamecaches.py 2011-02-14 22:26:18 +0000
367+++ lib/lp/bugs/scripts/bugtasktargetnamecaches.py 2011-07-22 04:53:42 +0000
368@@ -18,7 +18,7 @@
369 from canonical.launchpad.utilities.looptuner import LoopTuner
370 from lp.bugs.model.bugtask import (
371 BugTask,
372- determine_target,
373+ bug_target_from_key,
374 )
375 from lp.registry.model.distribution import Distribution
376 from lp.registry.model.distroseries import DistroSeries
377@@ -104,7 +104,7 @@
378 target_objects = (
379 (store.get(cls, id) if id is not None else None)
380 for cls, id in zip(target_classes, target_bits))
381- target = determine_target(*target_objects)
382+ target = bug_target_from_key(*target_objects)
383 new_name = target.bugtargetdisplayname
384 cached_names.discard(new_name)
385 # If there are any outdated names cached, update them all in
386
387=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
388--- lib/lp/bugs/stories/webservice/xx-bug.txt 2011-07-13 06:08:16 +0000
389+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2011-07-22 04:53:42 +0000
390@@ -477,27 +477,6 @@
391 >>> print task['target_link']
392 http://api.../debian/+source/alsa-utils
393
394-Just like in the web UI, we can't change a distribution task
395-into an upstream task.
396-
397- >>> print webservice.named_post(
398- ... task['self_link'], 'transitionToTarget',
399- ... target=webservice.getAbsoluteUrl('/bzr'))
400- HTTP/1.1 400 Bad Request
401- ...
402- Distribution bug tasks may only be re-targeted to
403- a package in the same distribution.
404-
405-We can't move a task from one distribution to another.
406-
407- >>> print webservice.named_post(
408- ... task['self_link'], 'transitionToTarget',
409- ... target=webservice.getAbsoluteUrl('/ubuntu/+source/alsa-utils'))
410- HTTP/1.1 400 Bad Request
411- ...
412- Distribution bug tasks may only be re-targeted to
413- a package in the same distribution.
414-
415 We can change an upstream task to target a different project.
416
417 >>> product_bugtask = webservice.get(
418@@ -508,16 +487,6 @@
419 HTTP/1.1 200 Ok
420 ...
421
422-But we can't make it into a distro task.
423-
424- >>> print webservice.named_post(
425- ... product_bugtask['self_link'].replace('jokosher', 'bzr'),
426- ... 'transitionToTarget',
427- ... target=webservice.getAbsoluteUrl('/ubuntu/+source/alsa-utils'))
428- HTTP/1.1 400 Bad Request
429- ...
430- Upstream bug tasks may only be re-targeted to another project.
431-
432 If the milestone of a task is on a target other than the new
433 target, we reset it in order to avoid data inconsistencies.
434
435
436=== modified file 'lib/lp/bugs/tests/test_bugnotification.py'
437--- lib/lp/bugs/tests/test_bugnotification.py 2011-07-21 07:11:09 +0000
438+++ lib/lp/bugs/tests/test_bugnotification.py 2011-07-22 04:53:42 +0000
439@@ -13,7 +13,6 @@
440 from lazr.lifecycle.snapshot import Snapshot
441 from storm.store import Store
442 from testtools.matchers import Not
443-from zope.component import getUtility
444 from zope.event import notify
445 from zope.interface import providedBy
446