launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00478
[Merge] lp:~deryck/launchpad/update-storm-17-add-back-dupe-changes into lp:launchpad/devel
Deryck Hodge has proposed merging lp:~deryck/launchpad/update-storm-17-add-back-dupe-changes into lp:launchpad/devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
My previous approved (and landed) work to converge on markAsDuplicate
rather than setting duplicateof directly was reverted due to a test
failure from a Storm bug. This branch includes my already approved
changes with a change to versions.cfg to get us on the Storm version
that was just released, 0.17.
I have already committed the new Storm to download-cache. I have run
tests. All that needs approval here is the change in Storm version.
Everything in the diff other than this change to versions.cfg is
approved in the MPs here:
https://code.edge.launchpad.net/~deryck/launchpad/do-the-right-thing-dupe-move-78596/+merge/27144
https://code.edge.launchpad.net/~deryck/launchpad/better-dupe-handling-ui/+merge/30309
And I have approval from Robert to change the version of Storm here:
https://code.edge.launchpad.net/~deryck/launchpad/update-storm-r365/+merge/31881
However, I didn't get this landed yesterday before the new Storm
today, so I thought I would do a versions.cfg update alongside
restoring my reverted duplicate handling work.
Cheers,
deryck
--
https://code.launchpad.net/~deryck/launchpad/update-storm-17-add-back-dupe-changes/+merge/31983
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~deryck/launchpad/update-storm-17-add-back-dupe-changes into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/fields/__init__.py'
--- lib/canonical/launchpad/fields/__init__.py 2010-07-22 12:17:41 +0000
+++ lib/canonical/launchpad/fields/__init__.py 2010-08-06 18:42:48 +0000
@@ -302,12 +302,8 @@
bug isn't a duplicate of itself, otherwise
return False.
"""
- from lp.bugs.interfaces.bug import IBugSet
- bugset = getUtility(IBugSet)
current_bug = self.context
dup_target = value
- current_bug_has_dup_refs = bool(bugset.searchAsUser(
- user=getUtility(ILaunchBag).user, duplicateof=current_bug))
if current_bug == dup_target:
raise LaunchpadValidationError(_(dedent("""
You can't mark a bug as a duplicate of itself.""")))
@@ -318,13 +314,6 @@
isn't a duplicate itself.
"""), mapping={'dup': dup_target.id,
'orig': dup_target.duplicateof.id}))
- elif current_bug_has_dup_refs:
- raise LaunchpadValidationError(_(dedent("""
- There are other bugs already marked as duplicates of
- Bug ${current}. These bugs should be changed to be
- duplicates of another bug if you are certain you would
- like to perform this change."""),
- mapping={'current': current_bug.id}))
else:
return True
=== modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
--- lib/canonical/launchpad/icing/style-3-0.css.in 2010-07-26 21:00:39 +0000
+++ lib/canonical/launchpad/icing/style-3-0.css.in 2010-08-06 18:42:48 +0000
@@ -1828,6 +1828,12 @@
*/
display: inline;
}
+body.tab-bugs #duplicate-actions .sprite {
+ /* Override sprite style for edit icon on "Mark as duplicate"
+ to make the text not appear on a second line.
+ */
+ display:inline;
+ }
.yui-picker-results li.sprite {
/* XXX: EdwinGrubbs 2009-07-27 bug=405476
* Override the .yui-picker-results style, so that the next icon
=== modified file 'lib/canonical/launchpad/mail/commands.py'
--- lib/canonical/launchpad/mail/commands.py 2010-08-02 02:13:52 +0000
+++ lib/canonical/launchpad/mail/commands.py 2010-08-06 18:42:48 +0000
@@ -382,30 +382,40 @@
return {'title': self.string_args[0]}
-class DuplicateEmailCommand(EditEmailCommand):
+class DuplicateEmailCommand(EmailCommand):
"""Marks a bug as a duplicate of another bug."""
implements(IBugEditEmailCommand)
_numberOfArguments = 1
- def convertArguments(self, context):
- """See EmailCommand."""
+ def execute(self, context, current_event):
+ """See IEmailCommand."""
+ self._ensureNumberOfArguments()
[bug_id] = self.string_args
- if bug_id == 'no':
+
+ if bug_id != 'no':
+ try:
+ bug = getUtility(IBugSet).getByNameOrID(bug_id)
+ except NotFoundError:
+ raise EmailProcessingError(
+ get_error_message('no-such-bug.txt', bug_id=bug_id))
+ else:
# 'no' is a special value for unmarking a bug as a duplicate.
- return {'duplicateof': None}
- try:
- bug = getUtility(IBugSet).getByNameOrID(bug_id)
- except NotFoundError:
- raise EmailProcessingError(
- get_error_message('no-such-bug.txt', bug_id=bug_id))
+ bug = None
+
duplicate_field = IBug['duplicateof'].bind(context)
try:
duplicate_field.validate(bug)
except ValidationError, error:
raise EmailProcessingError(error.doc())
- return {'duplicateof': bug}
+ context_snapshot = Snapshot(
+ context, providing=providedBy(context))
+ context.markAsDuplicate(bug)
+ current_event = ObjectModifiedEvent(
+ context, context_snapshot, 'duplicateof')
+ notify(current_event)
+ return bug, current_event
class CVEEmailCommand(EmailCommand):
=== modified file 'lib/lp/bugs/browser/bug.py'
--- lib/lp/bugs/browser/bug.py 2010-08-02 02:13:52 +0000
+++ lib/lp/bugs/browser/bug.py 2010-08-06 18:42:48 +0000
@@ -49,8 +49,9 @@
from canonical.cachedproperty import cachedproperty
from canonical.launchpad import _
+from canonical.launchpad.fields import DuplicateBug
+from canonical.launchpad.webapp.interfaces import ILaunchBag
from lp.app.errors import NotFoundError
-from canonical.launchpad.webapp.interfaces import ILaunchBag
from lp.bugs.interfaces.bug import IBug, IBugSet
from lp.bugs.interfaces.bugattachment import BugAttachmentType
from lp.bugs.interfaces.bugtask import (
@@ -659,9 +660,35 @@
field_names = ['duplicateof']
label = "Mark bug report as a duplicate"
+ def setUpFields(self):
+ """Make the readonly version of duplicateof available."""
+ super(BugMarkAsDuplicateView, self).setUpFields()
+
+ duplicateof_field = DuplicateBug(
+ __name__='duplicateof', title=_('Duplicate Of'), required=False)
+
+ self.form_fields = self.form_fields.omit('duplicateof')
+ self.form_fields = formlib.form.Fields(duplicateof_field)
+
+ @property
+ def initial_values(self):
+ """See `LaunchpadFormView.`"""
+ return {'duplicateof': self.context.bug.duplicateof}
+
@action('Change', name='change')
def change_action(self, action, data):
"""Update the bug."""
+ data = dict(data)
+ # We handle duplicate changes by hand instead of leaving it to
+ # the usual machinery because we must use bug.markAsDuplicate().
+ bug = self.context.bug
+ bug_before_modification = Snapshot(
+ bug, providing=providedBy(bug))
+ duplicateof = data.pop('duplicateof')
+ bug.markAsDuplicate(duplicateof)
+ notify(
+ ObjectModifiedEvent(bug, bug_before_modification, 'duplicateof'))
+ # Apply other changes.
self.updateBugFromData(data)
=== modified file 'lib/lp/bugs/browser/tests/bug-subscription-views.txt'
--- lib/lp/bugs/browser/tests/bug-subscription-views.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/browser/tests/bug-subscription-views.txt 2010-08-06 18:42:48 +0000
@@ -30,7 +30,7 @@
{"name12": "subscriber-12"}
>>> login('foo.bar@xxxxxxxxxxxxx')
- >>> bug_15.duplicateof = bug_13
+ >>> bug_15.markAsDuplicate(bug_13)
>>> bug_13 = getUtility(IBugSet).get(13)
>>> subscriber_ids_view = create_initialized_view(
=== modified file 'lib/lp/bugs/browser/tests/bug-views.txt'
--- lib/lp/bugs/browser/tests/bug-views.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/browser/tests/bug-views.txt 2010-08-06 18:42:48 +0000
@@ -249,7 +249,8 @@
and see how the returned link changes.
>>> bug_two = bugset.get(2)
- >>> bug_two.duplicateof = 5
+ >>> bug_five = bugset.get(5)
+ >>> bug_two.markAsDuplicate(bug_five)
>>> bug_page_view = getMultiAdapter(
... (bug_five_in_firefox.bug, request), name="+portlet-duplicates")
@@ -380,7 +381,7 @@
>>> from canonical.launchpad.ftests import syncUpdate
- >>> bug_two.duplicateof = bug_three
+ >>> bug_two.markAsDuplicate(bug_three)
>>> syncUpdate(bug_two)
>>> bug_menu.subscription().text
@@ -391,7 +392,7 @@
the current user is a member. So, for Foo Bar, bug #3 has a simple
Subscribe link initially.
- >>> bug_two.duplicateof = None
+ >>> bug_two.markAsDuplicate(None)
>>> syncUpdate(bug_two)
>>> login("foo.bar@xxxxxxxxxxxxx")
@@ -406,7 +407,7 @@
>>> bug_two.subscribe(ubuntu_team, ubuntu_team)
<BugSubscription...>
- >>> bug_two.duplicateof = bug_three
+ >>> bug_two.markAsDuplicate(bug_three)
>>> syncUpdate(bug_two)
>>> bug_menu.subscription().text
=== modified file 'lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt'
--- lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt 2010-08-06 18:42:48 +0000
@@ -125,7 +125,7 @@
Bugs that are duplicate of other bugs aren't included in the count.
- >>> another_bug.duplicateof = bug
+ >>> another_bug.markAsDuplicate(bug)
>>> syncUpdate(another_bug)
>>> view.bugs_fixed_elsewhere_count
=== modified file 'lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt'
--- lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt 2010-08-06 18:42:48 +0000
@@ -80,7 +80,7 @@
Bugs that are duplicates of other bugs will be omitted from the list as
well.
- >>> bug_b.duplicateof = bug_c
+ >>> bug_b.markAsDuplicate(bug_c)
>>> syncUpdate(bug_b)
>>> for bugtask in portlet_view.getMostRecentlyUpdatedBugTasks()[:2]:
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2010-07-29 19:48:15 +0000
+++ lib/lp/bugs/configure.zcml 2010-08-06 18:42:48 +0000
@@ -652,7 +652,6 @@
getBugTasksByPackageName
users_affected_count
users_unaffected_count
- readonly_duplicateof
users_affected
users_unaffected
users_affected_count_with_dupes
@@ -703,7 +702,6 @@
date_last_updated
date_made_private
datecreated
- duplicateof
hits
hitstimestamp
name
=== modified file 'lib/lp/bugs/doc/bug.txt'
--- lib/lp/bugs/doc/bug.txt 2010-07-29 19:12:10 +0000
+++ lib/lp/bugs/doc/bug.txt 2010-08-06 18:42:48 +0000
@@ -184,18 +184,22 @@
>>> login("foo.bar@xxxxxxxxxxxxx")
+ >>> from lp.registry.interfaces.product import IProductSet
+ >>> productset = getUtility(IProductSet)
+ >>> firefox = productset.get(4)
>>> firefox_test_bug = bugset.get(3)
- >>> firefox_test_bug.duplicateof = firefox_crashes.id
+ >>> firefox_master_bug = factory.makeBug(product=firefox)
+ >>> firefox_test_bug.markAsDuplicate(firefox_master_bug)
>>> flush_database_updates()
>>> dups_of_bug_six = bugset.searchAsUser(
- ... duplicateof=firefox_crashes, user=current_user())
+ ... duplicateof=firefox_master_bug, user=current_user())
>>> print dups_of_bug_six.count()
1
>>> dups_of_bug_six[0].id
3
- >>> firefox_test_bug.duplicateof = None
+ >>> firefox_test_bug.markAsDuplicate(None)
>>> flush_database_updates()
>>> dups_of_bug_six = bugset.searchAsUser(
... duplicateof=firefox_crashes, user=current_user())
@@ -426,11 +430,6 @@
When a public bug is filed:
- >>> from lp.registry.interfaces.product import IProductSet
- >>> productset = getUtility(IProductSet)
- >>> bugset = getUtility(IBugSet)
-
- >>> firefox = productset.get(4)
>>> params = CreateBugParams(
... title="test firefox bug", comment="blah blah blah", owner=foobar)
>>> params.setBugTarget(product=firefox)
@@ -765,7 +764,7 @@
>>> print firefox_bug.duplicateof
None
- >>> firefox_bug.duplicateof = firefox_crashes
+ >>> firefox_bug.markAsDuplicate(firefox_master_bug)
>>> bug_duplicateof_changed = ObjectModifiedEvent(
... firefox_bug, bug_before_modification, ["duplicateof"])
@@ -1241,7 +1240,7 @@
>>> dupe_affected_user = factory.makePerson(name='sheila-shakespeare')
>>> dupe_one = factory.makeBug(owner=dupe_affected_user)
- >>> dupe_one.duplicateof = test_bug
+ >>> dupe_one.markAsDuplicate(test_bug)
>>> test_bug.users_affected_count_with_dupes
3
@@ -1275,7 +1274,7 @@
>>> dupe_affected_other_user = factory.makePerson(name='napoleon-bonaparte')
>>> dupe_three = factory.makeBug(owner=dupe_affected_other_user)
- >>> dupe_three.duplicateof = test_bug
+ >>> dupe_three.markAsDuplicate(test_bug)
>>> test_bug.users_affected_count_with_dupes
4
@@ -1284,7 +1283,7 @@
user_affected_count still only increments by 1 for that user.
>>> dupe_two = factory.makeBug(owner=dupe_affected_user)
- >>> dupe_two.duplicateof = test_bug
+ >>> dupe_two.markAsDuplicate(test_bug)
>>> test_bug.users_affected_count_with_dupes
4
@@ -1438,7 +1437,7 @@
>>> new_bug_3 = bugs[3]
>>> new_bug_4 = bugs[4]
- >>> new_bug_3.duplicateof = new_bug_4
+ >>> new_bug_3.markAsDuplicate(new_bug_4)
>>> matching_bugs = getUtility(IBugSet).getDistinctBugsForBugTasks(
... bug_tasks, user=no_priv)
@@ -1566,7 +1565,7 @@
>>> dupe = factory.makeBug()
>>> subscription = dupe.subscribe(person, person)
- >>> dupe.duplicateof = bug
+ >>> dupe.markAsDuplicate(bug)
# Re-fetch the bug so that the fact that it's a duplicate definitely
# registers.
=== modified file 'lib/lp/bugs/doc/bugactivity.txt'
--- lib/lp/bugs/doc/bugactivity.txt 2010-07-22 16:27:12 +0000
+++ lib/lp/bugs/doc/bugactivity.txt 2010-08-06 18:42:48 +0000
@@ -123,7 +123,8 @@
... "id", "title", "description", "name",
... "private", "duplicateof", "security_related"]
>>> old_bug = Snapshot(bug, providing=IBug)
- >>> bug.duplicateof = 1
+ >>> latest_bug = factory.makeBug()
+ >>> bug.markAsDuplicate(latest_bug)
>>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
>>> notify(bug_edited)
>>> latest_activity = bug.activity[-1]
@@ -131,25 +132,26 @@
u'marked as duplicate'
>>> latest_activity.oldvalue is None
True
- >>> latest_activity.newvalue == u'1'
+ >>> latest_activity.newvalue == unicode(latest_bug.id)
True
-== Bug report has itss duplicate marker changed to another bug report ==
+== Bug report has its duplicate marker changed to another bug report ==
>>> edit_fields = [
... "id", "title", "description", "name", "private", "duplicateof",
... "security_related"]
>>> old_bug = Snapshot(bug, providing=IBug)
- >>> bug.duplicateof = 2
+ >>> another_bug = factory.makeBug()
+ >>> bug.markAsDuplicate(another_bug)
>>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
>>> notify(bug_edited)
>>> latest_activity = bug.activity[-1]
>>> latest_activity.whatchanged
u'changed duplicate marker'
- >>> latest_activity.oldvalue == u'1'
+ >>> latest_activity.oldvalue == unicode(latest_bug.id)
True
- >>> latest_activity.newvalue == u'2'
+ >>> latest_activity.newvalue == unicode(another_bug.id)
True
@@ -159,18 +161,58 @@
... "id", "title", "description", "name", "private", "duplicateof",
... "security_related"]
>>> old_bug = Snapshot(bug, providing=IBug)
- >>> bug.duplicateof = None
+ >>> bug.markAsDuplicate(None)
>>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
>>> notify(bug_edited)
>>> latest_activity = bug.activity[-1]
>>> latest_activity.whatchanged
u'removed duplicate marker'
- >>> latest_activity.oldvalue == u'2'
+ >>> latest_activity.oldvalue == unicode(another_bug.id)
True
>>> latest_activity.newvalue is None
True
+== A bug with multiple duplicates ==
+
+When a bug has multiple duplicates and is itself marked a duplicate,
+the duplicates are automatically duped to the same master bug. These changes
+are then reflected in the activity log for each bug itself.
+
+ >>> edit_fields = [
+ ... "id", "title", "description", "name", "private", "duplicateof",
+ ... "security_related"]
+ >>> initial_bug = factory.makeBug()
+ >>> dupe_one = factory.makeBug()
+ >>> dupe_two = factory.makeBug()
+ >>> dupe_one.markAsDuplicate(initial_bug)
+ >>> dupe_two.markAsDuplicate(initial_bug)
+
+After creating a few bugs to work with, we create a final bug and duplicate
+the initial bug against it.
+
+ >>> final_bug = factory.makeBug()
+ >>> initial_bug.markAsDuplicate(final_bug)
+
+Now, we confirm the activity log for the other bugs correctly list the
+final_bug as their master bug.
+
+ >>> latest_activity = dupe_one.activity[-1]
+ >>> print latest_activity.whatchanged
+ changed duplicate marker
+ >>> latest_activity.oldvalue == unicode(initial_bug.id)
+ True
+ >>> latest_activity.newvalue == unicode(final_bug.id)
+ True
+ >>> latest_activity = dupe_two.activity[-1]
+ >>> print latest_activity.whatchanged
+ changed duplicate marker
+ >>> latest_activity.oldvalue == unicode(initial_bug.id)
+ True
+ >>> latest_activity.newvalue == unicode(final_bug.id)
+ True
+
+
== BugActivityItem ==
BugActivityItem implements the stuff that BugActivity doesn't need to
=== modified file 'lib/lp/bugs/doc/bugnotification-sending.txt'
--- lib/lp/bugs/doc/bugnotification-sending.txt 2010-07-22 20:30:26 +0000
+++ lib/lp/bugs/doc/bugnotification-sending.txt 2010-08-06 18:42:48 +0000
@@ -760,13 +760,14 @@
>>> switch_db_to_launchpad()
>>> new_bug = ubuntu.createBug(params)
>>> switch_db_to_bugnotification()
-
>>> flush_notifications()
If a bug is a duplicate of another bug, a marker gets inserted at the
top of the email:
- >>> new_bug.duplicateof = bug_one
+ >>> switch_db_to_launchpad()
+ >>> new_bug.markAsDuplicate(bug_one)
+ >>> switch_db_to_bugnotification()
>>> comment = getUtility(IMessageSet).fromText(
... 'subject', 'a comment.', sample_person,
... datecreated=ten_minutes_ago)
=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt 2010-08-06 18:42:48 +0000
@@ -341,7 +341,7 @@
Stuart Bishop
Ubuntu Team
- >>> linux_source_bug_dupe.duplicateof = linux_source_bug
+ >>> linux_source_bug_dupe.markAsDuplicate(linux_source_bug)
>>> linux_source_bug_dupe.syncUpdate()
>>> print_displayname(linux_source_bug.getIndirectSubscribers())
@@ -868,7 +868,7 @@
... comment="one more dupe test description",
... owner=keybuk)
>>> dupe_ff_bug = firefox.createBug(params)
- >>> dupe_ff_bug.duplicateof = ff_bug
+ >>> dupe_ff_bug.markAsDuplicate(ff_bug)
>>> dupe_ff_bug.syncUpdate()
>>> dupe_ff_bug.subscribe(foobar, lifeless)
<BugSubscription at ...>
=== modified file 'lib/lp/bugs/doc/bugtask-package-bugcounts.txt'
--- lib/lp/bugs/doc/bugtask-package-bugcounts.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/doc/bugtask-package-bugcounts.txt 2010-08-06 18:42:48 +0000
@@ -170,7 +170,7 @@
Duplicates bugs are omitted from the counts.
>>> from canonical.launchpad.interfaces import IBugSet
- >>> bug.duplicateof = getUtility(IBugSet).get(1)
+ >>> bug.markAsDuplicate(getUtility(IBugSet).get(1))
>>> syncUpdate(bug)
>>> package_counts = getUtility(IBugTaskSet).getBugCountsForPackages(
... user=foo_bar, packages=packages)
=== modified file 'lib/lp/bugs/doc/bugtask-search.txt'
--- lib/lp/bugs/doc/bugtask-search.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/doc/bugtask-search.txt 2010-08-06 18:42:48 +0000
@@ -1037,7 +1037,7 @@
>>> bug_nine = getUtility(IBugSet).get(9)
>>> bug_ten = getUtility(IBugSet).get(10)
- >>> bug_ten.duplicateof = bug_nine
+ >>> bug_ten.markAsDuplicate(bug_nine)
>>> flush_database_updates()
Searching again reveals bug #9 at the top of the list, since it now has a duplicate.
=== modified file 'lib/lp/bugs/doc/bugzilla-import.txt'
--- lib/lp/bugs/doc/bugzilla-import.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/doc/bugzilla-import.txt 2010-08-06 18:42:48 +0000
@@ -87,8 +87,7 @@
>>> duplicates = [
... (1, 2),
- ... (2, 3),
- ... (2, 4),
+ ... (3, 4),
... ]
>>> class FakeBackend:
@@ -536,9 +535,9 @@
None
>>> bug2.duplicateof == bug1
True
- >>> bug3.duplicateof == bug2
+ >>> bug3.duplicateof == None
True
- >>> bug4.duplicateof == bug2
+ >>> bug4.duplicateof == bug3
True
=== modified file 'lib/lp/bugs/doc/checkwatches.txt'
--- lib/lp/bugs/doc/checkwatches.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/doc/checkwatches.txt 2010-08-06 18:42:48 +0000
@@ -490,8 +490,8 @@
another bug, comments won't be synced and the bug won't be linked back
to the remote bug.
+ >>> LaunchpadZopelessLayer.switchDbUser('launchpad')
>>> bug_15 = getUtility(IBugSet).get(15)
- >>> bug_watch.bug.duplicateof = bug_15
- >>> transaction.commit()
+ >>> bug_watch.bug.markAsDuplicate(bug_15)
>>> updater.updateBugWatches(remote_system, [bug_watch], now=nowish)
=== modified file 'lib/lp/bugs/doc/malone-karma.txt'
--- lib/lp/bugs/doc/malone-karma.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/doc/malone-karma.txt 2010-08-06 18:42:48 +0000
@@ -204,7 +204,7 @@
>>> bug_one = getUtility(IBugSet).get(1)
>>> old_bug = Snapshot(bug, providing=IBug)
- >>> bug.duplicateof = bug_one
+ >>> bug.markAsDuplicate(bug_one)
(Notice how changing a bug with multiple bugtasks will assign karma to you
once for each bugtask. This is so because we consider changes in a bug to
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py 2010-08-02 02:13:52 +0000
+++ lib/lp/bugs/interfaces/bug.py 2010-08-06 18:42:48 +0000
@@ -184,8 +184,7 @@
ownerID = Int(title=_('Owner'), required=True, readonly=True)
owner = exported(
Reference(IPerson, title=_("The owner's IPerson"), readonly=True))
- duplicateof = DuplicateBug(title=_('Duplicate Of'), required=False)
- readonly_duplicateof = exported(
+ duplicateof = exported(
DuplicateBug(title=_('Duplicate Of'), required=False, readonly=True),
exported_as='duplicate_of')
# This is redefined from IPrivacy.private because the attribute is
@@ -755,8 +754,8 @@
def markUserAffected(user, affected=True):
"""Mark :user: as affected by this bug."""
- @mutator_for(readonly_duplicateof)
- @operation_parameters(duplicate_of=copy_field(readonly_duplicateof))
+ @mutator_for(duplicateof)
+ @operation_parameters(duplicate_of=copy_field(duplicateof))
@export_write_operation()
def markAsDuplicate(duplicate_of):
"""Mark this bug as a duplicate of another."""
=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
--- lib/lp/bugs/javascript/bugtask_index.js 2010-07-22 16:27:12 +0000
+++ lib/lp/bugs/javascript/bugtask_index.js 2010-08-06 18:42:48 +0000
@@ -165,10 +165,23 @@
update_dupe_url = update_dupe_link.get('href');
var mark_dupe_form_url = update_dupe_url + '/++form++';
+ var form_header = '<p>Marking this bug as a duplicate will, by default, ' +
+ 'hide it from search results listings.</p>';
+
+ var has_dupes = Y.one('#portlet-duplicates');
+ if (has_dupes !== null) {
+ form_header = form_header +
+ '<p style="padding:2px 2px 0 36px;" ' +
+ 'class="large-warning"><strong>Note:</strong> ' +
+ 'This bug has duplicates of its own. ' +
+ 'If you go ahead, they too will become duplicates of ' +
+ 'the bug you specify here. This cannot be undone.' +
+ '</p></div>';
+ }
+
duplicate_form_overlay = new Y.lazr.FormOverlay({
headerContent: '<h2>Mark bug report as duplicate</h2>',
- form_header: 'Marking the bug as a duplicate will, by default, ' +
- 'hide it from search results listings.',
+ form_header: form_header,
form_submit_button: Y.Node.create(submit_button_html),
form_cancel_button: Y.Node.create(cancel_button_html),
centered: true,
@@ -187,6 +200,7 @@
if (duplicate_form_overlay){
e.preventDefault();
duplicate_form_overlay.show();
+ Y.DOM.byId('field.duplicateof').focus();
}
});
// Add a class denoting them as js-action links.
@@ -366,8 +380,15 @@
// Hide the formoverlay:
duplicate_form_overlay.hide();
+ // Hide the dupe edit icon if it exists.
+ var dupe_edit_icon = Y.one('#change_duplicate_bug');
+ if (dupe_edit_icon !== null) {
+ dupe_edit_icon.setStyle('display', 'none');
+ }
+
// Add the spinner...
var dupe_span = Y.one('#mark-duplicate-text');
+ dupe_span.removeClass('sprite bug-dupe');
dupe_span.addClass('update-in-progress-message');
// Set the new duplicate link on the bug entry.
@@ -390,28 +411,35 @@
if (new_dup_url !== null) {
dupe_span.set('innerHTML', [
- 'Duplicate of <a>bug #</a> ',
- '<a class="menu-link-mark-dupe js-action sprite edit">',
- '<span class="invisible-link">edit</span></a>'].join(""));
+ '<a id="change_duplicate_bug" ',
+ 'title="Edit or remove linked duplicate bug" ',
+ 'class="sprite edit"></a>',
+ 'Duplicate of <a>bug #</a>'].join(""));
dupe_span.all('a').item(0)
+ .set('href', update_dupe_url);
+ dupe_span.all('a').item(1)
.set('href', '/bugs/' + new_dup_id)
.appendChild(document.createTextNode(new_dup_id));
- dupe_span.all('a').item(1)
- .set('href', update_dupe_url);
+ var has_dupes = Y.one('#portlet-duplicates');
+ if (has_dupes !== null) {
+ has_dupes.get('parentNode').removeChild(has_dupes);
+ }
show_comment_on_duplicate_warning();
} else {
+ dupe_span.addClass('sprite bug-dupe');
dupe_span.set('innerHTML', [
- '<a class="menu-link-mark-dupe js-action ',
- 'sprite bug-dupe">Mark as duplicate</a>'].join(""));
+ '<a class="menu-link-mark-dupe js-action">',
+ 'Mark as duplicate</a>'].join(""));
dupe_span.one('a').set('href', update_dupe_url);
hide_comment_on_duplicate_warning();
}
Y.lazr.anim.green_flash({node: dupe_span}).run();
// ensure the new link is hooked up correctly:
- dupe_span.one('a.menu-link-mark-dupe').on(
+ dupe_span.one('a').on(
'click', function(e){
e.preventDefault();
duplicate_form_overlay.show();
+ Y.DOM.byId('field.duplicateof').focus();
});
},
failure: function(id, request) {
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2010-08-02 02:13:52 +0000
+++ lib/lp/bugs/model/bug.py 2010-08-06 18:42:48 +0000
@@ -1493,11 +1493,6 @@
self.updateHeat()
- @property
- def readonly_duplicateof(self):
- """See `IBug`."""
- return self.duplicateof
-
def markAsDuplicate(self, duplicate_of):
"""See `IBug`."""
field = DuplicateBug()
@@ -1506,6 +1501,16 @@
try:
if duplicate_of is not None:
field._validate(duplicate_of)
+ if self.duplicates:
+ for duplicate in self.duplicates:
+ # Fire a notify event in model code since moving
+ # duplicates of a duplicate does not normally fire an
+ # event.
+ dupe_before = Snapshot(
+ duplicate, providing=providedBy(duplicate))
+ duplicate.markAsDuplicate(duplicate_of)
+ notify(ObjectModifiedEvent(
+ duplicate, dupe_before, 'duplicateof'))
self.duplicateof = duplicate_of
except LaunchpadValidationError, validation_error:
raise InvalidDuplicateValue(validation_error)
=== modified file 'lib/lp/bugs/scripts/bugimport.py'
--- lib/lp/bugs/scripts/bugimport.py 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/scripts/bugimport.py 2010-08-06 18:42:48 +0000
@@ -466,7 +466,7 @@
self.logger.info(
'Marking bug %d as duplicate of bug %d',
other_bug.id, bug.id)
- other_bug.duplicateof = bug
+ other_bug.markAsDuplicate(bug)
del self.pending_duplicates[bug_id]
# Process this bug as a duplicate
if duplicateof is not None:
@@ -478,7 +478,7 @@
self.logger.info(
'Marking bug %d as duplicate of bug %d',
bug.id, other_bug.id)
- bug.duplicateof = other_bug
+ bug.markAsDuplicate(other_bug)
else:
self.pending_duplicates.setdefault(
duplicateof, []).append(bug.id)
=== modified file 'lib/lp/bugs/scripts/bugzilla.py'
--- lib/lp/bugs/scripts/bugzilla.py 2010-08-02 02:13:52 +0000
+++ lib/lp/bugs/scripts/bugzilla.py 2010-08-06 18:42:48 +0000
@@ -638,7 +638,7 @@
lpdupe.duplicateof is None):
logger.info('Marking %d as a duplicate of %d',
lpdupe.id, lpdupe_of.id)
- lpdupe.duplicateof = lpdupe_of
+ lpdupe.markAsDuplicate(lpdupe_of)
trans.commit()
def importBugs(self, trans, product=None, component=None, status=None):
=== removed directory 'lib/lp/bugs/stories/duplicate-bug-handling'
=== removed file 'lib/lp/bugs/stories/duplicate-bug-handling/10-mark-bug-as-duplicate.txt'
--- lib/lp/bugs/stories/duplicate-bug-handling/10-mark-bug-as-duplicate.txt 2010-07-22 19:59:00 +0000
+++ lib/lp/bugs/stories/duplicate-bug-handling/10-mark-bug-as-duplicate.txt 1970-01-01 00:00:00 +0000
@@ -1,48 +0,0 @@
-First, visit the +duplicate page on the bug you're looking at:
-
- >>> print http(r"""
- ... GET /debian/+source/mozilla-firefox/+bug/2/+duplicate HTTP/1.1
- ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
- ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
- ... """)
- HTTP/1.1 200 Ok
- ...
-
-Now, let's go ahead and mark bug 2 as a duplicate of bug 1.
-
- >>> print http(r"""
- ... POST /debian/+source/mozilla-firefox/+bug/2/+duplicate HTTP/1.1
- ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
- ... Content-Length: 309
- ... Content-Type: multipart/form-data; boundary=---------------------------17976867087135254391306687757
- ...
- ... -----------------------------17976867087135254391306687757
- ... Content-Disposition: form-data; name="field.duplicateof"
- ...
- ... 1
- ... -----------------------------17976867087135254391306687757
- ... Content-Disposition: form-data; name="field.actions.change"
- ...
- ... Change
- ... -----------------------------17976867087135254391306687757--
- ... """)
- HTTP/1.1 303 See Other
- ...
- Content-Length: 0
- ...
- Location: http://.../debian/+source/mozilla-firefox/+bug/2
- ...
-
-When the bug is marked as a duplicate, a notification was generated, to
-tell bug 2's subscribers that their bug is a dupe of bug 1:
-
- >>> from canonical.launchpad.database import BugNotification
- >>> BugNotification.select().count()
- 1
-
- >>> notification = BugNotification.select(orderBy='-id')[0]
- >>> notification.bug.id
- 2
- >>> print notification.message.text_contents
- ** This bug has been marked a duplicate of bug 1
- ...
=== removed file 'lib/lp/bugs/stories/duplicate-bug-handling/20-show-bug-is-duplicate.txt'
--- lib/lp/bugs/stories/duplicate-bug-handling/20-show-bug-is-duplicate.txt 2010-07-22 19:59:00 +0000
+++ lib/lp/bugs/stories/duplicate-bug-handling/20-show-bug-is-duplicate.txt 1970-01-01 00:00:00 +0000
@@ -1,41 +0,0 @@
-When a bug is marked as a duplicate of another bug, the duplicate bug's
-page shows this prominently, and the comment field is accompanied by an extra
-warning that it's a duplicate.
-
- >>> user_browser.open(
- ... 'http://launchpad.dev/debian/+source/mozilla-firefox/+bug/2')
- >>> print find_tag_by_id(
- ... user_browser.contents, 'duplicate-of').renderContents()
- bug #1...
- >>> for message in find_tags_by_class(user_browser.contents, 'message'):
- ... print extract_text(message)
- Remember, this bug report is a duplicate of bug #1. Comment here only if...
-
-The "Affects" lines are also not expandable, preventing people from changing
-the bug's status to no effect.
-
- >>> affects_table = find_tags_by_class(user_browser.contents, 'listing')[0]
- >>> "toggleFormVisibility('task" in affects_table
- False
-
-If the bug is no longer marked as a duplicate,
-
- >>> user_browser.getLink(id="change_duplicate_bug").click()
- >>> user_browser.url
- 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2/+duplicate'
- >>> user_browser.getControl(name='field.duplicateof').value = ''
- >>> user_browser.getControl('Change').click()
- >>> user_browser.url
- 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2'
-
-the duplicate notifications disappear,
-
- >>> find_tags_by_class(user_browser.contents, 'message')
- []
-
-and the "Affects" lines become expandable once more.
-
- >>> affects_table = find_tags_by_class(user_browser.contents, 'listing')[0]
- >>> print affects_table
- <...
- ...toggleFormVisibility('task...
=== removed file 'lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt'
--- lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt 2010-07-22 19:59:00 +0000
+++ lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt 1970-01-01 00:00:00 +0000
@@ -1,76 +0,0 @@
-=================================
-Mark as duplicate form validation
-=================================
-
- >>> bg3 = 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/3'
- >>> bug_3_dup_url = '%s/+duplicate' % bg3
- >>> user_browser.open(bug_3_dup_url)
-
-Tests the marking of a bug that is a duplicate of a duplicate.
-
- >>> user_browser.getControl('Duplicate Of').value = '6'
- >>> user_browser.getControl('Change').click()
- >>> print user_browser.contents
- <...
- <p class="error message">There is 1 error.</p>
- ...
- ...already a duplicate...
-
-Tests the marking of a bug that is a duplicate of itself.
-
- >>> user_browser.getControl('Duplicate Of').value = '3'
- >>> user_browser.getControl('Change').click()
- >>> print user_browser.contents
- <...
- <p class="error message">There is 1 error.</p>
- ...
- ...can't mark a bug as a duplicate of itself...
- ...
-
-Tests the marking of a bug that is already marked as duplicate with
-the same value. In this case, nothing should happen, since nothing
-changed.
-
- >>> user_browser.open(
- ... 'http://bugs.launchpad.dev/firefox/+bug/6/+duplicate')
- >>> user_browser.getControl('Duplicate Of').value
- '5'
- >>> user_browser.getControl('Change').click()
- >>> user_browser.url
- 'http://bugs.launchpad.dev/firefox/+bug/6'
-
-Tests the input of data that is not a valid value.
-
- >>> user_browser.open(
- ... 'http://bugs.launchpad.dev/firefox/+bug/6/+duplicate')
- >>> user_browser.getControl('Duplicate Of').value = 'xajskd'
- >>> user_browser.getControl('Change').click()
- >>> print user_browser.contents
- <...
- ...Not a valid bug number or nickname...
- ...
-
-Tests using a bug nickname as the duplicate value
-
- >>> user_browser.open(
- ... 'http://bugs.launchpad.dev/evolution/+bug/7/+duplicate')
- >>> user_browser.getControl('Duplicate Of').value = 'blackhole'
- >>> user_browser.getControl('Change').click()
- >>> user_browser.url
- 'http://bugs.launchpad.dev/evolution/+bug/7'
-
-Marking a bugX as a duplicate of bugY is not allowed if bugX has dupes.
-
- >>> user_browser.open(
- ... 'http://bugs.launchpad.dev/firefox/+bug/5/+duplicate')
- >>> user_browser.getControl('Duplicate Of').value = '4'
- >>> user_browser.getControl('Change').click()
- >>> print user_browser.contents
- <...
- ...There are other bugs already...
- ...
-
- >>> from canonical.launchpad.database import Bug
- >>> Bug.get(5).duplicateof is None
- True
-
=== modified file 'lib/lp/bugs/templates/bug-portlet-actions.pt'
--- lib/lp/bugs/templates/bug-portlet-actions.pt 2010-07-22 16:27:12 +0000
+++ lib/lp/bugs/templates/bug-portlet-actions.pt 2010-08-06 18:42:48 +0000
@@ -11,10 +11,11 @@
tal:define="link context_menu/markduplicate"
tal:condition="python: link.enabled and not
context.duplicateof"
+ class="sprite bug-dupe"
>
<a
tal:attributes="href link/path"
- class="menu-link-mark-dupe sprite bug-dupe">Mark as duplicate</a>
+ class="menu-link-mark-dupe">Mark as duplicate</a>
</span>
<tal:block
tal:condition="context/duplicates"
@@ -31,8 +32,7 @@
id="change_duplicate_bug"
title="Edit or remove linked duplicate bug"
class="sprite edit"
- tal:attributes="href link/url"></a>
- <span id="mark-duplicate-text">Duplicate of
+ tal:attributes="href link/url"></a><span id="mark-duplicate-text">Duplicate of
<a
tal:condition="duplicateof/required:launchpad.View"
tal:attributes="href duplicateof/fmt:url; title
=== modified file 'lib/lp/bugs/tests/bug.py'
--- lib/lp/bugs/tests/bug.py 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/tests/bug.py 2010-08-06 18:42:48 +0000
@@ -196,7 +196,8 @@
params = CreateBugParams(
owner=no_priv, title=title, comment='Something is broken.')
bug = target.createBug(params)
- bug.duplicateof = duplicateof
+ if duplicateof is not None:
+ bug.markAsDuplicate(duplicateof)
sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
if with_message is True:
bug.newMessage(
=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
--- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-08-06 18:42:48 +0000
@@ -926,32 +926,6 @@
duplicate itself.
...
-If the specified bug has other bugs marked as a duplicate of it, an
-error message is sent telling you this.
-
- >>> bug_four = getUtility(IBugSet).get(4)
- >>> bug_four.duplicateof.id
- 2
-
- >>> from canonical.launchpad.ftests import syncUpdate
- >>> syncUpdate(bug_four)
- >>> submit_commands(bug_four.duplicateof, 'duplicate 1')
- >>> bug_four.duplicateof.id
- 2
-
- >>> print_latest_email()
- Subject: Submit Request Failure
- To: test@xxxxxxxxxxxxx
- <BLANKLINE>
- ...
- Failing command:
- duplicate 1
- ...
- There are other bugs already marked as duplicates of Bug 2.
- These bugs should be changed to be duplicates of another bug
- if you are certain you would like to perform this change.
- ...
-
=== cve $cve ===
@@ -1916,7 +1890,7 @@
... Subject: A bug with no affects
...
... private yes
- ... duplicate 1
+ ... unsubscribe
... cve 1999-8979
... """
=== modified file 'lib/lp/bugs/tests/bugtarget-bugcount.txt'
--- lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-08-06 18:42:48 +0000
@@ -129,7 +129,7 @@
... bugtarget, "Another Test Bug", status=BugTaskStatus.NEW)
>>> Store.of(another_bug).flush()
>>> old_counts = bugtarget.getBugCounts(None)
- >>> another_bug.duplicateof = bug
+ >>> another_bug.markAsDuplicate(bug)
>>> syncUpdate(another_bug)
>>> new_counts = bugtarget.getBugCounts(None)
>>> print_count_difference(
=== modified file 'lib/lp/bugs/tests/test_bugchanges.py'
--- lib/lp/bugs/tests/test_bugchanges.py 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/tests/test_bugchanges.py 2010-08-06 18:42:48 +0000
@@ -90,7 +90,10 @@
:return: The value of `attribute` before modification.
"""
obj_before_modification = Snapshot(obj, providing=providedBy(obj))
- setattr(obj, attribute, new_value)
+ if attribute == 'duplicateof':
+ obj.markAsDuplicate(new_value)
+ else:
+ setattr(obj, attribute, new_value)
notify(ObjectModifiedEvent(
obj, obj_before_modification, [attribute], self.user))
@@ -1304,7 +1307,7 @@
duplicate_bug = self.factory.makeBug()
duplicate_bug_recipients = duplicate_bug.getBugNotificationRecipients(
level=BugNotificationLevel.METADATA).getRecipients()
- duplicate_bug.duplicateof = self.bug
+ duplicate_bug.markAsDuplicate(self.bug)
self.saveOldChanges(duplicate_bug)
self.changeAttribute(duplicate_bug, 'duplicateof', None)
@@ -1335,7 +1338,7 @@
bug_two = self.factory.makeBug()
bug_recipients = self.bug.getBugNotificationRecipients(
level=BugNotificationLevel.METADATA).getRecipients()
- self.bug.duplicateof = bug_one
+ self.bug.markAsDuplicate(bug_one)
self.saveOldChanges()
self.changeAttribute(self.bug, 'duplicateof', bug_two)
=== modified file 'lib/lp/bugs/tests/test_bugnotification.py'
--- lib/lp/bugs/tests/test_bugnotification.py 2010-07-22 12:17:41 +0000
+++ lib/lp/bugs/tests/test_bugnotification.py 2010-08-06 18:42:48 +0000
@@ -155,7 +155,7 @@
user='test@xxxxxxxxxxxxx')
self.bug = self.factory.makeBug()
self.dupe_bug = self.factory.makeBug()
- self.dupe_bug.duplicateof = self.bug
+ self.dupe_bug.markAsDuplicate(self.bug)
self.dupe_subscribers = set(
self.dupe_bug.getDirectSubscribers() +
self.dupe_bug.getIndirectSubscribers())
=== added file 'lib/lp/bugs/tests/test_duplicate_handling.py'
--- lib/lp/bugs/tests/test_duplicate_handling.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/tests/test_duplicate_handling.py 2010-08-06 18:42:48 +0000
@@ -0,0 +1,102 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for bug duplicate validation."""
+
+from textwrap import dedent
+import unittest
+
+from zope.security.interfaces import ForbiddenAttribute
+
+from canonical.testing import DatabaseFunctionalLayer
+
+from lp.bugs.interfaces.bug import InvalidDuplicateValue
+from lp.testing import TestCaseWithFactory
+
+
+class TestDuplicateAttributes(TestCaseWithFactory):
+ """Test bug attributes related to duplicate handling."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestDuplicateAttributes, self).setUp(user='test@xxxxxxxxxxxxx')
+
+ def setDuplicateofDirectly(self, bug, duplicateof):
+ """Helper method to set duplicateof directly."""
+ bug.duplicateof = duplicateof
+
+ def test_duplicateof_readonly(self):
+ # Test that no one can set duplicateof directly.
+ bug = self.factory.makeBug()
+ dupe_bug = self.factory.makeBug()
+ self.assertRaises(
+ ForbiddenAttribute, self.setDuplicateofDirectly, bug, dupe_bug)
+
+
+class TestMarkDuplicateValidation(TestCaseWithFactory):
+ """Test for validation around marking bug duplicates."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestMarkDuplicateValidation, self).setUp(
+ user='test@xxxxxxxxxxxxx')
+ self.bug = self.factory.makeBug()
+ self.dupe_bug = self.factory.makeBug()
+ self.dupe_bug.markAsDuplicate(self.bug)
+ self.possible_dupe = self.factory.makeBug()
+
+ def assertDuplicateError(self, bug, duplicateof, msg):
+ try:
+ bug.markAsDuplicate(duplicateof)
+ except InvalidDuplicateValue, err:
+ self.assertEqual(str(err), msg)
+
+ def test_error_on_duplicate_to_duplicate(self):
+ # Test that a bug cannot be marked a duplicate of
+ # a bug that is already itself a duplicate.
+ msg = dedent(u"""
+ Bug %s is already a duplicate of bug %s. You
+ can only mark a bug report as duplicate of one that
+ isn't a duplicate itself.
+ """ % (
+ self.dupe_bug.id, self.dupe_bug.duplicateof.id))
+ self.assertDuplicateError(
+ self.possible_dupe, self.dupe_bug, msg)
+
+ def test_error_duplicate_to_itself(self):
+ # Test that a bug cannot be marked its own duplicate
+ msg = dedent(u"""
+ You can't mark a bug as a duplicate of itself.""")
+ self.assertDuplicateError(self.bug, self.bug, msg)
+
+
+class TestMoveDuplicates(TestCaseWithFactory):
+ """Test duplicates are moved when master bug is marked a duplicate."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestMoveDuplicates, self).setUp(user='test@xxxxxxxxxxxxx')
+
+ def test_duplicates_are_moved(self):
+ # Confirm that a bug with two duplicates can be marked
+ # a duplicate of a new bug and that the duplicates will
+ # be re-marked as duplicates of the new bug, too.
+ bug = self.factory.makeBug()
+ dupe_one = self.factory.makeBug()
+ dupe_two = self.factory.makeBug()
+ dupe_one.markAsDuplicate(bug)
+ dupe_two.markAsDuplicate(bug)
+ self.assertEqual(dupe_one.duplicateof, bug)
+ self.assertEqual(dupe_two.duplicateof, bug)
+ new_bug = self.factory.makeBug()
+ bug.markAsDuplicate(new_bug)
+ self.assertEqual(bug.duplicateof, new_bug)
+ self.assertEqual(dupe_one.duplicateof, new_bug)
+ self.assertEqual(dupe_two.duplicateof, new_bug)
+
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromName(__name__)
=== modified file 'lib/lp/bugs/windmill/tests/test_mark_duplicate.py'
--- lib/lp/bugs/windmill/tests/test_mark_duplicate.py 2010-07-26 16:00:01 +0000
+++ lib/lp/bugs/windmill/tests/test_mark_duplicate.py 2010-08-06 18:42:48 +0000
@@ -44,10 +44,13 @@
client.waits.forElement(
xpath=MAIN_FORM_ELEMENT, timeout=constants.FOR_ELEMENT)
- # Initially the form overlay is hidden
+ # Initially the form overlay is hidden...
client.asserts.assertElemJS(
xpath=MAIN_FORM_ELEMENT, js=FORM_NOT_VISIBLE)
+ # ...and there is an expandable form for editing bug status, etc.
+ client.asserts.assertNode(classname='bug-status-expand')
+
# Clicking on the mark duplicate link brings up the formoverlay.
# Entering 1 as the duplicate ID changes the duplicate text.
client.click(classname=u'menu-link-mark-dupe')
@@ -66,7 +69,7 @@
id='warning-comment-on-duplicate', timeout=constants.FOR_ELEMENT)
# The duplicate can be cleared:
- client.click(classname=u'menu-link-mark-dupe')
+ client.click(id=u'mark-duplicate-text')
client.type(text=u'', id=u'field.duplicateof')
client.click(xpath=CHANGE_BUTTON)
client.waits.forElement(
@@ -77,7 +80,7 @@
client.asserts.assertNotNode(id='warning-comment-on-duplicate')
# Entering a false bug number results in input validation errors
- client.click(classname=u'menu-link-mark-dupe')
+ client.click(id=u'mark-duplicate-text')
client.type(text=u'123', id=u'field.duplicateof')
client.click(xpath=CHANGE_BUTTON)
error_xpath = (
@@ -105,6 +108,14 @@
xpath=u"//h1[@id='bug-title']/span[1]",
validator=u'Firefox does not support SVG')
+ # If someone wants to set the master to dupe another bug, there
+ # is a warning in the dupe widget about this bug having its own
+ # duplicates.
+ client.click(classname='menu-link-mark-dupe')
+ client.asserts.assertTextIn(
+ classname='large-warning', validator=u'This bug has duplicates',
+ timeout=constants.FOR_ELEMENT)
+
# When we go back to the page for the duplicate bug...
client.open(url=u'http://bugs.launchpad.dev:8085/bugs/15')
client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
@@ -115,6 +126,9 @@
# as the one we saw before.
client.asserts.assertNode(id='warning-comment-on-duplicate')
+ # Duplicate pages also do not have the expandable form on them.
+ client.asserts.assertNotNode(classname='bug-status-expand')
+
# Once we remove the duplicate mark...
client.click(id=u'change_duplicate_bug')
client.type(text=u'', id=u'field.duplicateof')
=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
--- lib/lp/registry/browser/tests/person-views.txt 2010-07-22 12:17:41 +0000
+++ lib/lp/registry/browser/tests/person-views.txt 2010-08-06 18:42:48 +0000
@@ -546,7 +546,7 @@
But duplicate bugs are never displayed.
- >>> another_bug.duplicateof = bug
+ >>> another_bug.markAsDuplicate(bug)
# Create a new view because we're testing some cached properties.
=== modified file 'versions.cfg'
--- versions.cfg 2010-08-05 09:57:43 +0000
+++ versions.cfg 2010-08-06 18:42:48 +0000
@@ -64,9 +64,7 @@
simplesettings = 0.4
SimpleTal = 4.1
sourcecodegen = 0.6.9
-# This is Storm 0.15 with r342 cherry-picked which fixes a memory leak
-# important for message sharing migration script.
-storm = 0.15danilo-storm-launchpad-r342
+storm = 0.17
# Has the LessThan matcher.
testtools = 0.9.6dev91
transaction = 1.0.0