Merge lp:~wgrant/launchpad/launchpadtargetwidget-on-bugtask into lp:launchpad

Proposed by William Grant
Status: Merged
Approved by: William Grant
Approved revision: no longer in the source branch.
Merged at revision: 13677
Proposed branch: lp:~wgrant/launchpad/launchpadtargetwidget-on-bugtask
Merge into: lp:launchpad
Diff against target: 701 lines (+219/-153)
9 files modified
lib/lp/bugs/browser/bugtask.py (+70/-81)
lib/lp/bugs/browser/tests/bugtask-edit-views.txt (+13/-12)
lib/lp/bugs/browser/tests/test_bugtask.py (+77/-4)
lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt (+10/-5)
lib/lp/bugs/stories/bugs/xx-bug-activity.txt (+2/-1)
lib/lp/bugs/stories/bugtask-management/xx-bug-privileged-statuses.txt (+2/-1)
lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt (+3/-0)
lib/lp/bugs/templates/bugtask-edit-form.pt (+39/-46)
lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt (+3/-3)
To merge this branch: bzr merge lp:~wgrant/launchpad/launchpadtargetwidget-on-bugtask
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+71156@code.launchpad.net

Commit message

[r=jcsackett][bug=80902,253508] Bug tasks can now be freely retargeted between products, distributions and distribution source packages.

Description of the change

This branch changes BugTaskEditView to use LaunchpadTargetWidget where possible, allowing free retargeting between products and distributions and distribution source packages. The widget is used on all non-series tasks.

ProductSeries tasks are not retargetable, so they have no target widget. DistroSeries and SourcePackage tasks have the old BugTaskSourcePackageNameWidget, as only their sourcepackagename can be changed. validate() maps that into an IBugTarget. I removed the feature flag override from the sourcepackagename widget, as it's highly experimental and doesn't really work, and we're not quite sure what's happening with it.

Now that validate() and updateContextFromData() deal with IBugTargets rather than the various bits of the target key, they can push most of their validation and updating into the model. validateTransitionToTarget() and transitionToTarget() now handle all the messy target logic.

I've removed a pointless and erroneous sourcepackagename-change-specific "The bug supervisor for %s has been subscribed to this bug." notification, fixing bug #253508.

Lots of tests are updated to use LaunchpadTargetWidget, and three new ones have been added for SourcePackage retargeting.

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

