Merge lp:~wgrant/launchpad/sensible-validate_target into lp:launchpad

Proposed by William Grant
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 13514
Proposed branch: lp:~wgrant/launchpad/sensible-validate_target
Merge into: lp:launchpad
Diff against target: 763 lines (+284/-248)
8 files modified
lib/canonical/launchpad/doc/validation.txt (+0/-105)
lib/canonical/launchpad/interfaces/validation.py (+0/-98)
lib/lp/bugs/browser/bugalsoaffects.py (+20/-22)
lib/lp/bugs/browser/bugtask.py (+16/-13)
lib/lp/bugs/browser/tests/test_bugtask.py (+2/-1)
lib/lp/bugs/model/bugtask.py (+65/-7)
lib/lp/bugs/model/tests/test_bugtask.py (+177/-0)
lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt (+4/-2)
To merge this branch: bzr merge lp:~wgrant/launchpad/sensible-validate_target
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+69175@code.launchpad.net

Commit message

Replace validate_distrotask and valid_upstreamtask with a more general and sensible validate_target.

Description of the change

This branch continues my ruthless campaign to move BugTask.target handling completely into the model. It takes validate_distrotask and valid_upstreamtask, combining them into a single validate_target which can be easily called by transitionToTarget. It now lives in lp.bugs, and has extensive unittests rather than doctests.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

This branch looks good to land. I have a comment that relates to my own recent work. Maybe your thoughts will help us to make a method faster.

> === modified file 'lib/lp/bugs/model/bugtask.py'
> --- lib/lp/bugs/model/bugtask.py 2011-07-22 04:50:51 +0000
> +++ lib/lp/bugs/model/bugtask.py 2011-07-25 23:06:42 +0000
...
> @@ -439,6 +437,65 @@
> return validate_target_attribute(self, attr, value)
>
>
> +def validate_target(bug, target):
> + """Validate a bugtask target against a bug's existing tasks.
> +
> + Checks that no conflicting tasks already exist.
> + """
> + if bug.getBugTask(target):
> + raise IllegalTarget(
> + "A fix for this bug has already been requested for %s"
> + % target.displayname)
> +
> + if IDistributionSourcePackage.providedBy(target):
> + # If the distribution has at least one series, check that the
> + # source package has been published in the distribution.
> + if (target.sourcepackagename is not None and
> + len(target.distribution.series) > 0):
> + try:
> + target.distribution.guessPublishedSourcePackageName(
> + target.sourcepackagename.name)

I believe this is the method that contributes to bugs
https://bugs.launchpad.net/launchpad/+bug/618366 and
https://bugs.launchpad.net/launchpad/+bug/721643

I tried to avoid using it. Do you know why guessPublishedSourcePackageName()
requires a string and does the extra work to get the spn. We already have
an spn half the time. I also want to avoid calling this method if the method
that provided the spn can be trusted to have provide one that was published
in the distro.

Ah, but this method is smart enough to verify the branch for the package.
I like that.

