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
1=== modified file 'lib/canonical/launchpad/doc/validation.txt'
2--- lib/canonical/launchpad/doc/validation.txt 2011-02-28 20:46:41 +0000
3+++ lib/canonical/launchpad/doc/validation.txt 2011-07-25 23:06:42 +0000
4@@ -1,110 +1,5 @@
5 = Validation =
6
7-The validation interface contains different kinds of validation functions.
8-
9-== validate_distrotask() ==
10-
11-The validate_distrotask() function is used to guarantee that distribution
12-bugtasks are unique per bug.
13-
14- >>> from canonical.launchpad.interfaces.validation import validate_distrotask
15- >>> from lp.bugs.interfaces.bug import IBugSet
16- >>> from lp.registry.interfaces.distribution import IDistributionSet
17- >>> from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
18- >>> bug_two = getUtility(IBugSet).get(2)
19- >>> debian = getUtility(IDistributionSet).getByName('debian')
20- >>> mozilla_firefox = getUtility(
21- ... ISourcePackageNameSet).queryByName('mozilla-firefox')
22- >>> alsa_utils = getUtility(
23- ... ISourcePackageNameSet).queryByName('alsa-utils')
24-
25-We aren't allowed to have two distrotasks with the same sourcepackage.
26-
27- >>> validate_distrotask(bug_two, debian, mozilla_firefox)
28- Traceback (most recent call last):
29- ...
30- LaunchpadValidationError: ...
31-
32-But it's legal to have two bugtasks with different sourcepackages.
33-
34- >>> validate_distrotask(bug_two, debian, alsa_utils)
35-
36-If the bug already has a distribution task with no source package, it's
37-not possible to add a another one.
38-
39- >>> from lp.bugs.interfaces.bug import CreateBugParams
40- >>> from lp.registry.interfaces.distribution import IDistributionSet
41-
42- >>> login('no-priv@canonical.com')
43- >>> no_priv = getUtility(ILaunchBag).user
44- >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
45- >>> ubuntu_bug_no_sourcepackage = ubuntu.createBug(
46- ... CreateBugParams(no_priv, 'Test Bug', 'This is a test'))
47- >>> validate_distrotask(ubuntu_bug_no_sourcepackage, ubuntu, None)
48- Traceback (most recent call last):
49- ...
50- LaunchpadValidationError: ...
51-
52-It's also not possible to have a bugtask for a sourcepackage that has
53-not been published in the target distribution.
54-
55- >>> foobar = getUtility(
56- ... ISourcePackageNameSet).queryByName('foobar')
57- >>> validate_distrotask(ubuntu_bug_no_sourcepackage, ubuntu, foobar)
58- Traceback (most recent call last):
59- ...
60- LaunchpadValidationError: u'Package foobar not published in Ubuntu'
61-
62-It's not allowed even if the sourcepackage has been published in a
63-PPA.
64-
65- >>> from lp.soyuz.enums import PackagePublishingStatus
66- >>> from lp.soyuz.tests.ppa import publishToPPA
67- >>> publishToPPA(
68- ... person_name='cprov',
69- ... sourcepackage_name='foobar',
70- ... sourcepackage_version='1.0',
71- ... distribution_name='ubuntu',
72- ... distroseries_name='hoary',
73- ... publishing_status=PackagePublishingStatus.PUBLISHED)
74-
75- >>> validate_distrotask(ubuntu_bug_no_sourcepackage, ubuntu, foobar)
76- Traceback (most recent call last):
77- ...
78- LaunchpadValidationError: u'Package foobar not published in Ubuntu'
79-
80-Adding a task with a source package is still possible.
81-
82- >>> validate_distrotask(ubuntu_bug_no_sourcepackage, ubuntu, alsa_utils)
83-
84-
85-== validate_new_distrotask() ==
86-
87-The validate_new_distrotask() function does the same checks as
88-validate_distrotask(), but it does some extra checks as well.
89-
90-We can't add a task on just the distribution, because it doesn't
91-make sense to have tasks open on just the distribution and also packages
92-in that distribution on the same bug.
93-
94- >>> from canonical.launchpad.interfaces.validation import (
95- ... validate_new_distrotask)
96- >>> validate_new_distrotask(bug_two, debian, None)
97- Traceback (most recent call last):
98- ...
99- LaunchpadValidationError: ...
100-
101-If the bug already has a distribution with no package, we can't add
102-another distribution task with a source package (the task with no
103-package should be edited instead).
104-
105- >>> validate_new_distrotask(
106- ... ubuntu_bug_no_sourcepackage, ubuntu, alsa_utils)
107- Traceback (most recent call last):
108- ...
109- LaunchpadValidationError: ...
110-
111-
112 == LaunchpadValidationError ==
113
114 LaunchpadValidationError is the standard exception used for custom
115
116=== modified file 'lib/canonical/launchpad/interfaces/validation.py'
117--- lib/canonical/launchpad/interfaces/validation.py 2011-06-14 20:35:20 +0000
118+++ lib/canonical/launchpad/interfaces/validation.py 2011-07-25 23:06:42 +0000
119@@ -12,9 +12,6 @@
120 'valid_cve_sequence',
121 'validate_new_team_email',
122 'validate_new_person_email',
123- 'validate_distrotask',
124- 'validate_new_distrotask',
125- 'valid_upstreamtask',
126 'valid_password',
127 'validate_date_interval',
128 ]
129@@ -25,8 +22,6 @@
130 from zope.app.form.interfaces import WidgetsError
131 from zope.component import getUtility
132
133-from lazr.restful.error import expose
134-
135 from canonical.launchpad import _
136 from canonical.launchpad.interfaces.emailaddress import (
137 IEmailAddressSet,
138@@ -150,99 +145,6 @@
139 return True
140
141
142-def validate_new_distrotask(bug, distribution, sourcepackagename=None):
143- """Validate a distribution bugtask to be added.
144-
145- Make sure that the isn't already a distribution task without a
146- source package, or that such task is added only when the bug doesn't
147- already have any tasks for the distribution.
148-
149- The same checks as `validate_distrotask` does are also done.
150- """
151- from canonical.launchpad.helpers import shortlist
152-
153- if sourcepackagename:
154- # Ensure that there isn't already a generic task open on the
155- # distribution for this bug, because if there were, that task
156- # should be reassigned to the sourcepackage, rather than a new
157- # task opened.
158- if bug.getBugTask(distribution) is not None:
159- raise LaunchpadValidationError(_(
160- 'This bug is already open on ${distribution} with no '
161- 'package specified. You should fill in a package '
162- 'name for the existing bug.',
163- mapping={'distribution': distribution.displayname}))
164- else:
165- # Prevent having a task on only the distribution if there's at
166- # least one task already on the distribution, whether or not
167- # that task also has a source package.
168- distribution_tasks_for_bug = [
169- bugtask for bugtask
170- in shortlist(bug.bugtasks, longest_expected=50)
171- if bugtask.distribution == distribution]
172-
173- if len(distribution_tasks_for_bug) > 0:
174- raise LaunchpadValidationError(_(
175- 'This bug is already on ${distribution}. Please '
176- 'specify an affected package in which the bug '
177- 'has not yet been reported.',
178- mapping={'distribution': distribution.displayname}))
179- validate_distrotask(bug, distribution, sourcepackagename)
180-
181-
182-def validate_distrotask(bug, distribution, sourcepackagename=None):
183- """Check if a distribution bugtask already exists for a given bug.
184-
185- If validation fails, a LaunchpadValidationError will be raised.
186- """
187- if sourcepackagename is not None and len(distribution.series) > 0:
188- # If the distribution has at least one series, check that the
189- # source package has been published in the distribution.
190- try:
191- distribution.guessPublishedSourcePackageName(
192- sourcepackagename.name)
193- except NotFoundError, e:
194- raise LaunchpadValidationError(e)
195- new_source_package = distribution.getSourcePackage(sourcepackagename)
196- if sourcepackagename is not None and (
197- bug.getBugTask(new_source_package) is not None):
198- # Ensure this distribution/sourcepackage task is unique.
199- raise LaunchpadValidationError(_(
200- 'This bug has already been reported on ${source} '
201- '(${distribution}).',
202- mapping={'source': sourcepackagename.name,
203- 'distribution': distribution.name}))
204- elif (sourcepackagename is None and
205- bug.getBugTask(distribution) is not None):
206- # Don't allow two distribution tasks with no source package.
207- raise LaunchpadValidationError(_(
208- 'This bug has already been reported on ${distribution}.',
209- mapping={'distribution': distribution.name}))
210- else:
211- # The bugtask is valid.
212- pass
213-
214-
215-def valid_upstreamtask(bug, bug_target):
216- """Check if a bugtask already exists for a given bug/target.
217-
218- If it exists, WidgetsError will be raised.
219- """
220- # Local import to avoid circular imports.
221- from lp.bugs.interfaces.bugtask import BugTaskSearchParams
222- errors = []
223- user = getUtility(ILaunchBag).user
224- params = BugTaskSearchParams(user, bug=bug)
225- if not bug_target.searchTasks(params).is_empty():
226- errors.append(
227- LaunchpadValidationError(_(
228- 'A fix for this bug has already been requested for ${target}',
229- mapping={'target': bug_target.displayname})))
230-
231- if len(errors) > 0:
232- raise expose(WidgetsError(errors), 400)
233-
234-
235 def valid_password(password):
236 """Return True if the argument is a valid password.
237
238
239=== modified file 'lib/lp/bugs/browser/bugalsoaffects.py'
240--- lib/lp/bugs/browser/bugalsoaffects.py 2011-07-13 02:17:29 +0000
241+++ lib/lp/bugs/browser/bugalsoaffects.py 2011-07-25 23:06:42 +0000
242@@ -16,10 +16,7 @@
243 from lazr.lifecycle.event import ObjectCreatedEvent
244 from z3c.ptcompat import ViewPageTemplateFile
245 from zope.app.form.browser import DropdownWidget
246-from zope.app.form.interfaces import (
247- MissingInputError,
248- WidgetsError,
249- )
250+from zope.app.form.interfaces import MissingInputError
251 from zope.component import getUtility
252 from zope.event import notify
253 from zope.formlib import form
254@@ -34,10 +31,6 @@
255 MultiStepView,
256 StepView,
257 )
258-from canonical.launchpad.interfaces.validation import (
259- valid_upstreamtask,
260- validate_new_distrotask,
261- )
262 from canonical.launchpad.webapp import canonical_url
263 from canonical.launchpad.webapp.interfaces import ILaunchBag
264 from canonical.launchpad.webapp.menu import structured
265@@ -62,6 +55,7 @@
266 BugTaskStatus,
267 IAddBugTaskForm,
268 IAddBugTaskWithProductCreationForm,
269+ IllegalTarget,
270 valid_remote_bug_url,
271 )
272 from lp.bugs.interfaces.bugtracker import (
273@@ -73,6 +67,10 @@
274 NoBugTrackerFound,
275 UnrecognizedBugTrackerURL,
276 )
277+from lp.bugs.model.bugtask import (
278+ validate_new_target,
279+ validate_target,
280+ )
281 from lp.registry.interfaces.distributionsourcepackage import (
282 IDistributionSourcePackage,
283 )
284@@ -157,8 +155,8 @@
285 upstream = bugtask.target.upstream_product
286 if upstream is not None:
287 try:
288- valid_upstreamtask(bugtask.bug, upstream)
289- except WidgetsError:
290+ validate_target(bugtask.bug, upstream)
291+ except IllegalTarget:
292 # There is already a task for the upstream.
293 pass
294 else:
295@@ -173,10 +171,9 @@
296 def validateStep(self, data):
297 if data.get('product'):
298 try:
299- valid_upstreamtask(self.context.bug, data.get('product'))
300- except WidgetsError, errors:
301- for error in errors:
302- self.setFieldError('product', error.snippet())
303+ validate_target(self.context.bug, data.get('product'))
304+ except IllegalTarget as e:
305+ self.setFieldError('product', e[0])
306 return
307
308 entered_product = self.request.form.get(self.widgets['product'].name)
309@@ -431,10 +428,12 @@
310 self.setFieldError('sourcepackagename', error)
311 else:
312 try:
313- validate_new_distrotask(
314- self.context.bug, distribution, sourcepackagename)
315- except LaunchpadValidationError, error:
316- self.setFieldError('sourcepackagename', error.snippet())
317+ target = distribution
318+ if sourcepackagename:
319+ target = target.getSourcePackage(sourcepackagename)
320+ validate_new_target(self.context.bug, target)
321+ except IllegalTarget as e:
322+ self.setFieldError('sourcepackagename', e[0])
323
324 super(DistroBugTaskCreationStep, self).validateStep(data)
325
326@@ -812,10 +811,9 @@
327 self._validate(action, data)
328 project = data.get('existing_product')
329 try:
330- valid_upstreamtask(self.context.bug, project)
331- except WidgetsError, errors:
332- for error in errors:
333- self.setFieldError('existing_product', error.snippet())
334+ validate_target(self.context.bug, project)
335+ except IllegalTarget as e:
336+ self.setFieldError('existing_product', e[0])
337
338 @action('Use Existing Project', name='use_existing_product',
339 validator=validate_existing_product)
340
341=== modified file 'lib/lp/bugs/browser/bugtask.py'
342--- lib/lp/bugs/browser/bugtask.py 2011-07-21 06:27:29 +0000
343+++ lib/lp/bugs/browser/bugtask.py 2011-07-25 23:06:42 +0000
344@@ -132,10 +132,6 @@
345 FeedsMixin,
346 )
347 from canonical.launchpad.interfaces.launchpad import IHasExternalBugTracker
348-from canonical.launchpad.interfaces.validation import (
349- valid_upstreamtask,
350- validate_distrotask,
351- )
352 from canonical.launchpad.mailnotification import get_unified_diff
353 from canonical.launchpad.searchbuilder import (
354 all,
355@@ -238,6 +234,7 @@
356 IBugTaskSet,
357 ICreateQuestionFromBugTaskForm,
358 IFrontPageBugTaskSearch,
359+ IllegalTarget,
360 INominationsReviewTableBatchNavigator,
361 IPersonBugTaskSearch,
362 IRemoveQuestionFromBugTaskForm,
363@@ -249,6 +246,7 @@
364 from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus
365 from lp.bugs.interfaces.cve import ICveSet
366 from lp.bugs.interfaces.malone import IMaloneApplication
367+from lp.bugs.model.bugtask import validate_target
368 from lp.registry.interfaces.distribution import (
369 IDistribution,
370 IDistributionSet,
371@@ -1372,15 +1370,20 @@
372 distro = bugtask.distroseries.distribution
373 else:
374 distro = bugtask.distribution
375- sourcename = bugtask.sourcepackagename
376 old_product = bugtask.product
377
378- if distro is not None and sourcename != data.get('sourcepackagename'):
379+ new_spn = data.get('sourcepackagename')
380+ if distro is not None and bugtask.sourcepackagename != new_spn:
381 try:
382- validate_distrotask(
383- bugtask.bug, distro, data.get('sourcepackagename'))
384- except LaunchpadValidationError, error:
385- self.setFieldError('sourcepackagename', str(error))
386+ target = distro
387+ if new_spn is not None:
388+ target = distro.getSourcePackage(new_spn)
389+ validate_target(bugtask.bug, target)
390+ except IllegalTarget as e:
391+ # The field validator may have already set an error.
392+ # Don't clobber it.
393+ if not self.getFieldError('sourcepackagename'):
394+ self.setFieldError('sourcepackagename', e[0])
395
396 new_product = data.get('product')
397 if (old_product is None or old_product == new_product or
398@@ -1394,9 +1397,9 @@
399 self.setFieldError('product', 'Enter a project name')
400 else:
401 try:
402- valid_upstreamtask(bugtask.bug, new_product)
403- except WidgetsError, errors:
404- self.setFieldError('product', errors.args[0])
405+ validate_target(bugtask.bug, new_product)
406+ except IllegalTarget as e:
407+ self.setFieldError('product', e[0])
408
409 def updateContextFromData(self, data, context=None):
410 """Updates the context object using the submitted form data.
411
412=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
413--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-07-18 11:32:51 +0000
414+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-07-25 23:06:42 +0000
415@@ -705,7 +705,8 @@
416 bug_task_2, name='+editstatus', form=form, principal=user)
417 self.assertEqual(1, len(view.errors))
418 self.assertEqual(
419- 'This bug has already been reported on mouse (ubuntu).',
420+ 'A fix for this bug has already been requested for mouse in '
421+ 'Ubuntu',
422 view.errors[0])
423
424 def setUpRetargetMilestone(self):
425
426=== modified file 'lib/lp/bugs/model/bugtask.py'
427--- lib/lp/bugs/model/bugtask.py 2011-07-22 04:50:51 +0000
428+++ lib/lp/bugs/model/bugtask.py 2011-07-25 23:06:42 +0000
429@@ -18,6 +18,8 @@
430 'get_bug_privacy_filter',
431 'get_related_bugtasks_search_params',
432 'search_value_to_where_condition',
433+ 'validate_new_target',
434+ 'validate_target',
435 ]
436
437
438@@ -85,10 +87,6 @@
439 )
440 from canonical.launchpad.helpers import shortlist
441 from canonical.launchpad.interfaces.lpstorm import IStore
442-from canonical.launchpad.interfaces.validation import (
443- valid_upstreamtask,
444- validate_new_distrotask,
445- )
446 from canonical.launchpad.searchbuilder import (
447 all,
448 any,
449@@ -439,6 +437,65 @@
450 return validate_target_attribute(self, attr, value)
451
452
453+def validate_target(bug, target):
454+ """Validate a bugtask target against a bug's existing tasks.
455+
456+ Checks that no conflicting tasks already exist.
457+ """
458+ if bug.getBugTask(target):
459+ raise IllegalTarget(
460+ "A fix for this bug has already been requested for %s"
461+ % target.displayname)
462+
463+ if IDistributionSourcePackage.providedBy(target):
464+ # If the distribution has at least one series, check that the
465+ # source package has been published in the distribution.
466+ if (target.sourcepackagename is not None and
467+ len(target.distribution.series) > 0):
468+ try:
469+ target.distribution.guessPublishedSourcePackageName(
470+ target.sourcepackagename.name)
471+ except NotFoundError, e:
472+ raise IllegalTarget(e[0])
473+
474+
475+def validate_new_target(bug, target):
476+ """Validate a bugtask target to be added.
477+
478+ Make sure that the isn't already a distribution task without a
479+ source package, or that such task is added only when the bug doesn't
480+ already have any tasks for the distribution.
481+
482+ The same checks as `validate_target` does are also done.
483+ """
484+ if IDistribution.providedBy(target):
485+ # Prevent having a task on only the distribution if there's at
486+ # least one task already on the distribution, whether or not
487+ # that task also has a source package.
488+ distribution_tasks_for_bug = [
489+ bugtask for bugtask
490+ in shortlist(bug.bugtasks, longest_expected=50)
491+ if bugtask.distribution == target]
492+
493+ if len(distribution_tasks_for_bug) > 0:
494+ raise IllegalTarget(
495+ "This bug is already on %s. Please specify an "
496+ "affected package in which the bug has not yet "
497+ "been reported." % target.displayname)
498+ elif IDistributionSourcePackage.providedBy(target):
499+ # Ensure that there isn't already a generic task open on the
500+ # distribution for this bug, because if there were, that task
501+ # should be reassigned to the sourcepackage, rather than a new
502+ # task opened.
503+ if bug.getBugTask(target.distribution) is not None:
504+ raise IllegalTarget(
505+ "This bug is already open on %s with no package "
506+ "specified. You should fill in a package name for "
507+ "the existing bug." % target.distribution.displayname)
508+
509+ validate_target(bug, target)
510+
511+
512 class BugTask(SQLBase):
513 """See `IBugTask`."""
514 implements(IBugTask)
515@@ -2653,7 +2710,8 @@
516 elif distribution is not None:
517 # Make sure there's no bug task already filed against
518 # this source package in this distribution.
519- validate_new_distrotask(bug, distribution, sourcepackagename)
520+ validate_new_target(
521+ bug, distribution.getSourcePackage(sourcepackagename))
522 stop_checking = True
523
524 if target is None and not stop_checking:
525@@ -2670,13 +2728,13 @@
526 target = distroseries
527 elif distribution is not None and not stop_checking:
528 # Bug filed against a distribution.
529- validate_new_distrotask(bug, distribution)
530+ validate_new_target(bug, distribution)
531 stop_checking = True
532
533 if target is not None and not stop_checking:
534 # Make sure there's no task for this bug already filed
535 # against the target.
536- valid_upstreamtask(bug, target)
537+ validate_target(bug, target)
538
539 if not bug.private and bug.security_related:
540 if product and product.security_contact:
541
542=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
543--- lib/lp/bugs/model/tests/test_bugtask.py 2011-07-22 04:50:51 +0000
544+++ lib/lp/bugs/model/tests/test_bugtask.py 2011-07-25 23:06:42 +0000
545@@ -42,6 +42,8 @@
546 bug_target_to_key,
547 build_tag_search_clause,
548 IllegalTarget,
549+ validate_new_target,
550+ validate_target,
551 )
552 from lp.bugs.tests.bug import (
553 create_old_bug,
554@@ -57,6 +59,7 @@
555 )
556 from lp.registry.interfaces.product import IProductSet
557 from lp.registry.interfaces.projectgroup import IProjectGroupSet
558+from lp.soyuz.interfaces.archive import ArchivePurpose
559 from lp.testing import (
560 ANONYMOUS,
561 EventRecorder,
562@@ -1831,3 +1834,177 @@
563 def test_no_target_for_bad_keys(self):
564 self.assertRaises(
565 AssertionError, bug_target_from_key, None, None, None, None, None)
566+
567+
568+class TestValidateTarget(TestCaseWithFactory):
569+
570+ layer = DatabaseFunctionalLayer
571+
572+ def test_new_product_is_allowed(self):
573+ # A new product not on the bug is OK.
574+ p1 = self.factory.makeProduct()
575+ task = self.factory.makeBugTask(target=p1)
576+ p2 = self.factory.makeProduct()
577+ validate_target(task.bug, p2)
578+
579+ def test_same_product_is_forbidden(self):
580+ # A product with an existing task is not.
581+ p = self.factory.makeProduct()
582+ task = self.factory.makeBugTask(target=p)
583+ self.assertRaisesWithContent(
584+ IllegalTarget,
585+ "A fix for this bug has already been requested for %s"
586+ % p.displayname,
587+ validate_target, task.bug, p)
588+
589+ def test_new_distribution_is_allowed(self):
590+ # A new distribution not on the bug is OK.
591+ d1 = self.factory.makeDistribution()
592+ task = self.factory.makeBugTask(target=d1)
593+ d2 = self.factory.makeDistribution()
594+ validate_target(task.bug, d2)
595+
596+ def test_new_productseries_is_allowed(self):
597+ # A new productseries not on the bug is OK.
598+ ds1 = self.factory.makeProductSeries()
599+ task = self.factory.makeBugTask(target=ds1)
600+ ds2 = self.factory.makeProductSeries()
601+ validate_target(task.bug, ds2)
602+
603+ def test_new_distroseries_is_allowed(self):
604+ # A new distroseries not on the bug is OK.
605+ ds1 = self.factory.makeDistroSeries()
606+ task = self.factory.makeBugTask(target=ds1)
607+ ds2 = self.factory.makeDistroSeries()
608+ validate_target(task.bug, ds2)
609+
610+ def test_new_sourcepackage_is_allowed(self):
611+ # A new sourcepackage not on the bug is OK.
612+ sp1 = self.factory.makeSourcePackage()
613+ task = self.factory.makeBugTask(target=sp1)
614+ sp2 = self.factory.makeSourcePackage()
615+ validate_target(task.bug, sp2)
616+
617+ def test_multiple_packageless_distribution_tasks_are_forbidden(self):
618+ # A distribution with an existing task is not.
619+ d = self.factory.makeDistribution()
620+ task = self.factory.makeBugTask(target=d)
621+ self.assertRaisesWithContent(
622+ IllegalTarget,
623+ "A fix for this bug has already been requested for %s"
624+ % d.displayname,
625+ validate_target, task.bug, d)
626+
627+ def test_distributionsourcepackage_task_is_allowed(self):
628+ # A DistributionSourcePackage task can coexist with a task for
629+ # its Distribution.
630+ d = self.factory.makeDistribution()
631+ task = self.factory.makeBugTask(target=d)
632+ dsp = self.factory.makeDistributionSourcePackage(distribution=d)
633+ validate_target(task.bug, dsp)
634+
635+ def test_different_distributionsourcepackage_tasks_are_allowed(self):
636+ # A DistributionSourcePackage task can also coexist with a task
637+ # for another one.
638+ dsp1 = self.factory.makeDistributionSourcePackage()
639+ task = self.factory.makeBugTask(target=dsp1)
640+ dsp2 = self.factory.makeDistributionSourcePackage(
641+ distribution=dsp1.distribution)
642+ validate_target(task.bug, dsp2)
643+
644+ def test_same_distributionsourcepackage_task_is_forbidden(self):
645+ # But a DistributionSourcePackage task cannot coexist with a
646+ # task for itself.
647+ dsp = self.factory.makeDistributionSourcePackage()
648+ task = self.factory.makeBugTask(target=dsp)
649+ self.assertRaisesWithContent(
650+ IllegalTarget,
651+ "A fix for this bug has already been requested for %s in %s"
652+ % (dsp.sourcepackagename.name, dsp.distribution.displayname),
653+ validate_target, task.bug, dsp)
654+
655+ def test_dsp_without_publications_disallowed(self):
656+ # If a distribution has series, a DistributionSourcePackage task
657+ # can only be created if the package is published in a distro
658+ # archive.
659+ series = self.factory.makeDistroSeries()
660+ dsp = self.factory.makeDistributionSourcePackage(
661+ distribution=series.distribution)
662+ task = self.factory.makeBugTask()
663+ self.assertRaisesWithContent(
664+ IllegalTarget,
665+ "Package %s not published in %s"
666+ % (dsp.sourcepackagename.name, dsp.distribution.displayname),
667+ validate_target, task.bug, dsp)
668+
669+ def test_dsp_with_publications_allowed(self):
670+ # If a distribution has series, a DistributionSourcePackage task
671+ # can only be created if the package is published in a distro
672+ # archive.
673+ series = self.factory.makeDistroSeries()
674+ dsp = self.factory.makeDistributionSourcePackage(
675+ distribution=series.distribution)
676+ task = self.factory.makeBugTask()
677+ self.factory.makeSourcePackagePublishingHistory(
678+ distroseries=series, sourcepackagename=dsp.sourcepackagename,
679+ archive=series.main_archive)
680+ validate_target(task.bug, dsp)
681+
682+ def test_dsp_with_only_ppa_publications_disallowed(self):
683+ # If a distribution has series, a DistributionSourcePackage task
684+ # can only be created if the package is published in a distro
685+ # archive. PPA publications don't count.
686+ series = self.factory.makeDistroSeries()
687+ dsp = self.factory.makeDistributionSourcePackage(
688+ distribution=series.distribution)
689+ task = self.factory.makeBugTask()
690+ self.factory.makeSourcePackagePublishingHistory(
691+ distroseries=series, sourcepackagename=dsp.sourcepackagename,
692+ archive=self.factory.makeArchive(purpose=ArchivePurpose.PPA))
693+ self.assertRaisesWithContent(
694+ IllegalTarget,
695+ "Package %s not published in %s"
696+ % (dsp.sourcepackagename.name, dsp.distribution.displayname),
697+ validate_target, task.bug, dsp)
698+
699+
700+class TestValidateNewTarget(TestCaseWithFactory):
701+
702+ layer = DatabaseFunctionalLayer
703+
704+ def test_products_are_ok(self):
705+ p1 = self.factory.makeProduct()
706+ task = self.factory.makeBugTask(target=p1)
707+ p2 = self.factory.makeProduct()
708+ validate_new_target(task.bug, p2)
709+
710+ def test_calls_validate_target(self):
711+ p = self.factory.makeProduct()
712+ task = self.factory.makeBugTask(target=p)
713+ self.assertRaisesWithContent(
714+ IllegalTarget,
715+ "A fix for this bug has already been requested for %s"
716+ % p.displayname,
717+ validate_new_target, task.bug, p)
718+
719+ def test_package_task_with_distribution_task_forbidden(self):
720+ d = self.factory.makeDistribution()
721+ dsp = self.factory.makeDistributionSourcePackage(distribution=d)
722+ task = self.factory.makeBugTask(target=d)
723+ self.assertRaisesWithContent(
724+ IllegalTarget,
725+ "This bug is already open on %s with no package specified. "
726+ "You should fill in a package name for the existing bug."
727+ % d.displayname,
728+ validate_new_target, task.bug, dsp)
729+
730+ def test_distribution_task_with_package_task_forbidden(self):
731+ d = self.factory.makeDistribution()
732+ dsp = self.factory.makeDistributionSourcePackage(distribution=d)
733+ task = self.factory.makeBugTask(target=dsp)
734+ self.assertRaisesWithContent(
735+ IllegalTarget,
736+ "This bug is already on %s. Please specify an affected "
737+ "package in which the bug has not yet been reported."
738+ % d.displayname,
739+ validate_new_target, task.bug, d)
740
741=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt'
742--- lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt 2011-07-06 19:37:54 +0000
743+++ lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt 2011-07-25 23:06:42 +0000
744@@ -76,7 +76,8 @@
745 >>> browser.getControl("Save Changes").click()
746 >>> print get_feedback_messages(browser.contents)
747 [...There is 1 error in the data you entered...
748- u'This bug has already been reported on evolution (ubuntu).']
749+ u'A fix for this bug has already been requested for evolution in
750+ Ubuntu']
751
752 Now let's add a Debian task to bug 1. Since Debian doesn't use Launchpad,
753 we add a bug watch as well.
754@@ -131,7 +132,8 @@
755 'http://bugs.../ubuntu/+source/mozilla-firefox/+bug/1/+editstatus'
756 >>> print get_feedback_messages(browser.contents)
757 [...There is 1 error in the data you entered...
758- u'This bug has already been reported on alsa-utils (ubuntu).']
759+ u'A fix for this bug has already been requested for alsa-utils in
760+ Ubuntu']
761
762 >>> browser.getControl('Package').value = 'pmount'
763 >>> browser.getControl('Save Changes').click()