Looks good to me. Thanks William, a lot of people will be happy with this change.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-08-11 02:33:23 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-08-12 02:43:27 +0000
@@ -182,6 +182,7 @@
182 IServiceUsage,182 IServiceUsage,
183 )183 )
184from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget184from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
185from lp.app.widgets.launchpadtarget import LaunchpadTargetWidget
185from lp.app.widgets.project import ProjectScopeWidget186from lp.app.widgets.project import ProjectScopeWidget
186from lp.bugs.browser.bug import (187from lp.bugs.browser.bug import (
187 BugContextMenu,188 BugContextMenu,
@@ -245,7 +246,6 @@
245from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus246from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus
246from lp.bugs.interfaces.cve import ICveSet247from lp.bugs.interfaces.cve import ICveSet
247from lp.bugs.interfaces.malone import IMaloneApplication248from lp.bugs.interfaces.malone import IMaloneApplication
248from lp.bugs.model.bugtask import validate_target
249from lp.registry.interfaces.distribution import (249from lp.registry.interfaces.distribution import (
250 IDistribution,250 IDistribution,
251 IDistributionSet,251 IDistributionSet,
@@ -267,7 +267,6 @@
267from lp.registry.interfaces.sourcepackage import ISourcePackage267from lp.registry.interfaces.sourcepackage import ISourcePackage
268from lp.registry.model.personroles import PersonRoles268from lp.registry.model.personroles import PersonRoles
269from lp.registry.vocabularies import MilestoneVocabulary269from lp.registry.vocabularies import MilestoneVocabulary
270from lp.services.features import getFeatureFlag
271from lp.services.fields import PersonChoice270from lp.services.fields import PersonChoice
272from lp.services.propertycache import cachedproperty271from lp.services.propertycache import cachedproperty
273272
@@ -1130,8 +1129,8 @@
1130 # depending on the current context and the permissions of the user viewing1129 # depending on the current context and the permissions of the user viewing
1131 # the form.1130 # the form.
1132 default_field_names = ['assignee', 'bugwatch', 'importance', 'milestone',1131 default_field_names = ['assignee', 'bugwatch', 'importance', 'milestone',
1133 'product', 'sourcepackagename', 'status',1132 'status', 'statusexplanation']
1134 'statusexplanation']1133 custom_widget('target', LaunchpadTargetWidget)
1135 custom_widget('sourcepackagename', BugTaskSourcePackageNameWidget)1134 custom_widget('sourcepackagename', BugTaskSourcePackageNameWidget)
1136 custom_widget('bugwatch', BugTaskBugWatchWidget)1135 custom_widget('bugwatch', BugTaskBugWatchWidget)
1137 custom_widget('assignee', BugTaskAssigneeWidget)1136 custom_widget('assignee', BugTaskAssigneeWidget)
@@ -1144,6 +1143,19 @@
11441143
1145 page_title = 'Edit status'1144 page_title = 'Edit status'
11461145
1146 @property
1147 def show_target_widget(self):
1148 # Only non-series tasks can be retargetted.
1149 return not ISeriesBugTarget.providedBy(self.context.target)
1150
1151 @property
1152 def show_sourcepackagename_widget(self):
1153 # SourcePackage tasks can have only their sourcepackagename changed.
1154 # Conjoinment means we can't rely on editing the
1155 # DistributionSourcePackage task for this :(
1156 return (IDistroSeries.providedBy(self.context.target) or
1157 ISourcePackage.providedBy(self.context.target))
1158
1147 @cachedproperty1159 @cachedproperty
1148 def field_names(self):1160 def field_names(self):
1149 """Return the field names that can be edited by the user."""1161 """Return the field names that can be edited by the user."""
@@ -1177,14 +1189,6 @@
1177 editable_field_names.remove("importance")1189 editable_field_names.remove("importance")
1178 else:1190 else:
1179 editable_field_names = set(('bugwatch', ))1191 editable_field_names = set(('bugwatch', ))
1180 if not IProduct.providedBy(self.context.target):
1181 #XXX: Bjorn Tillenius 2006-03-01:
1182 # Should be possible to edit the product as well,
1183 # but that's harder due to complications with bug
1184 # watches. The new product might use Launchpad
1185 # officially, thus we need to handle that case.
1186 # Let's deal with that later.
1187 editable_field_names.add('sourcepackagename')
1188 if self.context.bugwatch is None:1192 if self.context.bugwatch is None:
1189 editable_field_names.update(('status', 'assignee'))1193 editable_field_names.update(('status', 'assignee'))
1190 if ('importance' in self.default_field_names1194 if ('importance' in self.default_field_names
@@ -1198,6 +1202,11 @@
1198 and self.userCanEditImportance()):1202 and self.userCanEditImportance()):
1199 editable_field_names.add('importance')1203 editable_field_names.add('importance')
12001204
1205 if self.show_target_widget:
1206 editable_field_names.add('target')
1207 elif self.show_sourcepackagename_widget:
1208 editable_field_names.add('sourcepackagename')
1209
1201 # To help with caching, return an immutable object.1210 # To help with caching, return an immutable object.
1202 return frozenset(editable_field_names)1211 return frozenset(editable_field_names)
12031212
@@ -1242,6 +1251,11 @@
1242 super(BugTaskEditView, self).setUpFields()1251 super(BugTaskEditView, self).setUpFields()
1243 read_only_field_names = self._getReadOnlyFieldNames()1252 read_only_field_names = self._getReadOnlyFieldNames()
12441253
1254 if 'target' in self.editable_field_names:
1255 self.form_fields = self.form_fields.omit('target')
1256 target_field = copy_field(IBugTask['target'], readonly=False)
1257 self.form_fields += formlib.form.Fields(target_field)
1258
1245 # The status field is a special case because we alter the vocabulary1259 # The status field is a special case because we alter the vocabulary
1246 # it uses based on the permissions of the user viewing form.1260 # it uses based on the permissions of the user viewing form.
1247 if 'status' in self.editable_field_names:1261 if 'status' in self.editable_field_names:
@@ -1331,14 +1345,6 @@
1331 self.form_fields['assignee'].custom_widget = CustomWidgetFactory(1345 self.form_fields['assignee'].custom_widget = CustomWidgetFactory(
1332 BugTaskAssigneeWidget)1346 BugTaskAssigneeWidget)
13331347
1334 if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
1335 # Replace the default field with a field that uses the better
1336 # vocabulary.
1337 self.form_fields = self.form_fields.omit('sourcepackagename')
1338 self.form_fields += formlib.form.Fields(Choice(
1339 __name__='sourcepackagename', title=_('SourcePackageName'),
1340 required=False, vocabulary='DistributionSourcePackage'))
1341
1342 def _getReadOnlyFieldNames(self):1348 def _getReadOnlyFieldNames(self):
1343 """Return the names of fields that will be rendered read only."""1349 """Return the names of fields that will be rendered read only."""
1344 if self.context.target_uses_malone:1350 if self.context.target_uses_malone:
@@ -1372,42 +1378,22 @@
1372 return self.context.userCanEditImportance(self.user)1378 return self.context.userCanEditImportance(self.user)
13731379
1374 def validate(self, data):1380 def validate(self, data):
1375 """See `LaunchpadFormView`."""1381 if self.show_sourcepackagename_widget and 'sourcepackagename' in data:
1376 bugtask = self.context1382 data['target'] = self.context.distroseries
1377 if bugtask.distroseries is not None:1383 spn = data.get('sourcepackagename')
1378 distro = bugtask.distroseries.distribution1384 if spn:
1379 else:1385 data['target'] = data['target'].getSourcePackage(spn)
1380 distro = bugtask.distribution1386 del data['sourcepackagename']
1381 old_product = bugtask.product1387 error_field = 'sourcepackagename'
13821388 else:
1383 new_spn = data.get('sourcepackagename')1389 error_field = 'target'
1384 if distro is not None and bugtask.sourcepackagename != new_spn:1390
1385 try:1391 new_target = data.get('target')
1386 target = distro1392 if new_target and new_target != self.context.target:
1387 if new_spn is not None:1393 try:
1388 target = distro.getSourcePackage(new_spn)1394 self.context.validateTransitionToTarget(new_target)
1389 validate_target(bugtask.bug, target)1395 except IllegalTarget as e:
1390 except IllegalTarget as e:1396 self.setFieldError(error_field, e[0])
1391 # The field validator may have already set an error.
1392 # Don't clobber it.
1393 if not self.getFieldError('sourcepackagename'):
1394 self.setFieldError('sourcepackagename', e[0])
1395
1396 new_product = data.get('product')
1397 if (old_product is None or old_product == new_product or
1398 bugtask.pillar.bug_tracking_usage != ServiceUsage.LAUNCHPAD):
1399 # Either the product wasn't changed, we're dealing with a #
1400 # distro task, or the bugtask's product doesn't use Launchpad,
1401 # which means the product can't be changed.
1402 return
1403
1404 if new_product is None:
1405 self.setFieldError('product', 'Enter a project name')
1406 else:
1407 try:
1408 validate_target(bugtask.bug, new_product)
1409 except IllegalTarget as e:
1410 self.setFieldError('product', e[0])
14111397
1412 def updateContextFromData(self, data, context=None):1398 def updateContextFromData(self, data, context=None):
1413 """Updates the context object using the submitted form data.1399 """Updates the context object using the submitted form data.
@@ -1439,9 +1425,14 @@
1439 # product, we'll clear out the milestone value, to avoid1425 # product, we'll clear out the milestone value, to avoid
1440 # violating DB constraints that ensure an upstream task can't1426 # violating DB constraints that ensure an upstream task can't
1441 # be assigned to a milestone on a different product.1427 # be assigned to a milestone on a different product.
1428 # This is also done by transitionToTarget, but do it here so we
1429 # can display notifications and remove the milestone from the
1430 # submitted data.
1442 milestone_cleared = None1431 milestone_cleared = None
1443 milestone_ignored = False1432 milestone_ignored = False
1444 if bugtask.product and bugtask.product != new_values.get("product"):1433 missing = object()
1434 new_target = new_values.pop("target", missing)
1435 if new_target is not missing and bugtask.target != new_target:
1445 # We clear the milestone value if one was already set. We ignore1436 # We clear the milestone value if one was already set. We ignore
1446 # the milestone value if it was currently None, and the user tried1437 # the milestone value if it was currently None, and the user tried
1447 # to set a milestone value while also changing the product. This1438 # to set a milestone value while also changing the product. This
@@ -1460,12 +1451,10 @@
1460 # what it was!1451 # what it was!
1461 data_to_apply.pop('milestone', None)1452 data_to_apply.pop('milestone', None)
14621453
1463 # We special case setting assignee and status, because there's1454 # We special case setting target, status and assignee, because
1464 # a workflow associated with changes to these fields.1455 # there's a workflow associated with changes to these fields.
1465 if "assignee" in data_to_apply:1456 for manual_field in ('target', 'status', 'assignee'):
1466 del data_to_apply["assignee"]1457 data_to_apply.pop(manual_field, None)
1467 if "status" in data_to_apply:
1468 del data_to_apply["status"]
14691458
1470 # We grab the comment_on_change field before we update bugtask so as1459 # We grab the comment_on_change field before we update bugtask so as
1471 # to avoid problems accessing the field if the user has changed the1460 # to avoid problems accessing the field if the user has changed the
@@ -1476,6 +1465,16 @@
1476 changed = formlib.form.applyChanges(1465 changed = formlib.form.applyChanges(
1477 bugtask, self.form_fields, data_to_apply, self.adapters)1466 bugtask, self.form_fields, data_to_apply, self.adapters)
14781467
1468 # Set the "changed" flag properly, just in case status and/or assignee
1469 # happen to be the only values that changed. We explicitly verify that
1470 # we got a new status and/or assignee, because the form is not always
1471 # guaranteed to pass all the values. For example: bugtasks linked to a
1472 # bug watch don't allow editing the form, and the value is missing
1473 # from the form.
1474 if new_target is not missing and bugtask.target != new_target:
1475 changed = True
1476 bugtask.transitionToTarget(new_target)
1477
1479 # Now that we've updated the bugtask we can add messages about1478 # Now that we've updated the bugtask we can add messages about
1480 # milestone changes, if there were any.1479 # milestone changes, if there were any.
1481 if milestone_cleared:1480 if milestone_cleared:
@@ -1496,13 +1495,6 @@
1496 subject=bugtask.bug.followup_subject(),1495 subject=bugtask.bug.followup_subject(),
1497 content=comment_on_change)1496 content=comment_on_change)
14981497
1499 # Set the "changed" flag properly, just in case status and/or assignee
1500 # happen to be the only values that changed. We explicitly verify that
1501 # we got a new status and/or assignee, because the form is not always
1502 # guaranteed to pass all the values. For example: bugtasks linked to a
1503 # bug watch don't allow editing the form, and the value is missing
1504 # from the form.
1505 missing = object()
1506 new_status = new_values.pop("status", missing)1498 new_status = new_values.pop("status", missing)
1507 new_assignee = new_values.pop("assignee", missing)1499 new_assignee = new_values.pop("assignee", missing)
1508 if new_status is not missing and bugtask.status != new_status:1500 if new_status is not missing and bugtask.status != new_status:
@@ -1583,15 +1575,20 @@
1583 object_before_modification=bugtask_before_modification,1575 object_before_modification=bugtask_before_modification,
1584 edited_fields=field_names))1576 edited_fields=field_names))
15851577
1586 if bugtask.sourcepackagename is not None:1578 if (bugtask.sourcepackagename and (
1579 self.widgets.get('target') or
1580 self.widgets.get('sourcepackagename'))):
1587 real_package_name = bugtask.sourcepackagename.name1581 real_package_name = bugtask.sourcepackagename.name
15881582
1589 # We get entered_package_name directly from the form here, since1583 # We get entered_package_name directly from the form here, since
1590 # validating the sourcepackagename field mutates its value in to1584 # validating the sourcepackagename field mutates its value in to
1591 # the one already in real_package_name, which makes our comparison1585 # the one already in real_package_name, which makes our comparison
1592 # of the two below useless.1586 # of the two below useless.
1593 entered_package_name = self.request.form.get(1587 if self.widgets.get('sourcepackagename'):
1594 self.widgets['sourcepackagename'].name)1588 field_name = self.widgets['sourcepackagename'].name
1589 else:
1590 field_name = self.widgets['target'].package_widget.name
1591 entered_package_name = self.request.form.get(field_name)
15951592
1596 if real_package_name != entered_package_name:1593 if real_package_name != entered_package_name:
1597 # The user entered a binary package name which got1594 # The user entered a binary package name which got
@@ -1603,14 +1600,6 @@
1603 {'entered_package': entered_package_name,1600 {'entered_package': entered_package_name,
1604 'real_package': real_package_name})1601 'real_package': real_package_name})
16051602
1606 if (bugtask_before_modification.sourcepackagename !=
1607 bugtask.sourcepackagename):
1608 # The source package was changed, so tell the user that we've
1609 # subscribed the new bug supervisors.
1610 self.request.response.addNotification(
1611 "The bug supervisor for %s has been subscribed to this bug."
1612 % (bugtask.bugtargetdisplayname))
1613
1614 @action('Save Changes', name='save')1603 @action('Save Changes', name='save')
1615 def save_action(self, action, data):1604 def save_action(self, action, data):
1616 """Update the bugtask with the form data."""1605 """Update the bugtask with the form data."""
16171606
=== modified file 'lib/lp/bugs/browser/tests/bugtask-edit-views.txt'
--- lib/lp/bugs/browser/tests/bugtask-edit-views.txt 2011-07-27 08:04:46 +0000
+++ lib/lp/bugs/browser/tests/bugtask-edit-views.txt 2011-08-12 02:43:27 +0000
@@ -24,9 +24,7 @@
24 ... 'ubuntu_thunderbird.importance':24 ... 'ubuntu_thunderbird.importance':
25 ... ubuntu_thunderbird_task.importance.title,25 ... ubuntu_thunderbird_task.importance.title,
26 ... 'ubuntu_thunderbird.ubuntu_thunderbird.assignee.option':26 ... 'ubuntu_thunderbird.ubuntu_thunderbird.assignee.option':
27 ... 'ubuntu_thunderbird.assignee.assign_to_nobody',27 ... 'ubuntu_thunderbird.assignee.assign_to_nobody'}
28 ... 'ubuntu_thunderbird.sourcepackagename':
29 ... ubuntu_thunderbird_task.sourcepackagename.name}
30 >>> request = LaunchpadTestRequest(method='POST', form=edit_form)28 >>> request = LaunchpadTestRequest(method='POST', form=edit_form)
31 >>> edit_view = getMultiAdapter(29 >>> edit_view = getMultiAdapter(
32 ... (ubuntu_thunderbird_task, request), name='+editstatus')30 ... (ubuntu_thunderbird_task, request), name='+editstatus')
@@ -47,7 +45,9 @@
4745
4846
49 >>> ubuntu_thunderbird = ubuntu_thunderbird_task.target47 >>> ubuntu_thunderbird = ubuntu_thunderbird_task.target
50 >>> edit_form['ubuntu_thunderbird.sourcepackagename'] = u'linux-2.6.12'48 >>> edit_form['ubuntu_thunderbird.target'] = 'package'
49 >>> edit_form['ubuntu_thunderbird.target.distribution'] = 'ubuntu'
50 >>> edit_form['ubuntu_thunderbird.target.package'] = u'linux-2.6.12'
51 >>> request = LaunchpadTestRequest(method='POST', form=edit_form)51 >>> request = LaunchpadTestRequest(method='POST', form=edit_form)
52 >>> edit_view = getMultiAdapter(52 >>> edit_view = getMultiAdapter(
53 ... (ubuntu_thunderbird_task, request), name='+editstatus')53 ... (ubuntu_thunderbird_task, request), name='+editstatus')
@@ -62,8 +62,6 @@
62 ... print notification.message62 ... print notification.message
63 'linux-2.6.12' is a binary package. This bug has been assigned to63 'linux-2.6.12' is a binary package. This bug has been assigned to
64 its source package 'linux-source-2.6.15' instead.64 its source package 'linux-source-2.6.15' instead.
65 The bug supervisor for linux-source-2.6.15 (Ubuntu) has been
66 subscribed to this bug.
6765
68 >>> # The sampledata is bad -- the original thunderbird task should66 >>> # The sampledata is bad -- the original thunderbird task should
69 >>> # not exist, as there is no publication. Create one so we can67 >>> # not exist, as there is no publication. Create one so we can
@@ -77,15 +75,16 @@
77If we try to change the source package to package name that doesn't75If we try to change the source package to package name that doesn't
78exist in Launchpad. we'll get an error message.76exist in Launchpad. we'll get an error message.
7977
80 >>> edit_form['ubuntu_thunderbird.sourcepackagename'] = u'no-such-package'78 >>> edit_form['ubuntu_thunderbird.target.package'] = u'no-such-package'
81 >>> request = LaunchpadTestRequest(form=edit_form, method='POST')79 >>> request = LaunchpadTestRequest(form=edit_form, method='POST')
82 >>> edit_view = getMultiAdapter(80 >>> edit_view = getMultiAdapter(
83 ... (ubuntu_thunderbird_task, request), name='+editstatus')81 ... (ubuntu_thunderbird_task, request), name='+editstatus')
84 >>> edit_view.initialize()82 >>> edit_view.initialize()
85 >>> for error in edit_view.errors:83 >>> for error in edit_view.errors:
86 ... print error84 ... print error
87 (u"Launchpad doesn't know of any source package named 'no-such-package'85 (u'ubuntu_thunderbird.target', u'Target',
88 in Ubuntu.", None)86 LaunchpadValidationError(u"There is no package name 'no-such-package'
87 published in Ubuntu"))
8988
90An error is reported to the user when a bug is retargeted and there is89An error is reported to the user when a bug is retargeted and there is
91an existing task for the same target.90an existing task for the same target.
@@ -107,7 +106,8 @@
107 ... product_task.importance.title,106 ... product_task.importance.title,
108 ... 'evolution.evolution.assignee.option':107 ... 'evolution.evolution.assignee.option':
109 ... 'evolution.assignee.assign_to_nobody',108 ... 'evolution.assignee.assign_to_nobody',
110 ... 'evolution.product': 'firefox',109 ... 'evolution.target': 'product',
110 ... 'evolution.target.product': 'firefox',
111 ... }111 ... }
112 >>> request = LaunchpadTestRequest(form=edit_form, method='POST')112 >>> request = LaunchpadTestRequest(form=edit_form, method='POST')
113 >>> edit_view = getMultiAdapter(113 >>> edit_view = getMultiAdapter(
@@ -127,7 +127,8 @@
127 ... product_task.importance.title,127 ... product_task.importance.title,
128 ... 'firefox.firefox.assignee.option':128 ... 'firefox.firefox.assignee.option':
129 ... 'firefox.assignee.assign_to_nobody',129 ... 'firefox.assignee.assign_to_nobody',
130 ... 'firefox.product': '',130 ... 'firefox.target': 'product',
131 ... 'firefox.target.product': '',
131 ... }132 ... }
132 >>> request = LaunchpadTestRequest(form=edit_form, method='POST')133 >>> request = LaunchpadTestRequest(form=edit_form, method='POST')
133 >>> edit_view = getMultiAdapter(134 >>> edit_view = getMultiAdapter(
@@ -135,7 +136,7 @@
135 >>> edit_view.initialize()136 >>> edit_view.initialize()
136 >>> for error in edit_view.errors:137 >>> for error in edit_view.errors:
137 ... print error138 ... print error
138 Enter a project name139 ('product', u'Project', RequiredMissing())
139140
140141
141== Bug Watch Linkage ==142== Bug Watch Linkage ==
142143
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-08-03 05:00:46 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-08-12 02:43:27 +0000
@@ -171,7 +171,8 @@
171 transaction.commit()171 transaction.commit()
172 with person_logged_in(person):172 with person_logged_in(person):
173 form_data = {173 form_data = {
174 '%s.product' % product.name: product_2.name,174 '%s.target' % product.name: 'product',
175 '%s.target.product' % product.name: product_2.name,
175 '%s.status' % product.name: BugTaskStatus.TRIAGED.title,176 '%s.status' % product.name: BugTaskStatus.TRIAGED.title,
176 '%s.actions.save' % product.name: 'Save Changes',177 '%s.actions.save' % product.name: 'Save Changes',
177 }178 }
@@ -722,7 +723,9 @@
722 'ubuntu_rabbit.importance': 'High',723 'ubuntu_rabbit.importance': 'High',
723 'ubuntu_rabbit.assignee.option':724 'ubuntu_rabbit.assignee.option':
724 'ubuntu_rabbit.assignee.assign_to_nobody',725 'ubuntu_rabbit.assignee.assign_to_nobody',
725 'ubuntu_rabbit.sourcepackagename': 'mouse',726 'ubuntu_rabbit.target': 'package',
727 'ubuntu_rabbit.target.distribution': 'ubuntu',
728 'ubuntu_rabbit.target.package': 'mouse',
726 }729 }
727 view = create_initialized_view(730 view = create_initialized_view(
728 bug_task_2, name='+editstatus', form=form, principal=user)731 bug_task_2, name='+editstatus', form=form, principal=user)
@@ -755,7 +758,8 @@
755 form = {758 form = {
756 'bunny.status': 'In Progress',759 'bunny.status': 'In Progress',
757 'bunny.assignee.option': 'bunny.assignee.assign_to_nobody',760 'bunny.assignee.option': 'bunny.assignee.assign_to_nobody',
758 'bunny.product': 'duck',761 'bunny.target': 'product',
762 'bunny.target.product': 'duck',
759 'bunny.actions.save': 'Save Changes',763 'bunny.actions.save': 'Save Changes',
760 }764 }
761 view = create_initialized_view(765 view = create_initialized_view(
@@ -777,7 +781,8 @@
777 form = {781 form = {
778 'bunny.status': 'In Progress',782 'bunny.status': 'In Progress',
779 'bunny.assignee.option': 'bunny.assignee.assign_to_nobody',783 'bunny.assignee.option': 'bunny.assignee.assign_to_nobody',
780 'bunny.product': 'duck',784 'bunny.target': 'product',
785 'bunny.target.product': 'duck',
781 'bunny.milestone': milestone_id,786 'bunny.milestone': milestone_id,
782 'bunny.actions.save': 'Save Changes',787 'bunny.actions.save': 'Save Changes',
783 }788 }
@@ -791,6 +796,74 @@
791 expected = ('The milestone setting was ignored')796 expected = ('The milestone setting was ignored')
792 self.assertTrue(notifications.pop().message.startswith(expected))797 self.assertTrue(notifications.pop().message.startswith(expected))
793798
799 def createNameChangingViewForSourcePackageTask(self, bug_task, new_name):
800 login_person(bug_task.owner)
801 form_prefix = '%s_%s_%s' % (
802 bug_task.target.distroseries.distribution.name,
803 bug_task.target.distroseries.name,
804 bug_task.target.sourcepackagename.name)
805 form = {
806 form_prefix + '.sourcepackagename': new_name,
807 form_prefix + '.actions.save': 'Save Changes',
808 }
809 view = create_initialized_view(
810 bug_task, name='+editstatus', form=form)
811 return view
812
813 def test_retarget_sourcepackage(self):
814 # The sourcepackagename of a SourcePackage task can be changed.
815 ds = self.factory.makeDistroSeries()
816 sp1 = self.factory.makeSourcePackage(distroseries=ds, publish=True)
817 sp2 = self.factory.makeSourcePackage(distroseries=ds, publish=True)
818 bug_task = self.factory.makeBugTask(target=sp1)
819
820 view = self.createNameChangingViewForSourcePackageTask(
821 bug_task, sp2.sourcepackagename.name)
822 self.assertEqual([], view.errors)
823 self.assertEqual(sp2, bug_task.target)
824 notifications = view.request.response.notifications
825 self.assertEqual(0, len(notifications))
826
827 def test_retarget_sourcepackage_to_binary_name(self):
828 # The sourcepackagename of a SourcePackage task can be changed
829 # to a binarypackagename, which gets mapped back to the source.
830 ds = self.factory.makeDistroSeries()
831 das = self.factory.makeDistroArchSeries(distroseries=ds)
832 sp1 = self.factory.makeSourcePackage(distroseries=ds, publish=True)
833 # Now create a binary and its corresponding SourcePackage.
834 bp = self.factory.makeBinaryPackagePublishingHistory(
835 distroarchseries=das)
836 bpr = bp.binarypackagerelease
837 spn = bpr.build.source_package_release.sourcepackagename
838 sp2 = self.factory.makeSourcePackage(
839 distroseries=ds, sourcepackagename=spn, publish=True)
840 bug_task = self.factory.makeBugTask(target=sp1)
841
842 view = self.createNameChangingViewForSourcePackageTask(
843 bug_task, bpr.binarypackagename.name)
844 self.assertEqual([], view.errors)
845 self.assertEqual(sp2, bug_task.target)
846 notifications = view.request.response.notifications
847 self.assertEqual(1, len(notifications))
848 expected = (
849 "'%s' is a binary package. This bug has been assigned to its "
850 "source package '%s' instead."
851 % (bpr.binarypackagename.name, spn.name))
852 self.assertTrue(notifications.pop().message.startswith(expected))
853
854 def test_retarget_sourcepackage_to_distroseries(self):
855 # A SourcePackage task can be changed to a DistroSeries one.
856 ds = self.factory.makeDistroSeries()
857 sp = self.factory.makeSourcePackage(distroseries=ds, publish=True)
858 bug_task = self.factory.makeBugTask(target=sp)
859
860 view = self.createNameChangingViewForSourcePackageTask(
861 bug_task, '')
862 self.assertEqual([], view.errors)
863 self.assertEqual(ds, bug_task.target)
864 notifications = view.request.response.notifications
865 self.assertEqual(0, len(notifications))
866
794867
795class TestProjectGroupBugs(TestCaseWithFactory):868class TestProjectGroupBugs(TestCaseWithFactory):
796 """Test the bugs overview page for Project Groups."""869 """Test the bugs overview page for Project Groups."""
797870
=== 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-25 22:27:37 +0000
+++ lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt 2011-08-12 02:43:27 +0000
@@ -56,7 +56,8 @@
5656
57 >>> browser.open(57 >>> browser.open(
58 ... "http://localhost/ubuntu/+bug/6/+editstatus")58 ... "http://localhost/ubuntu/+bug/6/+editstatus")
59 >>> browser.getControl("Package").value = "mozilla-firefox"59 >>> browser.getControl(name="ubuntu.target.package").value = (
60 ... "mozilla-firefox")
60 >>> browser.getControl("Save Changes").click()61 >>> browser.getControl("Save Changes").click()
6162
62 >>> browser.open(63 >>> browser.open(
@@ -72,7 +73,8 @@
72 >>> browser.open(73 >>> browser.open(
73 ... "http://localhost/ubuntu/+source/mozilla-firefox/+bug/6/"74 ... "http://localhost/ubuntu/+source/mozilla-firefox/+bug/6/"
74 ... "+editstatus")75 ... "+editstatus")
75 >>> browser.getControl("Package").value = "evolution"76 >>> browser.getControl(name="ubuntu_mozilla-firefox.target.package"
77 ... ).value = "evolution"
76 >>> browser.getControl("Save Changes").click()78 >>> browser.getControl("Save Changes").click()
77 >>> print get_feedback_messages(browser.contents)79 >>> print get_feedback_messages(browser.contents)
78 [...There is 1 error in the data you entered...80 [...There is 1 error in the data you entered...
@@ -126,7 +128,8 @@
126 >>> browser.url128 >>> browser.url
127 'http://bugs.../ubuntu/+source/mozilla-firefox/+bug/1/+editstatus'129 'http://bugs.../ubuntu/+source/mozilla-firefox/+bug/1/+editstatus'
128130
129 >>> browser.getControl('Package').value = 'alsa-utils'131 >>> browser.getControl(name="ubuntu_mozilla-firefox.target.package"
132 ... ).value = 'alsa-utils'
130 >>> browser.getControl('Save Changes').click()133 >>> browser.getControl('Save Changes').click()
131 >>> browser.url134 >>> browser.url
132 'http://bugs.../ubuntu/+source/mozilla-firefox/+bug/1/+editstatus'135 'http://bugs.../ubuntu/+source/mozilla-firefox/+bug/1/+editstatus'
@@ -135,7 +138,8 @@
135 u'A fix for this bug has already been requested for alsa-utils in138 u'A fix for this bug has already been requested for alsa-utils in
136 Ubuntu']139 Ubuntu']
137140
138 >>> browser.getControl('Package').value = 'pmount'141 >>> browser.getControl(name="ubuntu_mozilla-firefox.target.package"
142 ... ).value = 'pmount'
139 >>> browser.getControl('Save Changes').click()143 >>> browser.getControl('Save Changes').click()
140 >>> browser.url144 >>> browser.url
141 'http://bugs.launchpad.dev/ubuntu/+source/pmount/+bug/1'145 'http://bugs.launchpad.dev/ubuntu/+source/pmount/+bug/1'
@@ -382,7 +386,8 @@
382 >>> user_browser.getLink(url='evolution/+bug/3/+editstatus').click()386 >>> user_browser.getLink(url='evolution/+bug/3/+editstatus').click()
383 >>> user_browser.url387 >>> user_browser.url
384 'http://bugs.launchpad.dev/evolution/+bug/3/+editstatus'388 'http://bugs.launchpad.dev/evolution/+bug/3/+editstatus'
385 >>> user_browser.getControl('Project').value = 'alsa-utils'389 >>> user_browser.getControl(name='evolution.target.product').value = (
390 ... 'alsa-utils')
386 >>> user_browser.getControl('Save Changes').click()391 >>> user_browser.getControl('Save Changes').click()
387 >>> user_browser.url392 >>> user_browser.url
388 'http://bugs.launchpad.dev/evolution/+bug/3/+editstatus'393 'http://bugs.launchpad.dev/evolution/+bug/3/+editstatus'
389394
=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-activity.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-activity.txt 2011-05-31 16:43:11 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-activity.txt 2011-08-12 02:43:27 +0000
@@ -291,7 +291,8 @@
291 ... 'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/'291 ... 'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/'
292 ... '1/+editstatus')292 ... '1/+editstatus')
293 >>> admin_browser.getControl(293 >>> admin_browser.getControl(
294 ... 'Package').value = 'linux-source-2.6.15'294 ... name='ubuntu_mozilla-firefox.target.package'
295 ... ).value = 'linux-source-2.6.15'
295 >>> admin_browser.getControl("Save Changes").click()296 >>> admin_browser.getControl("Save Changes").click()
296 >>> print_comments(admin_browser.contents)297 >>> print_comments(admin_browser.contents)
297 Foo Bar (name16)298 Foo Bar (name16)
298299
=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-bug-privileged-statuses.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-bug-privileged-statuses.txt 2010-06-16 17:31:50 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-bug-privileged-statuses.txt 2011-08-12 02:43:27 +0000
@@ -95,7 +95,8 @@
9595
96And a regular user can change other aspects of the bug:96And a regular user can change other aspects of the bug:
9797
98 >>> package_control = user_browser.getControl('Package')98 >>> package_control = user_browser.getControl(
99 ... name='ubuntu_mozilla-firefox.target.package')
99 >>> print package_control.value100 >>> print package_control.value
100 mozilla-firefox101 mozilla-firefox
101102
102103
=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt 2010-07-12 16:32:31 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt 2011-08-12 02:43:27 +0000
@@ -43,6 +43,9 @@
43Filed here by: Sample Person43Filed here by: Sample Person
44When: 2004-01-0244When: 2004-01-02
45Assigned: 2005-01-0245Assigned: 2005-01-02
46Target
47Distribution
48...
46Project (Find…)49Project (Find…)
47Status Importance Milestone50Status Importance Milestone
48New... Low... (no value)...51New... Low... (no value)...
4952
=== modified file 'lib/lp/bugs/templates/bugtask-edit-form.pt'
--- lib/lp/bugs/templates/bugtask-edit-form.pt 2011-05-18 18:34:19 +0000
+++ lib/lp/bugs/templates/bugtask-edit-form.pt 2011-08-12 02:43:27 +0000
@@ -114,52 +114,45 @@
114 </tr>114 </tr>
115 </table>115 </table>
116 <div class="field">116 <div class="field">
117 <table>117 <table tal:condition="view/show_target_widget">
118 <tal:distrotask118 <tr>
119 condition="python:context.distribution or context.distroseries"119 <td>
120 define="error python:view.getFieldError('sourcepackagename')"120 <label
121 >121 tal:attributes="for view/widgets/target/name"
122 <tr>122 tal:content="view/widgets/target/label"
123 <td>123 >Target</label>
124 <label124 </td>
125 tal:attributes="for view/widgets/sourcepackagename/name"125 </tr>
126 tal:content="view/widgets/sourcepackagename/label"126 <tr tal:define="error python:view.getFieldError('target')"
127 >Source package name</label>127 tal:attributes="class python:error and 'error' or None">
128 </td>128 <td>
129 </tr>129 <tal:widget content="structure view/widgets/target" />
130 <tr tal:attributes="class python:error and 'error' or None">130 <div tal:condition="error"
131 <td>131 class="message"
132 <tal:widget content="structure view/widgets/sourcepackagename" />132 tal:content="error">An error in target widget.
133 <div tal:condition="error"133 </div>
134 class="message"134 </td>
135 tal:content="error">An error in sourcepackagename widget.135 </tr>
136 </div>136 </table>
137 </td>137 <table tal:condition="view/show_sourcepackagename_widget">
138 </tr>138 <tr>
139 </tal:distrotask>139 <td>
140140 <label
141 <tal:upstreamtask141 tal:attributes="for view/widgets/sourcepackagename/name"
142 condition="python:context.product"142 tal:content="view/widgets/sourcepackagename/label"
143 define="omit_required python:True;143 >Package</label>
144 error python:view.getFieldError('product')">144 </td>
145 <tr>145 </tr>
146 <td colspan="2">146 <tr tal:define="error python:view.getFieldError('sourcepackagename')"
147 <label style="font-weight: bold"147 tal:attributes="class python:error and 'error' or None">
148 tal:attributes="for view/widgets/product/name"148 <td>
149 tal:content="view/widgets/product/label"149 <tal:widget content="structure view/widgets/sourcepackagename" />
150 >Upstream project</label>150 <div tal:condition="error"
151 </td>151 class="message"
152 </tr>152 tal:content="error">An error in sourcepackagename widget.
153 <tr tal:attributes="class python:error and 'error' or None">153 </div>
154 <td colspan="2" style="white-space: nowrap">154 </td>
155 <tal:widget content="structure view/widgets/product" />155 </tr>
156 <div tal:condition="error"
157 class="message"
158 tal:content="error">An error in product widget.
159 </div>
160 </td>
161 </tr>
162 </tal:upstreamtask>
163 </table>156 </table>
164 <table>157 <table>
165 <tr>158 <tr>
166159
=== modified file 'lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt'
--- lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt 2009-05-12 20:28:19 +0000
+++ lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt 2011-08-12 02:43:27 +0000
@@ -4,7 +4,7 @@
44
5 >>> browser = setupBrowser(auth="Basic foo.bar@canonical.com:test")5 >>> browser = setupBrowser(auth="Basic foo.bar@canonical.com:test")
6 >>> browser.open("http://bugs.launchpad.dev/firefox/+bug/1/+editstatus")6 >>> browser.open("http://bugs.launchpad.dev/firefox/+bug/1/+editstatus")
7 >>> browser.getControl("Project").value = "evolution"7 >>> browser.getControl(name="firefox.target.product").value = "evolution"
8 >>> browser.getControl("Milestone").value = ["1"]8 >>> browser.getControl("Milestone").value = ["1"]
9 >>> browser.getControl("Save Changes").click()9 >>> browser.getControl("Save Changes").click()
1010
@@ -16,7 +16,7 @@
16(Revert the change we just made.)16(Revert the change we just made.)
1717
18 >>> browser.open("http://bugs.launchpad.dev/evolution/+bug/1/+editstatus")18 >>> browser.open("http://bugs.launchpad.dev/evolution/+bug/1/+editstatus")
19 >>> browser.getControl("Project").value = "firefox"19 >>> browser.getControl(name="evolution.target.product").value = "firefox"
20 >>> browser.getControl("Save Changes").click()20 >>> browser.getControl("Save Changes").click()
2121
22(The "ignore" message doesn't appear when the user didn't set a22(The "ignore" message doesn't appear when the user didn't set a
@@ -33,7 +33,7 @@
33 >>> browser.getControl("Save Changes").click()33 >>> browser.getControl("Save Changes").click()
3434
35 >>> browser.open("http://localhost:9000/firefox/+bug/1/+editstatus")35 >>> browser.open("http://localhost:9000/firefox/+bug/1/+editstatus")
36 >>> browser.getControl("Project").value = "evolution"36 >>> browser.getControl(name="firefox.target.product").value = "evolution"
37 >>> browser.getControl("Save Changes").click()37 >>> browser.getControl("Save Changes").click()
3838
39 >>> for message in find_tags_by_class(browser.contents, 'message'):39 >>> for message in find_tags_by_class(browser.contents, 'message'):