The binary name is in part the fault of the existing vocabs. I hope
Steven's work will make this path in the method obsolete, or at least very
rare.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/doc/validation.txt'
--- lib/canonical/launchpad/doc/validation.txt 2011-02-28 20:46:41 +0000
+++ lib/canonical/launchpad/doc/validation.txt 2011-07-25 23:06:42 +0000
@@ -1,110 +1,5 @@
1= Validation =1= Validation =
22
3The validation interface contains different kinds of validation functions.
4
5== validate_distrotask() ==
6
7The validate_distrotask() function is used to guarantee that distribution
8bugtasks are unique per bug.
9
10 >>> from canonical.launchpad.interfaces.validation import validate_distrotask
11 >>> from lp.bugs.interfaces.bug import IBugSet
12 >>> from lp.registry.interfaces.distribution import IDistributionSet
13 >>> from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
14 >>> bug_two = getUtility(IBugSet).get(2)
15 >>> debian = getUtility(IDistributionSet).getByName('debian')
16 >>> mozilla_firefox = getUtility(
17 ... ISourcePackageNameSet).queryByName('mozilla-firefox')
18 >>> alsa_utils = getUtility(
19 ... ISourcePackageNameSet).queryByName('alsa-utils')
20
21We aren't allowed to have two distrotasks with the same sourcepackage.
22
23 >>> validate_distrotask(bug_two, debian, mozilla_firefox)
24 Traceback (most recent call last):
25 ...
26 LaunchpadValidationError: ...
27
28But it's legal to have two bugtasks with different sourcepackages.
29
30 >>> validate_distrotask(bug_two, debian, alsa_utils)
31
32If the bug already has a distribution task with no source package, it's
33not possible to add a another one.
34
35 >>> from lp.bugs.interfaces.bug import CreateBugParams
36 >>> from lp.registry.interfaces.distribution import IDistributionSet
37
38 >>> login('no-priv@canonical.com')
39 >>> no_priv = getUtility(ILaunchBag).user
40 >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
41 >>> ubuntu_bug_no_sourcepackage = ubuntu.createBug(
42 ... CreateBugParams(no_priv, 'Test Bug', 'This is a test'))
43 >>> validate_distrotask(ubuntu_bug_no_sourcepackage, ubuntu, None)
44 Traceback (most recent call last):
45 ...
46 LaunchpadValidationError: ...
47
48It's also not possible to have a bugtask for a sourcepackage that has
49not been published in the target distribution.
50
51 >>> foobar = getUtility(
52 ... ISourcePackageNameSet).queryByName('foobar')
53 >>> validate_distrotask(ubuntu_bug_no_sourcepackage, ubuntu, foobar)
54 Traceback (most recent call last):
55 ...
56 LaunchpadValidationError: u'Package foobar not published in Ubuntu'
57
58It's not allowed even if the sourcepackage has been published in a
59PPA.
60
61 >>> from lp.soyuz.enums import PackagePublishingStatus
62 >>> from lp.soyuz.tests.ppa import publishToPPA
63 >>> publishToPPA(
64 ... person_name='cprov',
65 ... sourcepackage_name='foobar',
66 ... sourcepackage_version='1.0',
67 ... distribution_name='ubuntu',
68 ... distroseries_name='hoary',
69 ... publishing_status=PackagePublishingStatus.PUBLISHED)
70
71 >>> validate_distrotask(ubuntu_bug_no_sourcepackage, ubuntu, foobar)
72 Traceback (most recent call last):
73 ...
74 LaunchpadValidationError: u'Package foobar not published in Ubuntu'
75
76Adding a task with a source package is still possible.
77
78 >>> validate_distrotask(ubuntu_bug_no_sourcepackage, ubuntu, alsa_utils)
79
80
81== validate_new_distrotask() ==
82
83The validate_new_distrotask() function does the same checks as
84validate_distrotask(), but it does some extra checks as well.
85
86We can't add a task on just the distribution, because it doesn't
87make sense to have tasks open on just the distribution and also packages
88in that distribution on the same bug.
89
90 >>> from canonical.launchpad.interfaces.validation import (
91 ... validate_new_distrotask)
92 >>> validate_new_distrotask(bug_two, debian, None)
93 Traceback (most recent call last):
94 ...
95 LaunchpadValidationError: ...
96
97If the bug already has a distribution with no package, we can't add
98another distribution task with a source package (the task with no
99package should be edited instead).
100
101 >>> validate_new_distrotask(
102 ... ubuntu_bug_no_sourcepackage, ubuntu, alsa_utils)
103 Traceback (most recent call last):
104 ...
105 LaunchpadValidationError: ...
106
107
108== LaunchpadValidationError ==3== LaunchpadValidationError ==
1094
110LaunchpadValidationError is the standard exception used for custom5LaunchpadValidationError is the standard exception used for custom
1116
=== modified file 'lib/canonical/launchpad/interfaces/validation.py'
--- lib/canonical/launchpad/interfaces/validation.py 2011-06-14 20:35:20 +0000
+++ lib/canonical/launchpad/interfaces/validation.py 2011-07-25 23:06:42 +0000
@@ -12,9 +12,6 @@
12 'valid_cve_sequence',12 'valid_cve_sequence',
13 'validate_new_team_email',13 'validate_new_team_email',
14 'validate_new_person_email',14 'validate_new_person_email',
15 'validate_distrotask',
16 'validate_new_distrotask',
17 'valid_upstreamtask',
18 'valid_password',15 'valid_password',
19 'validate_date_interval',16 'validate_date_interval',
20 ]17 ]
@@ -25,8 +22,6 @@
25from zope.app.form.interfaces import WidgetsError22from zope.app.form.interfaces import WidgetsError
26from zope.component import getUtility23from zope.component import getUtility
2724
28from lazr.restful.error import expose
29
30from canonical.launchpad import _25from canonical.launchpad import _
31from canonical.launchpad.interfaces.emailaddress import (26from canonical.launchpad.interfaces.emailaddress import (
32 IEmailAddressSet,27 IEmailAddressSet,
@@ -150,99 +145,6 @@
150 return True145 return True
151146
152147
153def validate_new_distrotask(bug, distribution, sourcepackagename=None):
154 """Validate a distribution bugtask to be added.
155
156 Make sure that the isn't already a distribution task without a
157 source package, or that such task is added only when the bug doesn't
158 already have any tasks for the distribution.
159
160 The same checks as `validate_distrotask` does are also done.
161 """
162 from canonical.launchpad.helpers import shortlist
163
164 if sourcepackagename:
165 # Ensure that there isn't already a generic task open on the
166 # distribution for this bug, because if there were, that task
167 # should be reassigned to the sourcepackage, rather than a new
168 # task opened.
169 if bug.getBugTask(distribution) is not None:
170 raise LaunchpadValidationError(_(
171 'This bug is already open on ${distribution} with no '
172 'package specified. You should fill in a package '
173 'name for the existing bug.',
174 mapping={'distribution': distribution.displayname}))
175 else:
176 # Prevent having a task on only the distribution if there's at
177 # least one task already on the distribution, whether or not
178 # that task also has a source package.
179 distribution_tasks_for_bug = [
180 bugtask for bugtask
181 in shortlist(bug.bugtasks, longest_expected=50)
182 if bugtask.distribution == distribution]
183
184 if len(distribution_tasks_for_bug) > 0:
185 raise LaunchpadValidationError(_(
186 'This bug is already on ${distribution}. Please '
187 'specify an affected package in which the bug '
188 'has not yet been reported.',
189 mapping={'distribution': distribution.displayname}))
190 validate_distrotask(bug, distribution, sourcepackagename)
191
192
193def validate_distrotask(bug, distribution, sourcepackagename=None):
194 """Check if a distribution bugtask already exists for a given bug.
195
196 If validation fails, a LaunchpadValidationError will be raised.
197 """
198 if sourcepackagename is not None and len(distribution.series) > 0:
199 # If the distribution has at least one series, check that the
200 # source package has been published in the distribution.
201 try:
202 distribution.guessPublishedSourcePackageName(
203 sourcepackagename.name)
204 except NotFoundError, e:
205 raise LaunchpadValidationError(e)
206 new_source_package = distribution.getSourcePackage(sourcepackagename)
207 if sourcepackagename is not None and (
208 bug.getBugTask(new_source_package) is not None):
209 # Ensure this distribution/sourcepackage task is unique.
210 raise LaunchpadValidationError(_(
211 'This bug has already been reported on ${source} '
212 '(${distribution}).',
213 mapping={'source': sourcepackagename.name,
214 'distribution': distribution.name}))
215 elif (sourcepackagename is None and
216 bug.getBugTask(distribution) is not None):
217 # Don't allow two distribution tasks with no source package.
218 raise LaunchpadValidationError(_(
219 'This bug has already been reported on ${distribution}.',
220 mapping={'distribution': distribution.name}))
221 else:
222 # The bugtask is valid.
223 pass
224
225
226def valid_upstreamtask(bug, bug_target):
227 """Check if a bugtask already exists for a given bug/target.
228
229 If it exists, WidgetsError will be raised.
230 """
231 # Local import to avoid circular imports.
232 from lp.bugs.interfaces.bugtask import BugTaskSearchParams
233 errors = []
234 user = getUtility(ILaunchBag).user
235 params = BugTaskSearchParams(user, bug=bug)
236 if not bug_target.searchTasks(params).is_empty():
237 errors.append(
238 LaunchpadValidationError(_(
239 'A fix for this bug has already been requested for ${target}',
240 mapping={'target': bug_target.displayname})))
241
242 if len(errors) > 0:
243 raise expose(WidgetsError(errors), 400)
244
245
246def valid_password(password):148def valid_password(password):
247 """Return True if the argument is a valid password.149 """Return True if the argument is a valid password.
248150
249151
=== modified file 'lib/lp/bugs/browser/bugalsoaffects.py'
--- lib/lp/bugs/browser/bugalsoaffects.py 2011-07-13 02:17:29 +0000
+++ lib/lp/bugs/browser/bugalsoaffects.py 2011-07-25 23:06:42 +0000
@@ -16,10 +16,7 @@
16from lazr.lifecycle.event import ObjectCreatedEvent16from lazr.lifecycle.event import ObjectCreatedEvent
17from z3c.ptcompat import ViewPageTemplateFile17from z3c.ptcompat import ViewPageTemplateFile
18from zope.app.form.browser import DropdownWidget18from zope.app.form.browser import DropdownWidget
19from zope.app.form.interfaces import (19from zope.app.form.interfaces import MissingInputError
20 MissingInputError,
21 WidgetsError,
22 )
23from zope.component import getUtility20from zope.component import getUtility
24from zope.event import notify21from zope.event import notify
25from zope.formlib import form22from zope.formlib import form
@@ -34,10 +31,6 @@
34 MultiStepView,31 MultiStepView,
35 StepView,32 StepView,
36 )33 )
37from canonical.launchpad.interfaces.validation import (
38 valid_upstreamtask,
39 validate_new_distrotask,
40 )
41from canonical.launchpad.webapp import canonical_url34from canonical.launchpad.webapp import canonical_url
42from canonical.launchpad.webapp.interfaces import ILaunchBag35from canonical.launchpad.webapp.interfaces import ILaunchBag
43from canonical.launchpad.webapp.menu import structured36from canonical.launchpad.webapp.menu import structured
@@ -62,6 +55,7 @@
62 BugTaskStatus,55 BugTaskStatus,
63 IAddBugTaskForm,56 IAddBugTaskForm,
64 IAddBugTaskWithProductCreationForm,57 IAddBugTaskWithProductCreationForm,
58 IllegalTarget,
65 valid_remote_bug_url,59 valid_remote_bug_url,
66 )60 )
67from lp.bugs.interfaces.bugtracker import (61from lp.bugs.interfaces.bugtracker import (
@@ -73,6 +67,10 @@
73 NoBugTrackerFound,67 NoBugTrackerFound,
74 UnrecognizedBugTrackerURL,68 UnrecognizedBugTrackerURL,
75 )69 )
70from lp.bugs.model.bugtask import (
71 validate_new_target,
72 validate_target,
73 )
76from lp.registry.interfaces.distributionsourcepackage import (74from lp.registry.interfaces.distributionsourcepackage import (
77 IDistributionSourcePackage,75 IDistributionSourcePackage,
78 )76 )
@@ -157,8 +155,8 @@
157 upstream = bugtask.target.upstream_product155 upstream = bugtask.target.upstream_product
158 if upstream is not None:156 if upstream is not None:
159 try:157 try:
160 valid_upstreamtask(bugtask.bug, upstream)158 validate_target(bugtask.bug, upstream)
161 except WidgetsError:159 except IllegalTarget:
162 # There is already a task for the upstream.160 # There is already a task for the upstream.
163 pass161 pass
164 else:162 else:
@@ -173,10 +171,9 @@
173 def validateStep(self, data):171 def validateStep(self, data):
174 if data.get('product'):172 if data.get('product'):
175 try:173 try:
176 valid_upstreamtask(self.context.bug, data.get('product'))174 validate_target(self.context.bug, data.get('product'))
177 except WidgetsError, errors:175 except IllegalTarget as e:
178 for error in errors:176 self.setFieldError('product', e[0])
179 self.setFieldError('product', error.snippet())
180 return177 return
181178
182 entered_product = self.request.form.get(self.widgets['product'].name)179 entered_product = self.request.form.get(self.widgets['product'].name)
@@ -431,10 +428,12 @@
431 self.setFieldError('sourcepackagename', error)428 self.setFieldError('sourcepackagename', error)
432 else:429 else:
433 try:430 try:
434 validate_new_distrotask(431 target = distribution
435 self.context.bug, distribution, sourcepackagename)432 if sourcepackagename:
436 except LaunchpadValidationError, error:433 target = target.getSourcePackage(sourcepackagename)
437 self.setFieldError('sourcepackagename', error.snippet())434 validate_new_target(self.context.bug, target)
435 except IllegalTarget as e:
436 self.setFieldError('sourcepackagename', e[0])
438437
439 super(DistroBugTaskCreationStep, self).validateStep(data)438 super(DistroBugTaskCreationStep, self).validateStep(data)
440439
@@ -812,10 +811,9 @@
812 self._validate(action, data)811 self._validate(action, data)
813 project = data.get('existing_product')812 project = data.get('existing_product')
814 try:813 try:
815 valid_upstreamtask(self.context.bug, project)814 validate_target(self.context.bug, project)
816 except WidgetsError, errors:815 except IllegalTarget as e:
817 for error in errors:816 self.setFieldError('existing_product', e[0])
818 self.setFieldError('existing_product', error.snippet())
819817
820 @action('Use Existing Project', name='use_existing_product',818 @action('Use Existing Project', name='use_existing_product',
821 validator=validate_existing_product)819 validator=validate_existing_product)
822820
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-07-21 06:27:29 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-07-25 23:06:42 +0000
@@ -132,10 +132,6 @@
132 FeedsMixin,132 FeedsMixin,
133 )133 )
134from canonical.launchpad.interfaces.launchpad import IHasExternalBugTracker134from canonical.launchpad.interfaces.launchpad import IHasExternalBugTracker
135from canonical.launchpad.interfaces.validation import (
136 valid_upstreamtask,
137 validate_distrotask,
138 )
139from canonical.launchpad.mailnotification import get_unified_diff135from canonical.launchpad.mailnotification import get_unified_diff
140from canonical.launchpad.searchbuilder import (136from canonical.launchpad.searchbuilder import (
141 all,137 all,
@@ -238,6 +234,7 @@
238 IBugTaskSet,234 IBugTaskSet,
239 ICreateQuestionFromBugTaskForm,235 ICreateQuestionFromBugTaskForm,
240 IFrontPageBugTaskSearch,236 IFrontPageBugTaskSearch,
237 IllegalTarget,
241 INominationsReviewTableBatchNavigator,238 INominationsReviewTableBatchNavigator,
242 IPersonBugTaskSearch,239 IPersonBugTaskSearch,
243 IRemoveQuestionFromBugTaskForm,240 IRemoveQuestionFromBugTaskForm,
@@ -249,6 +246,7 @@
249from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus246from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus
250from lp.bugs.interfaces.cve import ICveSet247from lp.bugs.interfaces.cve import ICveSet
251from lp.bugs.interfaces.malone import IMaloneApplication248from lp.bugs.interfaces.malone import IMaloneApplication
249from lp.bugs.model.bugtask import validate_target
252from lp.registry.interfaces.distribution import (250from lp.registry.interfaces.distribution import (
253 IDistribution,251 IDistribution,
254 IDistributionSet,252 IDistributionSet,
@@ -1372,15 +1370,20 @@
1372 distro = bugtask.distroseries.distribution1370 distro = bugtask.distroseries.distribution
1373 else:1371 else:
1374 distro = bugtask.distribution1372 distro = bugtask.distribution
1375 sourcename = bugtask.sourcepackagename
1376 old_product = bugtask.product1373 old_product = bugtask.product
13771374
1378 if distro is not None and sourcename != data.get('sourcepackagename'):1375 new_spn = data.get('sourcepackagename')
1376 if distro is not None and bugtask.sourcepackagename != new_spn:
1379 try:1377 try:
1380 validate_distrotask(1378 target = distro
1381 bugtask.bug, distro, data.get('sourcepackagename'))1379 if new_spn is not None:
1382 except LaunchpadValidationError, error:1380 target = distro.getSourcePackage(new_spn)
1383 self.setFieldError('sourcepackagename', str(error))1381 validate_target(bugtask.bug, target)
1382 except IllegalTarget as e:
1383 # The field validator may have already set an error.
1384 # Don't clobber it.
1385 if not self.getFieldError('sourcepackagename'):
1386 self.setFieldError('sourcepackagename', e[0])
13841387
1385 new_product = data.get('product')1388 new_product = data.get('product')
1386 if (old_product is None or old_product == new_product or1389 if (old_product is None or old_product == new_product or
@@ -1394,9 +1397,9 @@
1394 self.setFieldError('product', 'Enter a project name')1397 self.setFieldError('product', 'Enter a project name')
1395 else:1398 else:
1396 try:1399 try:
1397 valid_upstreamtask(bugtask.bug, new_product)1400 validate_target(bugtask.bug, new_product)
1398 except WidgetsError, errors:1401 except IllegalTarget as e:
1399 self.setFieldError('product', errors.args[0])1402 self.setFieldError('product', e[0])
14001403
1401 def updateContextFromData(self, data, context=None):1404 def updateContextFromData(self, data, context=None):
1402 """Updates the context object using the submitted form data.1405 """Updates the context object using the submitted form data.
14031406
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-07-18 11:32:51 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-07-25 23:06:42 +0000
@@ -705,7 +705,8 @@
705 bug_task_2, name='+editstatus', form=form, principal=user)705 bug_task_2, name='+editstatus', form=form, principal=user)
706 self.assertEqual(1, len(view.errors))706 self.assertEqual(1, len(view.errors))
707 self.assertEqual(707 self.assertEqual(
708 'This bug has already been reported on mouse (ubuntu).',708 'A fix for this bug has already been requested for mouse in '
709 'Ubuntu',
709 view.errors[0])710 view.errors[0])
710711
711 def setUpRetargetMilestone(self):712 def setUpRetargetMilestone(self):
712713
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py 2011-07-22 04:50:51 +0000
+++ lib/lp/bugs/model/bugtask.py 2011-07-25 23:06:42 +0000
@@ -18,6 +18,8 @@
18 'get_bug_privacy_filter',18 'get_bug_privacy_filter',
19 'get_related_bugtasks_search_params',19 'get_related_bugtasks_search_params',
20 'search_value_to_where_condition',20 'search_value_to_where_condition',
21 'validate_new_target',
22 'validate_target',
21 ]23 ]
2224
2325
@@ -85,10 +87,6 @@
85 )87 )
86from canonical.launchpad.helpers import shortlist88from canonical.launchpad.helpers import shortlist
87from canonical.launchpad.interfaces.lpstorm import IStore89from canonical.launchpad.interfaces.lpstorm import IStore
88from canonical.launchpad.interfaces.validation import (
89 valid_upstreamtask,
90 validate_new_distrotask,
91 )
92from canonical.launchpad.searchbuilder import (90from canonical.launchpad.searchbuilder import (
93 all,91 all,
94 any,92 any,
@@ -439,6 +437,65 @@
439 return validate_target_attribute(self, attr, value)437 return validate_target_attribute(self, attr, value)
440438
441439
440def validate_target(bug, target):
441 """Validate a bugtask target against a bug's existing tasks.
442
443 Checks that no conflicting tasks already exist.
444 """
445 if bug.getBugTask(target):
446 raise IllegalTarget(
447 "A fix for this bug has already been requested for %s"
448 % target.displayname)
449
450 if IDistributionSourcePackage.providedBy(target):
451 # If the distribution has at least one series, check that the
452 # source package has been published in the distribution.
453 if (target.sourcepackagename is not None and
454 len(target.distribution.series) > 0):
455 try:
456 target.distribution.guessPublishedSourcePackageName(
457 target.sourcepackagename.name)
458 except NotFoundError, e:
459 raise IllegalTarget(e[0])
460
461
462def validate_new_target(bug, target):
463 """Validate a bugtask target to be added.
464
465 Make sure that the isn't already a distribution task without a
466 source package, or that such task is added only when the bug doesn't
467 already have any tasks for the distribution.
468
469 The same checks as `validate_target` does are also done.
470 """
471 if IDistribution.providedBy(target):
472 # Prevent having a task on only the distribution if there's at
473 # least one task already on the distribution, whether or not
474 # that task also has a source package.
475 distribution_tasks_for_bug = [
476 bugtask for bugtask
477 in shortlist(bug.bugtasks, longest_expected=50)
478 if bugtask.distribution == target]
479
480 if len(distribution_tasks_for_bug) > 0:
481 raise IllegalTarget(
482 "This bug is already on %s. Please specify an "
483 "affected package in which the bug has not yet "
484 "been reported." % target.displayname)
485 elif IDistributionSourcePackage.providedBy(target):
486 # Ensure that there isn't already a generic task open on the
487 # distribution for this bug, because if there were, that task
488 # should be reassigned to the sourcepackage, rather than a new
489 # task opened.
490 if bug.getBugTask(target.distribution) is not None:
491 raise IllegalTarget(
492 "This bug is already open on %s with no package "
493 "specified. You should fill in a package name for "
494 "the existing bug." % target.distribution.displayname)
495
496 validate_target(bug, target)
497
498
442class BugTask(SQLBase):499class BugTask(SQLBase):
443 """See `IBugTask`."""500 """See `IBugTask`."""
444 implements(IBugTask)501 implements(IBugTask)
@@ -2653,7 +2710,8 @@
2653 elif distribution is not None:2710 elif distribution is not None:
2654 # Make sure there's no bug task already filed against2711 # Make sure there's no bug task already filed against
2655 # this source package in this distribution.2712 # this source package in this distribution.
2656 validate_new_distrotask(bug, distribution, sourcepackagename)2713 validate_new_target(
2714 bug, distribution.getSourcePackage(sourcepackagename))
2657 stop_checking = True2715 stop_checking = True
26582716
2659 if target is None and not stop_checking:2717 if target is None and not stop_checking:
@@ -2670,13 +2728,13 @@
2670 target = distroseries2728 target = distroseries
2671 elif distribution is not None and not stop_checking:2729 elif distribution is not None and not stop_checking:
2672 # Bug filed against a distribution.2730 # Bug filed against a distribution.
2673 validate_new_distrotask(bug, distribution)2731 validate_new_target(bug, distribution)
2674 stop_checking = True2732 stop_checking = True
26752733
2676 if target is not None and not stop_checking:2734 if target is not None and not stop_checking:
2677 # Make sure there's no task for this bug already filed2735 # Make sure there's no task for this bug already filed
2678 # against the target.2736 # against the target.
2679 valid_upstreamtask(bug, target)2737 validate_target(bug, target)
26802738
2681 if not bug.private and bug.security_related:2739 if not bug.private and bug.security_related:
2682 if product and product.security_contact:2740 if product and product.security_contact:
26832741
=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
--- lib/lp/bugs/model/tests/test_bugtask.py 2011-07-22 04:50:51 +0000
+++ lib/lp/bugs/model/tests/test_bugtask.py 2011-07-25 23:06:42 +0000
@@ -42,6 +42,8 @@
42 bug_target_to_key,42 bug_target_to_key,
43 build_tag_search_clause,43 build_tag_search_clause,
44 IllegalTarget,44 IllegalTarget,
45 validate_new_target,
46 validate_target,
45 )47 )
46from lp.bugs.tests.bug import (48from lp.bugs.tests.bug import (
47 create_old_bug,49 create_old_bug,
@@ -57,6 +59,7 @@
57 )59 )
58from lp.registry.interfaces.product import IProductSet60from lp.registry.interfaces.product import IProductSet
59from lp.registry.interfaces.projectgroup import IProjectGroupSet61from lp.registry.interfaces.projectgroup import IProjectGroupSet
62from lp.soyuz.interfaces.archive import ArchivePurpose
60from lp.testing import (63from lp.testing import (
61 ANONYMOUS,64 ANONYMOUS,
62 EventRecorder,65 EventRecorder,
@@ -1831,3 +1834,177 @@
1831 def test_no_target_for_bad_keys(self):1834 def test_no_target_for_bad_keys(self):
1832 self.assertRaises(1835 self.assertRaises(
1833 AssertionError, bug_target_from_key, None, None, None, None, None)1836 AssertionError, bug_target_from_key, None, None, None, None, None)
1837
1838
1839class TestValidateTarget(TestCaseWithFactory):
1840
1841 layer = DatabaseFunctionalLayer
1842
1843 def test_new_product_is_allowed(self):
1844 # A new product not on the bug is OK.
1845 p1 = self.factory.makeProduct()
1846 task = self.factory.makeBugTask(target=p1)
1847 p2 = self.factory.makeProduct()
1848 validate_target(task.bug, p2)
1849
1850 def test_same_product_is_forbidden(self):
1851 # A product with an existing task is not.
1852 p = self.factory.makeProduct()
1853 task = self.factory.makeBugTask(target=p)
1854 self.assertRaisesWithContent(
1855 IllegalTarget,
1856 "A fix for this bug has already been requested for %s"
1857 % p.displayname,
1858 validate_target, task.bug, p)
1859
1860 def test_new_distribution_is_allowed(self):
1861 # A new distribution not on the bug is OK.
1862 d1 = self.factory.makeDistribution()
1863 task = self.factory.makeBugTask(target=d1)
1864 d2 = self.factory.makeDistribution()
1865 validate_target(task.bug, d2)
1866
1867 def test_new_productseries_is_allowed(self):
1868 # A new productseries not on the bug is OK.
1869 ds1 = self.factory.makeProductSeries()
1870 task = self.factory.makeBugTask(target=ds1)
1871 ds2 = self.factory.makeProductSeries()
1872 validate_target(task.bug, ds2)
1873
1874 def test_new_distroseries_is_allowed(self):
1875 # A new distroseries not on the bug is OK.
1876 ds1 = self.factory.makeDistroSeries()
1877 task = self.factory.makeBugTask(target=ds1)
1878 ds2 = self.factory.makeDistroSeries()
1879 validate_target(task.bug, ds2)
1880
1881 def test_new_sourcepackage_is_allowed(self):
1882 # A new sourcepackage not on the bug is OK.
1883 sp1 = self.factory.makeSourcePackage()
1884 task = self.factory.makeBugTask(target=sp1)
1885 sp2 = self.factory.makeSourcePackage()
1886 validate_target(task.bug, sp2)
1887
1888 def test_multiple_packageless_distribution_tasks_are_forbidden(self):
1889 # A distribution with an existing task is not.
1890 d = self.factory.makeDistribution()
1891 task = self.factory.makeBugTask(target=d)
1892 self.assertRaisesWithContent(
1893 IllegalTarget,
1894 "A fix for this bug has already been requested for %s"
1895 % d.displayname,
1896 validate_target, task.bug, d)
1897
1898 def test_distributionsourcepackage_task_is_allowed(self):
1899 # A DistributionSourcePackage task can coexist with a task for
1900 # its Distribution.
1901 d = self.factory.makeDistribution()
1902 task = self.factory.makeBugTask(target=d)
1903 dsp = self.factory.makeDistributionSourcePackage(distribution=d)
1904 validate_target(task.bug, dsp)
1905
1906 def test_different_distributionsourcepackage_tasks_are_allowed(self):
1907 # A DistributionSourcePackage task can also coexist with a task
1908 # for another one.
1909 dsp1 = self.factory.makeDistributionSourcePackage()
1910 task = self.factory.makeBugTask(target=dsp1)
1911 dsp2 = self.factory.makeDistributionSourcePackage(
1912 distribution=dsp1.distribution)
1913 validate_target(task.bug, dsp2)
1914
1915 def test_same_distributionsourcepackage_task_is_forbidden(self):
1916 # But a DistributionSourcePackage task cannot coexist with a
1917 # task for itself.
1918 dsp = self.factory.makeDistributionSourcePackage()
1919 task = self.factory.makeBugTask(target=dsp)
1920 self.assertRaisesWithContent(
1921 IllegalTarget,
1922 "A fix for this bug has already been requested for %s in %s"
1923 % (dsp.sourcepackagename.name, dsp.distribution.displayname),
1924 validate_target, task.bug, dsp)
1925
1926 def test_dsp_without_publications_disallowed(self):
1927 # If a distribution has series, a DistributionSourcePackage task
1928 # can only be created if the package is published in a distro
1929 # archive.
1930 series = self.factory.makeDistroSeries()
1931 dsp = self.factory.makeDistributionSourcePackage(
1932 distribution=series.distribution)
1933 task = self.factory.makeBugTask()
1934 self.assertRaisesWithContent(
1935 IllegalTarget,
1936 "Package %s not published in %s"
1937 % (dsp.sourcepackagename.name, dsp.distribution.displayname),
1938 validate_target, task.bug, dsp)
1939
1940 def test_dsp_with_publications_allowed(self):
1941 # If a distribution has series, a DistributionSourcePackage task
1942 # can only be created if the package is published in a distro
1943 # archive.
1944 series = self.factory.makeDistroSeries()
1945 dsp = self.factory.makeDistributionSourcePackage(
1946 distribution=series.distribution)
1947 task = self.factory.makeBugTask()
1948 self.factory.makeSourcePackagePublishingHistory(
1949 distroseries=series, sourcepackagename=dsp.sourcepackagename,
1950 archive=series.main_archive)
1951 validate_target(task.bug, dsp)
1952
1953 def test_dsp_with_only_ppa_publications_disallowed(self):
1954 # If a distribution has series, a DistributionSourcePackage task
1955 # can only be created if the package is published in a distro
1956 # archive. PPA publications don't count.
1957 series = self.factory.makeDistroSeries()
1958 dsp = self.factory.makeDistributionSourcePackage(
1959 distribution=series.distribution)
1960 task = self.factory.makeBugTask()
1961 self.factory.makeSourcePackagePublishingHistory(
1962 distroseries=series, sourcepackagename=dsp.sourcepackagename,
1963 archive=self.factory.makeArchive(purpose=ArchivePurpose.PPA))
1964 self.assertRaisesWithContent(
1965 IllegalTarget,
1966 "Package %s not published in %s"
1967 % (dsp.sourcepackagename.name, dsp.distribution.displayname),
1968 validate_target, task.bug, dsp)
1969
1970
1971class TestValidateNewTarget(TestCaseWithFactory):
1972
1973 layer = DatabaseFunctionalLayer
1974
1975 def test_products_are_ok(self):
1976 p1 = self.factory.makeProduct()
1977 task = self.factory.makeBugTask(target=p1)
1978 p2 = self.factory.makeProduct()
1979 validate_new_target(task.bug, p2)
1980
1981 def test_calls_validate_target(self):
1982 p = self.factory.makeProduct()
1983 task = self.factory.makeBugTask(target=p)
1984 self.assertRaisesWithContent(
1985 IllegalTarget,
1986 "A fix for this bug has already been requested for %s"
1987 % p.displayname,
1988 validate_new_target, task.bug, p)
1989
1990 def test_package_task_with_distribution_task_forbidden(self):
1991 d = self.factory.makeDistribution()
1992 dsp = self.factory.makeDistributionSourcePackage(distribution=d)
1993 task = self.factory.makeBugTask(target=d)
1994 self.assertRaisesWithContent(
1995 IllegalTarget,
1996 "This bug is already open on %s with no package specified. "
1997 "You should fill in a package name for the existing bug."
1998 % d.displayname,
1999 validate_new_target, task.bug, dsp)
2000
2001 def test_distribution_task_with_package_task_forbidden(self):
2002 d = self.factory.makeDistribution()
2003 dsp = self.factory.makeDistributionSourcePackage(distribution=d)
2004 task = self.factory.makeBugTask(target=dsp)
2005 self.assertRaisesWithContent(
2006 IllegalTarget,
2007 "This bug is already on %s. Please specify an affected "
2008 "package in which the bug has not yet been reported."
2009 % d.displayname,
2010 validate_new_target, task.bug, d)
18342011
=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt'
--- lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt 2011-07-06 19:37:54 +0000
+++ lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt 2011-07-25 23:06:42 +0000
@@ -76,7 +76,8 @@
76 >>> browser.getControl("Save Changes").click()76 >>> browser.getControl("Save Changes").click()
77 >>> print get_feedback_messages(browser.contents)77 >>> print get_feedback_messages(browser.contents)
78 [...There is 1 error in the data you entered...78 [...There is 1 error in the data you entered...
79 u'This bug has already been reported on evolution (ubuntu).']79 u'A fix for this bug has already been requested for evolution in
80 Ubuntu']
8081
81Now let's add a Debian task to bug 1. Since Debian doesn't use Launchpad,82Now let's add a Debian task to bug 1. Since Debian doesn't use Launchpad,
82we add a bug watch as well.83we add a bug watch as well.
@@ -131,7 +132,8 @@
131 'http://bugs.../ubuntu/+source/mozilla-firefox/+bug/1/+editstatus'132 'http://bugs.../ubuntu/+source/mozilla-firefox/+bug/1/+editstatus'
132 >>> print get_feedback_messages(browser.contents)133 >>> print get_feedback_messages(browser.contents)
133 [...There is 1 error in the data you entered...134 [...There is 1 error in the data you entered...
134 u'This bug has already been reported on alsa-utils (ubuntu).']135 u'A fix for this bug has already been requested for alsa-utils in
136 Ubuntu']
135137
136 >>> browser.getControl('Package').value = 'pmount'138 >>> browser.getControl('Package').value = 'pmount'
137 >>> browser.getControl('Save Changes').click()139 >>> browser.getControl('Save Changes').click()