Merge lp:~deryck/launchpad/do-the-right-thing-dupe-move-78596 into lp:launchpad

Proposed by Deryck Hodge
Status: Merged
Approved by: Deryck Hodge
Approved revision: no longer in the source branch.
Merged at revision: 11188
Proposed branch: lp:~deryck/launchpad/do-the-right-thing-dupe-move-78596
Merge into: lp:launchpad
Diff against target: 1018 lines (+214/-276)
31 files modified
lib/canonical/launchpad/fields/__init__.py (+0/-11)
lib/canonical/launchpad/mail/commands.py (+21/-11)
lib/lp/bugs/browser/bug.py (+27/-0)
lib/lp/bugs/browser/tests/bug-subscription-views.txt (+1/-1)
lib/lp/bugs/browser/tests/bug-views.txt (+5/-4)
lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt (+1/-1)
lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt (+1/-1)
lib/lp/bugs/configure.zcml (+0/-2)
lib/lp/bugs/doc/bug.txt (+13/-14)
lib/lp/bugs/doc/bugactivity.txt (+9/-7)
lib/lp/bugs/doc/bugnotification-sending.txt (+3/-2)
lib/lp/bugs/doc/bugsubscription.txt (+2/-2)
lib/lp/bugs/doc/bugtask-package-bugcounts.txt (+1/-1)
lib/lp/bugs/doc/bugtask-search.txt (+1/-1)
lib/lp/bugs/doc/bugzilla-import.txt (+3/-4)
lib/lp/bugs/doc/checkwatches.txt (+2/-2)
lib/lp/bugs/doc/malone-karma.txt (+1/-1)
lib/lp/bugs/interfaces/bug.py (+3/-4)
lib/lp/bugs/model/bug.py (+3/-5)
lib/lp/bugs/scripts/bugimport.py (+2/-2)
lib/lp/bugs/scripts/bugzilla.py (+1/-1)
lib/lp/bugs/stories/duplicate-bug-handling/10-mark-bug-as-duplicate.txt (+0/-48)
lib/lp/bugs/stories/duplicate-bug-handling/20-show-bug-is-duplicate.txt (+0/-41)
lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt (+0/-76)
lib/lp/bugs/tests/bug.py (+2/-1)
lib/lp/bugs/tests/bugs-emailinterface.txt (+1/-27)
lib/lp/bugs/tests/bugtarget-bugcount.txt (+1/-1)
lib/lp/bugs/tests/test_bugchanges.py (+6/-3)
lib/lp/bugs/tests/test_bugnotification.py (+1/-1)
lib/lp/bugs/tests/test_duplicate_handling.py (+102/-0)
lib/lp/registry/browser/tests/person-views.txt (+1/-1)
To merge this branch: bzr merge lp:~deryck/launchpad/do-the-right-thing-dupe-move-78596
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+27144@code.launchpad.net

Commit message

Converge on using markAsDuplicate rather than setting duplicateof directly. Also, enable automatic moving of duplicates when marking a bug a duplicate that itself has duplicates.

Description of the change

This branch fixes the bugs team's handling of setting duplicates. Before
this work, there were two ways to set a duplicate of a bug -- setting
duplicateof directly or using markAsDuplicate. Setting duplicateof
directly was commonly used; however, this bypassed validation provided in
markAsDuplicate and really should not have been done. This branch fixes
that by converting to use markAsDuplicate consistently and changing the
interface and zcml to make duplicateof readonly. I also removed the
previous field readonly_duplicate, which seemed to be a hackish way to
achieve this for the API without doing all the work here.

This fixes bug 589660.

While I was here and because I was changing how validation was tested, I
decided to do a three line fix to enable moving of duplicates. Until now,
if you wanted to mark a bug as a duplicate and that bug had duplicates
itself, you had to manually move all the duplicates. This changes that
behavior so that the duplicates are moved across automatically.

This fixes bug 78596.

I don't want to land this branch until I add a confirmation dialog in
JavaScript for marking duplicate bug that also has duplicates. This was
suggested during a bugs team standup, since it would be a lot of work to
undo the damage if people aren't aware of the coming change. This branch
has gotten long with all the tests changed, so I want to review it first
before doing the small UI work left.

I had pre-implementation discussions with Björn and mid-implementation
discussions with Abel.

Appologies for the length of the diff, but most of this is removing dead
tests and one-line changes to lots of files to move from using duplicateof
to markAsDuplicate.

Files changed:

 lib/lp/bugs/configure.zcml
 lib/lp/bugs/interfaces/bug.py
 lib/lp/bugs/model/bug.py
    Removed readonly_duplicate and made duplicateof
    a properly readonly field.

 lib/lp/bugs/browser/bug.py
    Update form to use markAsDuplicate and not
    set duplicateof directly.

 lib/lp/bugs/browser/tests/bug-subscription-views.txt
 lib/lp/bugs/browser/tests/bug-views.txt
 lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt
 lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt
 lib/lp/bugs/doc/bug.txt
 lib/lp/bugs/doc/bugactivity.txt
 lib/lp/bugs/doc/bugnotification-sending.txt
 lib/lp/bugs/doc/bugsubscription.txt
 lib/lp/bugs/doc/bugtask-package-bugcounts.txt
 lib/lp/bugs/doc/bugtask-search.txt
 lib/lp/bugs/doc/bugzilla-import.txt
 lib/lp/bugs/doc/checkwatches.txt
 lib/lp/bugs/doc/malone-karma.txt
 lib/lp/bugs/scripts/bugimport.py
 lib/lp/bugs/scripts/bugzilla.py
 lib/lp/bugs/scripts/tests/test_bugheat.py
 lib/lp/bugs/tests/bug.py
 lib/lp/bugs/tests/bugtarget-bugcount.txt
 lib/lp/bugs/tests/test_bugchanges.py
 lib/lp/bugs/tests/test_bugnotification.py
    All tests and call sites updated to use markAsDuplicate,
    rather than set duplicateof directly

    In some cases, I changed the data used in the test
    because tests were wrong. Using duplicateof
    bypassed validation, and tests were settings bugs
    to duplicate of bugs that were already themselves
    duplicates.

 lib/lp/bugs/stories/duplicate-bug-handling/10-mark-bug-as-duplicate.txt
 lib/lp/bugs/stories/duplicate-bug-handling/20-show-bug-is-duplicate.txt
 lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt
    Removed page tests that are now covered by other
    tests, either the new dupe handling unit test or
    Windmill test.

 lib/lp/bugs/tests/test_duplicate_handling.py
    Added unit test to verify duplicateof is read-only,
    that validation works, and that duplicates are
    moved properly.

 lib/canonical/launchpad/fields/__init__.py
    Remove validation preventing moving duplicates.

To post a comment you must log in.
Revision history for this message
Deryck Hodge (deryck) wrote :

I've filed Bug #591705 about the remaining UI work before this can land.

Revision history for this message
Abel Deuring (adeuring) wrote :

Overall, the changes look good. I only have one concern (sorry for coming up so late with it...): We have the new behaviour that marking bug A as a duplicate of bug B changes all old duplicates of bug A into duplicates of bug B. This is not recorded in the activity log of the duplicates, leading to the odd situation that you might have bug C marked as a duplcate of bug B, while the activity log says that it is a duplicate of bug A...

Also, having activity log entries for the changes of the duplicate_of value gives us the opportunity to revert the duplicate_of value of bug C, when bug A is "unmarked" as a duplicate of bug B.

But that's something for another branch (assuming that we think it is worth the effort), so I'm marking this one as approved. thanks for the tedious work of cleaning up the duplicate_of handling!

review: Approve (code)
Revision history for this message
Francis J. Lacoste (flacoste) wrote :

What about the API aspect of this?

Shouldn't markAsDuplicate becomes a mutator for the field?

That's also arguably a backward incompatible change, so should be tagged
accordingly.

Talk to Leonard for help on how to do this.

--
Francis J. Lacoste
<email address hidden>

Revision history for this message
Deryck Hodge (deryck) wrote :

On Wed, Jun 9, 2010 at 9:55 AM, Francis J. Lacoste
<email address hidden> wrote:
> What about the API aspect of this?
>
> Shouldn't markAsDuplicate becomes a mutator for the field?
>

markAsDuplicate has always been the mutator for duplicateof. This
change affects only the internal use of duplicateof vs.
markAsDuplicate. The API should be unaffected.

Cheers,
deryck

Revision history for this message
Deryck Hodge (deryck) wrote :

Abel, I agree we should do the activity log handling correctly. I'll fix that in my UI branch for this work.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/fields/__init__.py'
--- lib/canonical/launchpad/fields/__init__.py 2010-02-17 11:19:42 +0000
+++ lib/canonical/launchpad/fields/__init__.py 2010-07-21 12:33:49 +0000
@@ -302,12 +302,8 @@
302 bug isn't a duplicate of itself, otherwise302 bug isn't a duplicate of itself, otherwise
303 return False.303 return False.
304 """304 """
305 from lp.bugs.interfaces.bug import IBugSet
306 bugset = getUtility(IBugSet)
307 current_bug = self.context305 current_bug = self.context
308 dup_target = value306 dup_target = value
309 current_bug_has_dup_refs = bool(bugset.searchAsUser(
310 user=getUtility(ILaunchBag).user, duplicateof=current_bug))
311 if current_bug == dup_target:307 if current_bug == dup_target:
312 raise LaunchpadValidationError(_(dedent("""308 raise LaunchpadValidationError(_(dedent("""
313 You can't mark a bug as a duplicate of itself.""")))309 You can't mark a bug as a duplicate of itself.""")))
@@ -318,13 +314,6 @@
318 isn't a duplicate itself.314 isn't a duplicate itself.
319 """), mapping={'dup': dup_target.id,315 """), mapping={'dup': dup_target.id,
320 'orig': dup_target.duplicateof.id}))316 'orig': dup_target.duplicateof.id}))
321 elif current_bug_has_dup_refs:
322 raise LaunchpadValidationError(_(dedent("""
323 There are other bugs already marked as duplicates of
324 Bug ${current}. These bugs should be changed to be
325 duplicates of another bug if you are certain you would
326 like to perform this change."""),
327 mapping={'current': current_bug.id}))
328 else:317 else:
329 return True318 return True
330319
331320
=== modified file 'lib/canonical/launchpad/mail/commands.py'
--- lib/canonical/launchpad/mail/commands.py 2010-05-25 03:11:16 +0000
+++ lib/canonical/launchpad/mail/commands.py 2010-07-21 12:33:49 +0000
@@ -382,30 +382,40 @@
382 return {'title': self.string_args[0]}382 return {'title': self.string_args[0]}
383383
384384
385class DuplicateEmailCommand(EditEmailCommand):385class DuplicateEmailCommand(EmailCommand):
386 """Marks a bug as a duplicate of another bug."""386 """Marks a bug as a duplicate of another bug."""
387387
388 implements(IBugEditEmailCommand)388 implements(IBugEditEmailCommand)
389 _numberOfArguments = 1389 _numberOfArguments = 1
390390
391 def convertArguments(self, context):391 def execute(self, context, current_event):
392 """See EmailCommand."""392 """See IEmailCommand."""
393 self._ensureNumberOfArguments()
393 [bug_id] = self.string_args394 [bug_id] = self.string_args
394 if bug_id == 'no':395
396 if bug_id != 'no':
397 try:
398 bug = getUtility(IBugSet).getByNameOrID(bug_id)
399 except NotFoundError:
400 raise EmailProcessingError(
401 get_error_message('no-such-bug.txt', bug_id=bug_id))
402 else:
395 # 'no' is a special value for unmarking a bug as a duplicate.403 # 'no' is a special value for unmarking a bug as a duplicate.
396 return {'duplicateof': None}404 bug = None
397 try:405
398 bug = getUtility(IBugSet).getByNameOrID(bug_id)
399 except NotFoundError:
400 raise EmailProcessingError(
401 get_error_message('no-such-bug.txt', bug_id=bug_id))
402 duplicate_field = IBug['duplicateof'].bind(context)406 duplicate_field = IBug['duplicateof'].bind(context)
403 try:407 try:
404 duplicate_field.validate(bug)408 duplicate_field.validate(bug)
405 except ValidationError, error:409 except ValidationError, error:
406 raise EmailProcessingError(error.doc())410 raise EmailProcessingError(error.doc())
407411
408 return {'duplicateof': bug}412 context_snapshot = Snapshot(
413 context, providing=providedBy(context))
414 context.markAsDuplicate(bug)
415 current_event = ObjectModifiedEvent(
416 context, context_snapshot, 'duplicateof')
417 notify(current_event)
418 return bug, current_event
409419
410420
411class CVEEmailCommand(EmailCommand):421class CVEEmailCommand(EmailCommand):
412422
=== modified file 'lib/lp/bugs/browser/bug.py'
--- lib/lp/bugs/browser/bug.py 2010-07-15 08:38:19 +0000
+++ lib/lp/bugs/browser/bug.py 2010-07-21 12:33:49 +0000
@@ -49,6 +49,7 @@
49from canonical.cachedproperty import cachedproperty49from canonical.cachedproperty import cachedproperty
5050
51from canonical.launchpad import _51from canonical.launchpad import _
52from canonical.launchpad.fields import DuplicateBug
52from canonical.launchpad.webapp.interfaces import ILaunchBag, NotFoundError53from canonical.launchpad.webapp.interfaces import ILaunchBag, NotFoundError
53from lp.bugs.interfaces.bug import IBug, IBugSet54from lp.bugs.interfaces.bug import IBug, IBugSet
54from lp.bugs.interfaces.bugattachment import BugAttachmentType55from lp.bugs.interfaces.bugattachment import BugAttachmentType
@@ -658,9 +659,35 @@
658 field_names = ['duplicateof']659 field_names = ['duplicateof']
659 label = "Mark bug report as a duplicate"660 label = "Mark bug report as a duplicate"
660661
662 def setUpFields(self):
663 """Make the readonly version of duplicateof available."""
664 super(BugMarkAsDuplicateView, self).setUpFields()
665
666 duplicateof_field = DuplicateBug(
667 __name__='duplicateof', title=_('Duplicate Of'), required=False)
668
669 self.form_fields = self.form_fields.omit('duplicateof')
670 self.form_fields = formlib.form.Fields(duplicateof_field)
671
672 @property
673 def initial_values(self):
674 """See `LaunchpadFormView.`"""
675 return {'duplicateof': self.context.bug.duplicateof}
676
661 @action('Change', name='change')677 @action('Change', name='change')
662 def change_action(self, action, data):678 def change_action(self, action, data):
663 """Update the bug."""679 """Update the bug."""
680 data = dict(data)
681 # We handle duplicate changes by hand instead of leaving it to
682 # the usual machinery because we must use bug.markAsDuplicate().
683 bug = self.context.bug
684 bug_before_modification = Snapshot(
685 bug, providing=providedBy(bug))
686 duplicateof = data.pop('duplicateof')
687 bug.markAsDuplicate(duplicateof)
688 notify(
689 ObjectModifiedEvent(bug, bug_before_modification, 'duplicateof'))
690 # Apply other changes.
664 self.updateBugFromData(data)691 self.updateBugFromData(data)
665692
666693
667694
=== modified file 'lib/lp/bugs/browser/tests/bug-subscription-views.txt'
--- lib/lp/bugs/browser/tests/bug-subscription-views.txt 2009-11-18 11:53:50 +0000
+++ lib/lp/bugs/browser/tests/bug-subscription-views.txt 2010-07-21 12:33:49 +0000
@@ -30,7 +30,7 @@
30 {"name12": "subscriber-12"}30 {"name12": "subscriber-12"}
3131
32 >>> login('foo.bar@canonical.com')32 >>> login('foo.bar@canonical.com')
33 >>> bug_15.duplicateof = bug_1333 >>> bug_15.markAsDuplicate(bug_13)
34 >>> bug_13 = getUtility(IBugSet).get(13)34 >>> bug_13 = getUtility(IBugSet).get(13)
3535
36 >>> subscriber_ids_view = create_initialized_view(36 >>> subscriber_ids_view = create_initialized_view(
3737
=== modified file 'lib/lp/bugs/browser/tests/bug-views.txt'
--- lib/lp/bugs/browser/tests/bug-views.txt 2010-06-25 20:37:53 +0000
+++ lib/lp/bugs/browser/tests/bug-views.txt 2010-07-21 12:33:49 +0000
@@ -249,7 +249,8 @@
249and see how the returned link changes.249and see how the returned link changes.
250250
251 >>> bug_two = bugset.get(2)251 >>> bug_two = bugset.get(2)
252 >>> bug_two.duplicateof = 5252 >>> bug_five = bugset.get(5)
253 >>> bug_two.markAsDuplicate(bug_five)
253254
254 >>> bug_page_view = getMultiAdapter(255 >>> bug_page_view = getMultiAdapter(
255 ... (bug_five_in_firefox.bug, request), name="+portlet-duplicates")256 ... (bug_five_in_firefox.bug, request), name="+portlet-duplicates")
@@ -380,7 +381,7 @@
380381
381 >>> from canonical.launchpad.ftests import syncUpdate382 >>> from canonical.launchpad.ftests import syncUpdate
382383
383 >>> bug_two.duplicateof = bug_three384 >>> bug_two.markAsDuplicate(bug_three)
384 >>> syncUpdate(bug_two)385 >>> syncUpdate(bug_two)
385386
386 >>> bug_menu.subscription().text387 >>> bug_menu.subscription().text
@@ -391,7 +392,7 @@
391 the current user is a member. So, for Foo Bar, bug #3 has a simple392 the current user is a member. So, for Foo Bar, bug #3 has a simple
392 Subscribe link initially.393 Subscribe link initially.
393394
394 >>> bug_two.duplicateof = None395 >>> bug_two.markAsDuplicate(None)
395 >>> syncUpdate(bug_two)396 >>> syncUpdate(bug_two)
396397
397 >>> login("foo.bar@canonical.com")398 >>> login("foo.bar@canonical.com")
@@ -406,7 +407,7 @@
406 >>> bug_two.subscribe(ubuntu_team, ubuntu_team)407 >>> bug_two.subscribe(ubuntu_team, ubuntu_team)
407 <BugSubscription...>408 <BugSubscription...>
408409
409 >>> bug_two.duplicateof = bug_three410 >>> bug_two.markAsDuplicate(bug_three)
410 >>> syncUpdate(bug_two)411 >>> syncUpdate(bug_two)
411412
412 >>> bug_menu.subscription().text413 >>> bug_menu.subscription().text
413414
=== modified file 'lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt'
--- lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt 2009-11-27 14:05:09 +0000
+++ lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt 2010-07-21 12:33:49 +0000
@@ -125,7 +125,7 @@
125125
126Bugs that are duplicate of other bugs aren't included in the count.126Bugs that are duplicate of other bugs aren't included in the count.
127127
128 >>> another_bug.duplicateof = bug128 >>> another_bug.markAsDuplicate(bug)
129 >>> syncUpdate(another_bug)129 >>> syncUpdate(another_bug)
130130
131 >>> view.bugs_fixed_elsewhere_count131 >>> view.bugs_fixed_elsewhere_count
132132
=== modified file 'lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt'
--- lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt 2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt 2010-07-21 12:33:49 +0000
@@ -80,7 +80,7 @@
80Bugs that are duplicates of other bugs will be omitted from the list as80Bugs that are duplicates of other bugs will be omitted from the list as
81well.81well.
8282
83 >>> bug_b.duplicateof = bug_c83 >>> bug_b.markAsDuplicate(bug_c)
84 >>> syncUpdate(bug_b)84 >>> syncUpdate(bug_b)
8585
86 >>> for bugtask in portlet_view.getMostRecentlyUpdatedBugTasks()[:2]:86 >>> for bugtask in portlet_view.getMostRecentlyUpdatedBugTasks()[:2]:
8787
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2010-07-12 16:32:31 +0000
+++ lib/lp/bugs/configure.zcml 2010-07-21 12:33:49 +0000
@@ -649,7 +649,6 @@
649 getBugTasksByPackageName649 getBugTasksByPackageName
650 users_affected_count650 users_affected_count
651 users_unaffected_count651 users_unaffected_count
652 readonly_duplicateof
653 users_affected652 users_affected
654 users_unaffected653 users_unaffected
655 users_affected_count_with_dupes654 users_affected_count_with_dupes
@@ -700,7 +699,6 @@
700 date_last_updated699 date_last_updated
701 date_made_private700 date_made_private
702 datecreated701 datecreated
703 duplicateof
704 hits702 hits
705 hitstimestamp703 hitstimestamp
706 name704 name
707705
=== modified file 'lib/lp/bugs/doc/bug.txt'
--- lib/lp/bugs/doc/bug.txt 2010-06-29 15:48:57 +0000
+++ lib/lp/bugs/doc/bug.txt 2010-07-21 12:33:49 +0000
@@ -184,18 +184,22 @@
184184
185 >>> login("foo.bar@canonical.com")185 >>> login("foo.bar@canonical.com")
186186
187 >>> from lp.registry.interfaces.product import IProductSet
188 >>> productset = getUtility(IProductSet)
189 >>> firefox = productset.get(4)
187 >>> firefox_test_bug = bugset.get(3)190 >>> firefox_test_bug = bugset.get(3)
188 >>> firefox_test_bug.duplicateof = firefox_crashes.id191 >>> firefox_master_bug = factory.makeBug(product=firefox)
192 >>> firefox_test_bug.markAsDuplicate(firefox_master_bug)
189 >>> flush_database_updates()193 >>> flush_database_updates()
190194
191 >>> dups_of_bug_six = bugset.searchAsUser(195 >>> dups_of_bug_six = bugset.searchAsUser(
192 ... duplicateof=firefox_crashes, user=current_user())196 ... duplicateof=firefox_master_bug, user=current_user())
193 >>> print dups_of_bug_six.count()197 >>> print dups_of_bug_six.count()
194 1198 1
195 >>> dups_of_bug_six[0].id199 >>> dups_of_bug_six[0].id
196 3200 3
197201
198 >>> firefox_test_bug.duplicateof = None202 >>> firefox_test_bug.markAsDuplicate(None)
199 >>> flush_database_updates()203 >>> flush_database_updates()
200 >>> dups_of_bug_six = bugset.searchAsUser(204 >>> dups_of_bug_six = bugset.searchAsUser(
201 ... duplicateof=firefox_crashes, user=current_user())205 ... duplicateof=firefox_crashes, user=current_user())
@@ -426,11 +430,6 @@
426430
427When a public bug is filed:431When a public bug is filed:
428432
429 >>> from lp.registry.interfaces.product import IProductSet
430 >>> productset = getUtility(IProductSet)
431 >>> bugset = getUtility(IBugSet)
432
433 >>> firefox = productset.get(4)
434 >>> params = CreateBugParams(433 >>> params = CreateBugParams(
435 ... title="test firefox bug", comment="blah blah blah", owner=foobar)434 ... title="test firefox bug", comment="blah blah blah", owner=foobar)
436 >>> params.setBugTarget(product=firefox)435 >>> params.setBugTarget(product=firefox)
@@ -765,7 +764,7 @@
765764
766 >>> print firefox_bug.duplicateof765 >>> print firefox_bug.duplicateof
767 None766 None
768 >>> firefox_bug.duplicateof = firefox_crashes767 >>> firefox_bug.markAsDuplicate(firefox_master_bug)
769768
770 >>> bug_duplicateof_changed = ObjectModifiedEvent(769 >>> bug_duplicateof_changed = ObjectModifiedEvent(
771 ... firefox_bug, bug_before_modification, ["duplicateof"])770 ... firefox_bug, bug_before_modification, ["duplicateof"])
@@ -1274,7 +1273,7 @@
12741273
1275 >>> dupe_affected_user = factory.makePerson(name='sheila-shakespeare')1274 >>> dupe_affected_user = factory.makePerson(name='sheila-shakespeare')
1276 >>> dupe_one = factory.makeBug(owner=dupe_affected_user)1275 >>> dupe_one = factory.makeBug(owner=dupe_affected_user)
1277 >>> dupe_one.duplicateof = test_bug1276 >>> dupe_one.markAsDuplicate(test_bug)
1278 >>> test_bug.users_affected_count_with_dupes1277 >>> test_bug.users_affected_count_with_dupes
1279 31278 3
12801279
@@ -1308,7 +1307,7 @@
13081307
1309 >>> dupe_affected_other_user = factory.makePerson(name='napoleon-bonaparte')1308 >>> dupe_affected_other_user = factory.makePerson(name='napoleon-bonaparte')
1310 >>> dupe_three = factory.makeBug(owner=dupe_affected_other_user)1309 >>> dupe_three = factory.makeBug(owner=dupe_affected_other_user)
1311 >>> dupe_three.duplicateof = test_bug1310 >>> dupe_three.markAsDuplicate(test_bug)
1312 >>> test_bug.users_affected_count_with_dupes1311 >>> test_bug.users_affected_count_with_dupes
1313 41312 4
13141313
@@ -1317,7 +1316,7 @@
1317user_affected_count still only increments by 1 for that user.1316user_affected_count still only increments by 1 for that user.
13181317
1319 >>> dupe_two = factory.makeBug(owner=dupe_affected_user)1318 >>> dupe_two = factory.makeBug(owner=dupe_affected_user)
1320 >>> dupe_two.duplicateof = test_bug1319 >>> dupe_two.markAsDuplicate(test_bug)
1321 >>> test_bug.users_affected_count_with_dupes1320 >>> test_bug.users_affected_count_with_dupes
1322 41321 4
13231322
@@ -1471,7 +1470,7 @@
14711470
1472 >>> new_bug_3 = bugs[3]1471 >>> new_bug_3 = bugs[3]
1473 >>> new_bug_4 = bugs[4]1472 >>> new_bug_4 = bugs[4]
1474 >>> new_bug_3.duplicateof = new_bug_41473 >>> new_bug_3.markAsDuplicate(new_bug_4)
1475 >>> matching_bugs = getUtility(IBugSet).getDistinctBugsForBugTasks(1474 >>> matching_bugs = getUtility(IBugSet).getDistinctBugsForBugTasks(
1476 ... bug_tasks, user=no_priv)1475 ... bug_tasks, user=no_priv)
14771476
@@ -1599,7 +1598,7 @@
1599 >>> dupe = factory.makeBug()1598 >>> dupe = factory.makeBug()
1600 >>> subscription = dupe.subscribe(person, person)1599 >>> subscription = dupe.subscribe(person, person)
16011600
1602 >>> dupe.duplicateof = bug1601 >>> dupe.markAsDuplicate(bug)
16031602
1604 # Re-fetch the bug so that the fact that it's a duplicate definitely1603 # Re-fetch the bug so that the fact that it's a duplicate definitely
1605 # registers.1604 # registers.
16061605
=== modified file 'lib/lp/bugs/doc/bugactivity.txt'
--- lib/lp/bugs/doc/bugactivity.txt 2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/doc/bugactivity.txt 2010-07-21 12:33:49 +0000
@@ -123,7 +123,8 @@
123 ... "id", "title", "description", "name",123 ... "id", "title", "description", "name",
124 ... "private", "duplicateof", "security_related"]124 ... "private", "duplicateof", "security_related"]
125 >>> old_bug = Snapshot(bug, providing=IBug)125 >>> old_bug = Snapshot(bug, providing=IBug)
126 >>> bug.duplicateof = 1126 >>> latest_bug = factory.makeBug()
127 >>> bug.markAsDuplicate(latest_bug)
127 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)128 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
128 >>> notify(bug_edited)129 >>> notify(bug_edited)
129 >>> latest_activity = bug.activity[-1]130 >>> latest_activity = bug.activity[-1]
@@ -131,7 +132,7 @@
131 u'marked as duplicate'132 u'marked as duplicate'
132 >>> latest_activity.oldvalue is None133 >>> latest_activity.oldvalue is None
133 True134 True
134 >>> latest_activity.newvalue == u'1'135 >>> latest_activity.newvalue == unicode(latest_bug.id)
135 True136 True
136137
137138
@@ -141,15 +142,16 @@
141 ... "id", "title", "description", "name", "private", "duplicateof",142 ... "id", "title", "description", "name", "private", "duplicateof",
142 ... "security_related"]143 ... "security_related"]
143 >>> old_bug = Snapshot(bug, providing=IBug)144 >>> old_bug = Snapshot(bug, providing=IBug)
144 >>> bug.duplicateof = 2145 >>> another_bug = factory.makeBug()
146 >>> bug.markAsDuplicate(another_bug)
145 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)147 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
146 >>> notify(bug_edited)148 >>> notify(bug_edited)
147 >>> latest_activity = bug.activity[-1]149 >>> latest_activity = bug.activity[-1]
148 >>> latest_activity.whatchanged150 >>> latest_activity.whatchanged
149 u'changed duplicate marker'151 u'changed duplicate marker'
150 >>> latest_activity.oldvalue == u'1'152 >>> latest_activity.oldvalue == unicode(latest_bug.id)
151 True153 True
152 >>> latest_activity.newvalue == u'2'154 >>> latest_activity.newvalue == unicode(another_bug.id)
153 True155 True
154156
155157
@@ -159,13 +161,13 @@
159 ... "id", "title", "description", "name", "private", "duplicateof",161 ... "id", "title", "description", "name", "private", "duplicateof",
160 ... "security_related"]162 ... "security_related"]
161 >>> old_bug = Snapshot(bug, providing=IBug)163 >>> old_bug = Snapshot(bug, providing=IBug)
162 >>> bug.duplicateof = None164 >>> bug.markAsDuplicate(None)
163 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)165 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
164 >>> notify(bug_edited)166 >>> notify(bug_edited)
165 >>> latest_activity = bug.activity[-1]167 >>> latest_activity = bug.activity[-1]
166 >>> latest_activity.whatchanged168 >>> latest_activity.whatchanged
167 u'removed duplicate marker'169 u'removed duplicate marker'
168 >>> latest_activity.oldvalue == u'2'170 >>> latest_activity.oldvalue == unicode(another_bug.id)
169 True171 True
170 >>> latest_activity.newvalue is None172 >>> latest_activity.newvalue is None
171 True173 True
172174
=== modified file 'lib/lp/bugs/doc/bugnotification-sending.txt'
--- lib/lp/bugs/doc/bugnotification-sending.txt 2010-06-23 21:24:13 +0000
+++ lib/lp/bugs/doc/bugnotification-sending.txt 2010-07-21 12:33:49 +0000
@@ -760,13 +760,14 @@
760 >>> switch_db_to_launchpad()760 >>> switch_db_to_launchpad()
761 >>> new_bug = ubuntu.createBug(params)761 >>> new_bug = ubuntu.createBug(params)
762 >>> switch_db_to_bugnotification()762 >>> switch_db_to_bugnotification()
763
764 >>> flush_notifications()763 >>> flush_notifications()
765764
766If a bug is a duplicate of another bug, a marker gets inserted at the765If a bug is a duplicate of another bug, a marker gets inserted at the
767top of the email:766top of the email:
768767
769 >>> new_bug.duplicateof = bug_one768 >>> switch_db_to_launchpad()
769 >>> new_bug.markAsDuplicate(bug_one)
770 >>> switch_db_to_bugnotification()
770 >>> comment = getUtility(IMessageSet).fromText(771 >>> comment = getUtility(IMessageSet).fromText(
771 ... 'subject', 'a comment.', sample_person,772 ... 'subject', 'a comment.', sample_person,
772 ... datecreated=ten_minutes_ago)773 ... datecreated=ten_minutes_ago)
773774
=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt 2010-05-25 15:43:59 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt 2010-07-21 12:33:49 +0000
@@ -341,7 +341,7 @@
341 Stuart Bishop341 Stuart Bishop
342 Ubuntu Team342 Ubuntu Team
343343
344 >>> linux_source_bug_dupe.duplicateof = linux_source_bug344 >>> linux_source_bug_dupe.markAsDuplicate(linux_source_bug)
345 >>> linux_source_bug_dupe.syncUpdate()345 >>> linux_source_bug_dupe.syncUpdate()
346346
347 >>> print_displayname(linux_source_bug.getIndirectSubscribers())347 >>> print_displayname(linux_source_bug.getIndirectSubscribers())
@@ -868,7 +868,7 @@
868 ... comment="one more dupe test description",868 ... comment="one more dupe test description",
869 ... owner=keybuk)869 ... owner=keybuk)
870 >>> dupe_ff_bug = firefox.createBug(params)870 >>> dupe_ff_bug = firefox.createBug(params)
871 >>> dupe_ff_bug.duplicateof = ff_bug871 >>> dupe_ff_bug.markAsDuplicate(ff_bug)
872 >>> dupe_ff_bug.syncUpdate()872 >>> dupe_ff_bug.syncUpdate()
873 >>> dupe_ff_bug.subscribe(foobar, lifeless)873 >>> dupe_ff_bug.subscribe(foobar, lifeless)
874 <BugSubscription at ...>874 <BugSubscription at ...>
875875
=== modified file 'lib/lp/bugs/doc/bugtask-package-bugcounts.txt'
--- lib/lp/bugs/doc/bugtask-package-bugcounts.txt 2010-04-15 21:13:29 +0000
+++ lib/lp/bugs/doc/bugtask-package-bugcounts.txt 2010-07-21 12:33:49 +0000
@@ -170,7 +170,7 @@
170Duplicates bugs are omitted from the counts.170Duplicates bugs are omitted from the counts.
171171
172 >>> from canonical.launchpad.interfaces import IBugSet172 >>> from canonical.launchpad.interfaces import IBugSet
173 >>> bug.duplicateof = getUtility(IBugSet).get(1)173 >>> bug.markAsDuplicate(getUtility(IBugSet).get(1))
174 >>> syncUpdate(bug)174 >>> syncUpdate(bug)
175 >>> package_counts = getUtility(IBugTaskSet).getBugCountsForPackages(175 >>> package_counts = getUtility(IBugTaskSet).getBugCountsForPackages(
176 ... user=foo_bar, packages=packages)176 ... user=foo_bar, packages=packages)
177177
=== modified file 'lib/lp/bugs/doc/bugtask-search.txt'
--- lib/lp/bugs/doc/bugtask-search.txt 2010-05-28 14:53:57 +0000
+++ lib/lp/bugs/doc/bugtask-search.txt 2010-07-21 12:33:49 +0000
@@ -1037,7 +1037,7 @@
10371037
1038 >>> bug_nine = getUtility(IBugSet).get(9)1038 >>> bug_nine = getUtility(IBugSet).get(9)
1039 >>> bug_ten = getUtility(IBugSet).get(10)1039 >>> bug_ten = getUtility(IBugSet).get(10)
1040 >>> bug_ten.duplicateof = bug_nine1040 >>> bug_ten.markAsDuplicate(bug_nine)
1041 >>> flush_database_updates()1041 >>> flush_database_updates()
10421042
1043Searching again reveals bug #9 at the top of the list, since it now has a duplicate.1043Searching again reveals bug #9 at the top of the list, since it now has a duplicate.
10441044
=== modified file 'lib/lp/bugs/doc/bugzilla-import.txt'
--- lib/lp/bugs/doc/bugzilla-import.txt 2010-04-08 14:34:58 +0000
+++ lib/lp/bugs/doc/bugzilla-import.txt 2010-07-21 12:33:49 +0000
@@ -87,8 +87,7 @@
8787
88 >>> duplicates = [88 >>> duplicates = [
89 ... (1, 2),89 ... (1, 2),
90 ... (2, 3),90 ... (3, 4),
91 ... (2, 4),
92 ... ]91 ... ]
9392
94 >>> class FakeBackend:93 >>> class FakeBackend:
@@ -536,9 +535,9 @@
536 None535 None
537 >>> bug2.duplicateof == bug1536 >>> bug2.duplicateof == bug1
538 True537 True
539 >>> bug3.duplicateof == bug2538 >>> bug3.duplicateof == None
540 True539 True
541 >>> bug4.duplicateof == bug2540 >>> bug4.duplicateof == bug3
542 True541 True
543542
544543
545544
=== modified file 'lib/lp/bugs/doc/checkwatches.txt'
--- lib/lp/bugs/doc/checkwatches.txt 2010-04-23 11:19:49 +0000
+++ lib/lp/bugs/doc/checkwatches.txt 2010-07-21 12:33:49 +0000
@@ -490,8 +490,8 @@
490another bug, comments won't be synced and the bug won't be linked back490another bug, comments won't be synced and the bug won't be linked back
491to the remote bug.491to the remote bug.
492492
493 >>> LaunchpadZopelessLayer.switchDbUser('launchpad')
493 >>> bug_15 = getUtility(IBugSet).get(15)494 >>> bug_15 = getUtility(IBugSet).get(15)
494 >>> bug_watch.bug.duplicateof = bug_15495 >>> bug_watch.bug.markAsDuplicate(bug_15)
495 >>> transaction.commit()
496496
497 >>> updater.updateBugWatches(remote_system, [bug_watch], now=nowish)497 >>> updater.updateBugWatches(remote_system, [bug_watch], now=nowish)
498498
=== modified file 'lib/lp/bugs/doc/malone-karma.txt'
--- lib/lp/bugs/doc/malone-karma.txt 2010-02-16 20:36:48 +0000
+++ lib/lp/bugs/doc/malone-karma.txt 2010-07-21 12:33:49 +0000
@@ -204,7 +204,7 @@
204204
205 >>> bug_one = getUtility(IBugSet).get(1)205 >>> bug_one = getUtility(IBugSet).get(1)
206 >>> old_bug = Snapshot(bug, providing=IBug)206 >>> old_bug = Snapshot(bug, providing=IBug)
207 >>> bug.duplicateof = bug_one207 >>> bug.markAsDuplicate(bug_one)
208208
209 (Notice how changing a bug with multiple bugtasks will assign karma to you209 (Notice how changing a bug with multiple bugtasks will assign karma to you
210 once for each bugtask. This is so because we consider changes in a bug to210 once for each bugtask. This is so because we consider changes in a bug to
211211
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py 2010-06-30 21:19:36 +0000
+++ lib/lp/bugs/interfaces/bug.py 2010-07-21 12:33:49 +0000
@@ -183,8 +183,7 @@
183 ownerID = Int(title=_('Owner'), required=True, readonly=True)183 ownerID = Int(title=_('Owner'), required=True, readonly=True)
184 owner = exported(184 owner = exported(
185 Reference(IPerson, title=_("The owner's IPerson"), readonly=True))185 Reference(IPerson, title=_("The owner's IPerson"), readonly=True))
186 duplicateof = DuplicateBug(title=_('Duplicate Of'), required=False)186 duplicateof = exported(
187 readonly_duplicateof = exported(
188 DuplicateBug(title=_('Duplicate Of'), required=False, readonly=True),187 DuplicateBug(title=_('Duplicate Of'), required=False, readonly=True),
189 exported_as='duplicate_of')188 exported_as='duplicate_of')
190 # This is redefined from IPrivacy.private because the attribute is189 # This is redefined from IPrivacy.private because the attribute is
@@ -754,8 +753,8 @@
754 def markUserAffected(user, affected=True):753 def markUserAffected(user, affected=True):
755 """Mark :user: as affected by this bug."""754 """Mark :user: as affected by this bug."""
756755
757 @mutator_for(readonly_duplicateof)756 @mutator_for(duplicateof)
758 @operation_parameters(duplicate_of=copy_field(readonly_duplicateof))757 @operation_parameters(duplicate_of=copy_field(duplicateof))
759 @export_write_operation()758 @export_write_operation()
760 def markAsDuplicate(duplicate_of):759 def markAsDuplicate(duplicate_of):
761 """Mark this bug as a duplicate of another."""760 """Mark this bug as a duplicate of another."""
762761
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2010-07-19 12:08:05 +0000
+++ lib/lp/bugs/model/bug.py 2010-07-21 12:33:49 +0000
@@ -1493,11 +1493,6 @@
14931493
1494 self.updateHeat()1494 self.updateHeat()
14951495
1496 @property
1497 def readonly_duplicateof(self):
1498 """See `IBug`."""
1499 return self.duplicateof
1500
1501 def markAsDuplicate(self, duplicate_of):1496 def markAsDuplicate(self, duplicate_of):
1502 """See `IBug`."""1497 """See `IBug`."""
1503 field = DuplicateBug()1498 field = DuplicateBug()
@@ -1506,6 +1501,9 @@
1506 try:1501 try:
1507 if duplicate_of is not None:1502 if duplicate_of is not None:
1508 field._validate(duplicate_of)1503 field._validate(duplicate_of)
1504 if self.duplicates:
1505 for duplicate in self.duplicates:
1506 duplicate.markAsDuplicate(duplicate_of)
1509 self.duplicateof = duplicate_of1507 self.duplicateof = duplicate_of
1510 except LaunchpadValidationError, validation_error:1508 except LaunchpadValidationError, validation_error:
1511 raise InvalidDuplicateValue(validation_error)1509 raise InvalidDuplicateValue(validation_error)
15121510
=== modified file 'lib/lp/bugs/scripts/bugimport.py'
--- lib/lp/bugs/scripts/bugimport.py 2010-04-14 12:55:44 +0000
+++ lib/lp/bugs/scripts/bugimport.py 2010-07-21 12:33:49 +0000
@@ -466,7 +466,7 @@
466 self.logger.info(466 self.logger.info(
467 'Marking bug %d as duplicate of bug %d',467 'Marking bug %d as duplicate of bug %d',
468 other_bug.id, bug.id)468 other_bug.id, bug.id)
469 other_bug.duplicateof = bug469 other_bug.markAsDuplicate(bug)
470 del self.pending_duplicates[bug_id]470 del self.pending_duplicates[bug_id]
471 # Process this bug as a duplicate471 # Process this bug as a duplicate
472 if duplicateof is not None:472 if duplicateof is not None:
@@ -478,7 +478,7 @@
478 self.logger.info(478 self.logger.info(
479 'Marking bug %d as duplicate of bug %d',479 'Marking bug %d as duplicate of bug %d',
480 bug.id, other_bug.id)480 bug.id, other_bug.id)
481 bug.duplicateof = other_bug481 bug.markAsDuplicate(other_bug)
482 else:482 else:
483 self.pending_duplicates.setdefault(483 self.pending_duplicates.setdefault(
484 duplicateof, []).append(bug.id)484 duplicateof, []).append(bug.id)
485485
=== modified file 'lib/lp/bugs/scripts/bugzilla.py'
--- lib/lp/bugs/scripts/bugzilla.py 2010-05-27 13:51:06 +0000
+++ lib/lp/bugs/scripts/bugzilla.py 2010-07-21 12:33:49 +0000
@@ -638,7 +638,7 @@
638 lpdupe.duplicateof is None):638 lpdupe.duplicateof is None):
639 logger.info('Marking %d as a duplicate of %d',639 logger.info('Marking %d as a duplicate of %d',
640 lpdupe.id, lpdupe_of.id)640 lpdupe.id, lpdupe_of.id)
641 lpdupe.duplicateof = lpdupe_of641 lpdupe.markAsDuplicate(lpdupe_of)
642 trans.commit()642 trans.commit()
643643
644 def importBugs(self, trans, product=None, component=None, status=None):644 def importBugs(self, trans, product=None, component=None, status=None):
645645
=== 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 2009-10-22 18:13:42 +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 @@
1First, visit the +duplicate page on the bug you're looking at:
2
3 >>> print http(r"""
4 ... GET /debian/+source/mozilla-firefox/+bug/2/+duplicate HTTP/1.1
5 ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
6 ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
7 ... """)
8 HTTP/1.1 200 Ok
9 ...
10
11Now, let's go ahead and mark bug 2 as a duplicate of bug 1.
12
13 >>> print http(r"""
14 ... POST /debian/+source/mozilla-firefox/+bug/2/+duplicate HTTP/1.1
15 ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
16 ... Content-Length: 309
17 ... Content-Type: multipart/form-data; boundary=---------------------------17976867087135254391306687757
18 ...
19 ... -----------------------------17976867087135254391306687757
20 ... Content-Disposition: form-data; name="field.duplicateof"
21 ...
22 ... 1
23 ... -----------------------------17976867087135254391306687757
24 ... Content-Disposition: form-data; name="field.actions.change"
25 ...
26 ... Change
27 ... -----------------------------17976867087135254391306687757--
28 ... """)
29 HTTP/1.1 303 See Other
30 ...
31 Content-Length: 0
32 ...
33 Location: http://.../debian/+source/mozilla-firefox/+bug/2
34 ...
35
36When the bug is marked as a duplicate, a notification was generated, to
37tell bug 2's subscribers that their bug is a dupe of bug 1:
38
39 >>> from canonical.launchpad.database import BugNotification
40 >>> BugNotification.select().count()
41 1
42
43 >>> notification = BugNotification.select(orderBy='-id')[0]
44 >>> notification.bug.id
45 2
46 >>> print notification.message.text_contents
47 ** This bug has been marked a duplicate of bug 1
48 ...
490
=== 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-01-04 15:01:34 +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 @@
1When a bug is marked as a duplicate of another bug, the duplicate bug's
2page shows this prominently, and the comment field is accompanied by an extra
3warning that it's a duplicate.
4
5 >>> user_browser.open(
6 ... 'http://launchpad.dev/debian/+source/mozilla-firefox/+bug/2')
7 >>> print find_tag_by_id(
8 ... user_browser.contents, 'duplicate-of').renderContents()
9 bug #1...
10 >>> for message in find_tags_by_class(user_browser.contents, 'message'):
11 ... print extract_text(message)
12 Remember, this bug report is a duplicate of bug #1. Comment here only if...
13
14The "Affects" lines are also not expandable, preventing people from changing
15the bug's status to no effect.
16
17 >>> affects_table = find_tags_by_class(user_browser.contents, 'listing')[0]
18 >>> "toggleFormVisibility('task" in affects_table
19 False
20
21If the bug is no longer marked as a duplicate,
22
23 >>> user_browser.getLink(id="change_duplicate_bug").click()
24 >>> user_browser.url
25 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2/+duplicate'
26 >>> user_browser.getControl(name='field.duplicateof').value = ''
27 >>> user_browser.getControl('Change').click()
28 >>> user_browser.url
29 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2'
30
31the duplicate notifications disappear,
32
33 >>> find_tags_by_class(user_browser.contents, 'message')
34 []
35
36and the "Affects" lines become expandable once more.
37
38 >>> affects_table = find_tags_by_class(user_browser.contents, 'listing')[0]
39 >>> print affects_table
40 <...
41 ...toggleFormVisibility('task...
420
=== 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 2009-08-21 18:40:36 +0000
+++ lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt 1970-01-01 00:00:00 +0000
@@ -1,76 +0,0 @@
1=================================
2Mark as duplicate form validation
3=================================
4
5 >>> bg3 = 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/3'
6 >>> bug_3_dup_url = '%s/+duplicate' % bg3
7 >>> user_browser.open(bug_3_dup_url)
8
9Tests the marking of a bug that is a duplicate of a duplicate.
10
11 >>> user_browser.getControl('Duplicate Of').value = '6'
12 >>> user_browser.getControl('Change').click()
13 >>> print user_browser.contents
14 <...
15 <p class="error message">There is 1 error.</p>
16 ...
17 ...already a duplicate...
18
19Tests the marking of a bug that is a duplicate of itself.
20
21 >>> user_browser.getControl('Duplicate Of').value = '3'
22 >>> user_browser.getControl('Change').click()
23 >>> print user_browser.contents
24 <...
25 <p class="error message">There is 1 error.</p>
26 ...
27 ...can't mark a bug as a duplicate of itself...
28 ...
29
30Tests the marking of a bug that is already marked as duplicate with
31the same value. In this case, nothing should happen, since nothing
32changed.
33
34 >>> user_browser.open(
35 ... 'http://bugs.launchpad.dev/firefox/+bug/6/+duplicate')
36 >>> user_browser.getControl('Duplicate Of').value
37 '5'
38 >>> user_browser.getControl('Change').click()
39 >>> user_browser.url
40 'http://bugs.launchpad.dev/firefox/+bug/6'
41
42Tests the input of data that is not a valid value.
43
44 >>> user_browser.open(
45 ... 'http://bugs.launchpad.dev/firefox/+bug/6/+duplicate')
46 >>> user_browser.getControl('Duplicate Of').value = 'xajskd'
47 >>> user_browser.getControl('Change').click()
48 >>> print user_browser.contents
49 <...
50 ...Not a valid bug number or nickname...
51 ...
52
53Tests using a bug nickname as the duplicate value
54
55 >>> user_browser.open(
56 ... 'http://bugs.launchpad.dev/evolution/+bug/7/+duplicate')
57 >>> user_browser.getControl('Duplicate Of').value = 'blackhole'
58 >>> user_browser.getControl('Change').click()
59 >>> user_browser.url
60 'http://bugs.launchpad.dev/evolution/+bug/7'
61
62Marking a bugX as a duplicate of bugY is not allowed if bugX has dupes.
63
64 >>> user_browser.open(
65 ... 'http://bugs.launchpad.dev/firefox/+bug/5/+duplicate')
66 >>> user_browser.getControl('Duplicate Of').value = '4'
67 >>> user_browser.getControl('Change').click()
68 >>> print user_browser.contents
69 <...
70 ...There are other bugs already...
71 ...
72
73 >>> from canonical.launchpad.database import Bug
74 >>> Bug.get(5).duplicateof is None
75 True
76
770
=== modified file 'lib/lp/bugs/tests/bug.py'
--- lib/lp/bugs/tests/bug.py 2010-06-29 15:48:57 +0000
+++ lib/lp/bugs/tests/bug.py 2010-07-21 12:33:49 +0000
@@ -196,7 +196,8 @@
196 params = CreateBugParams(196 params = CreateBugParams(
197 owner=no_priv, title=title, comment='Something is broken.')197 owner=no_priv, title=title, comment='Something is broken.')
198 bug = target.createBug(params)198 bug = target.createBug(params)
199 bug.duplicateof = duplicateof199 if duplicateof is not None:
200 bug.markAsDuplicate(duplicateof)
200 sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')201 sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
201 if with_message is True:202 if with_message is True:
202 bug.newMessage(203 bug.newMessage(
203204
=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
--- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-06-05 10:41:52 +0000
+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-07-21 12:33:49 +0000
@@ -926,32 +926,6 @@
926 duplicate itself.926 duplicate itself.
927 ...927 ...
928928
929If the specified bug has other bugs marked as a duplicate of it, an
930error message is sent telling you this.
931
932 >>> bug_four = getUtility(IBugSet).get(4)
933 >>> bug_four.duplicateof.id
934 2
935
936 >>> from canonical.launchpad.ftests import syncUpdate
937 >>> syncUpdate(bug_four)
938 >>> submit_commands(bug_four.duplicateof, 'duplicate 1')
939 >>> bug_four.duplicateof.id
940 2
941
942 >>> print_latest_email()
943 Subject: Submit Request Failure
944 To: test@canonical.com
945 <BLANKLINE>
946 ...
947 Failing command:
948 duplicate 1
949 ...
950 There are other bugs already marked as duplicates of Bug 2.
951 These bugs should be changed to be duplicates of another bug
952 if you are certain you would like to perform this change.
953 ...
954
955929
956=== cve $cve ===930=== cve $cve ===
957931
@@ -1916,7 +1890,7 @@
1916 ... Subject: A bug with no affects1890 ... Subject: A bug with no affects
1917 ...1891 ...
1918 ... private yes1892 ... private yes
1919 ... duplicate 11893 ... unsubscribe
1920 ... cve 1999-89791894 ... cve 1999-8979
1921 ... """1895 ... """
19221896
19231897
=== modified file 'lib/lp/bugs/tests/bugtarget-bugcount.txt'
--- lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-06-01 10:12:15 +0000
+++ lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-07-21 12:33:49 +0000
@@ -129,7 +129,7 @@
129 ... bugtarget, "Another Test Bug", status=BugTaskStatus.NEW)129 ... bugtarget, "Another Test Bug", status=BugTaskStatus.NEW)
130 >>> Store.of(another_bug).flush()130 >>> Store.of(another_bug).flush()
131 >>> old_counts = bugtarget.getBugCounts(None)131 >>> old_counts = bugtarget.getBugCounts(None)
132 >>> another_bug.duplicateof = bug132 >>> another_bug.markAsDuplicate(bug)
133 >>> syncUpdate(another_bug)133 >>> syncUpdate(another_bug)
134 >>> new_counts = bugtarget.getBugCounts(None)134 >>> new_counts = bugtarget.getBugCounts(None)
135 >>> print_count_difference(135 >>> print_count_difference(
136136
=== modified file 'lib/lp/bugs/tests/test_bugchanges.py'
--- lib/lp/bugs/tests/test_bugchanges.py 2010-06-23 01:15:11 +0000
+++ lib/lp/bugs/tests/test_bugchanges.py 2010-07-21 12:33:49 +0000
@@ -90,7 +90,10 @@
90 :return: The value of `attribute` before modification.90 :return: The value of `attribute` before modification.
91 """91 """
92 obj_before_modification = Snapshot(obj, providing=providedBy(obj))92 obj_before_modification = Snapshot(obj, providing=providedBy(obj))
93 setattr(obj, attribute, new_value)93 if attribute == 'duplicateof':
94 obj.markAsDuplicate(new_value)
95 else:
96 setattr(obj, attribute, new_value)
94 notify(ObjectModifiedEvent(97 notify(ObjectModifiedEvent(
95 obj, obj_before_modification, [attribute], self.user))98 obj, obj_before_modification, [attribute], self.user))
9699
@@ -1304,7 +1307,7 @@
1304 duplicate_bug = self.factory.makeBug()1307 duplicate_bug = self.factory.makeBug()
1305 duplicate_bug_recipients = duplicate_bug.getBugNotificationRecipients(1308 duplicate_bug_recipients = duplicate_bug.getBugNotificationRecipients(
1306 level=BugNotificationLevel.METADATA).getRecipients()1309 level=BugNotificationLevel.METADATA).getRecipients()
1307 duplicate_bug.duplicateof = self.bug1310 duplicate_bug.markAsDuplicate(self.bug)
1308 self.saveOldChanges(duplicate_bug)1311 self.saveOldChanges(duplicate_bug)
1309 self.changeAttribute(duplicate_bug, 'duplicateof', None)1312 self.changeAttribute(duplicate_bug, 'duplicateof', None)
13101313
@@ -1335,7 +1338,7 @@
1335 bug_two = self.factory.makeBug()1338 bug_two = self.factory.makeBug()
1336 bug_recipients = self.bug.getBugNotificationRecipients(1339 bug_recipients = self.bug.getBugNotificationRecipients(
1337 level=BugNotificationLevel.METADATA).getRecipients()1340 level=BugNotificationLevel.METADATA).getRecipients()
1338 self.bug.duplicateof = bug_one1341 self.bug.markAsDuplicate(bug_one)
1339 self.saveOldChanges()1342 self.saveOldChanges()
1340 self.changeAttribute(self.bug, 'duplicateof', bug_two)1343 self.changeAttribute(self.bug, 'duplicateof', bug_two)
13411344
13421345
=== modified file 'lib/lp/bugs/tests/test_bugnotification.py'
--- lib/lp/bugs/tests/test_bugnotification.py 2010-06-03 13:53:29 +0000
+++ lib/lp/bugs/tests/test_bugnotification.py 2010-07-21 12:33:49 +0000
@@ -155,7 +155,7 @@
155 user='test@canonical.com')155 user='test@canonical.com')
156 self.bug = self.factory.makeBug()156 self.bug = self.factory.makeBug()
157 self.dupe_bug = self.factory.makeBug()157 self.dupe_bug = self.factory.makeBug()
158 self.dupe_bug.duplicateof = self.bug158 self.dupe_bug.markAsDuplicate(self.bug)
159 self.dupe_subscribers = set(159 self.dupe_subscribers = set(
160 self.dupe_bug.getDirectSubscribers() +160 self.dupe_bug.getDirectSubscribers() +
161 self.dupe_bug.getIndirectSubscribers())161 self.dupe_bug.getIndirectSubscribers())
162162
=== 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-07-21 12:33:49 +0000
@@ -0,0 +1,102 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for bug duplicate validation."""
5
6from textwrap import dedent
7import unittest
8
9from zope.security.interfaces import ForbiddenAttribute
10
11from canonical.testing import DatabaseFunctionalLayer
12
13from lp.bugs.interfaces.bug import InvalidDuplicateValue
14from lp.testing import TestCaseWithFactory
15
16
17class TestDuplicateAttributes(TestCaseWithFactory):
18 """Test bug attributes related to duplicate handling."""
19
20 layer = DatabaseFunctionalLayer
21
22 def setUp(self):
23 super(TestDuplicateAttributes, self).setUp(user='test@canonical.com')
24
25 def setDuplicateofDirectly(self, bug, duplicateof):
26 """Helper method to set duplicateof directly."""
27 bug.duplicateof = duplicateof
28
29 def test_duplicateof_readonly(self):
30 # Test that no one can set duplicateof directly.
31 bug = self.factory.makeBug()
32 dupe_bug = self.factory.makeBug()
33 self.assertRaises(
34 ForbiddenAttribute, self.setDuplicateofDirectly, bug, dupe_bug)
35
36
37class TestMarkDuplicateValidation(TestCaseWithFactory):
38 """Test for validation around marking bug duplicates."""
39
40 layer = DatabaseFunctionalLayer
41
42 def setUp(self):
43 super(TestMarkDuplicateValidation, self).setUp(
44 user='test@canonical.com')
45 self.bug = self.factory.makeBug()
46 self.dupe_bug = self.factory.makeBug()
47 self.dupe_bug.markAsDuplicate(self.bug)
48 self.possible_dupe = self.factory.makeBug()
49
50 def assertDuplicateError(self, bug, duplicateof, msg):
51 try:
52 bug.markAsDuplicate(duplicateof)
53 except InvalidDuplicateValue, err:
54 self.assertEqual(str(err), msg)
55
56 def test_error_on_duplicate_to_duplicate(self):
57 # Test that a bug cannot be marked a duplicate of
58 # a bug that is already itself a duplicate.
59 msg = dedent(u"""
60 Bug %s is already a duplicate of bug %s. You
61 can only mark a bug report as duplicate of one that
62 isn't a duplicate itself.
63 """ % (
64 self.dupe_bug.id, self.dupe_bug.duplicateof.id))
65 self.assertDuplicateError(
66 self.possible_dupe, self.dupe_bug, msg)
67
68 def test_error_duplicate_to_itself(self):
69 # Test that a bug cannot be marked its own duplicate
70 msg = dedent(u"""
71 You can't mark a bug as a duplicate of itself.""")
72 self.assertDuplicateError(self.bug, self.bug, msg)
73
74
75class TestMoveDuplicates(TestCaseWithFactory):
76 """Test duplicates are moved when master bug is marked a duplicate."""
77
78 layer = DatabaseFunctionalLayer
79
80 def setUp(self):
81 super(TestMoveDuplicates, self).setUp(user='test@canonical.com')
82
83 def test_duplicates_are_moved(self):
84 # Confirm that a bug with two duplicates can be marked
85 # a duplicate of a new bug and that the duplicates will
86 # be re-marked as duplicates of the new bug, too.
87 bug = self.factory.makeBug()
88 dupe_one = self.factory.makeBug()
89 dupe_two = self.factory.makeBug()
90 dupe_one.markAsDuplicate(bug)
91 dupe_two.markAsDuplicate(bug)
92 self.assertEqual(dupe_one.duplicateof, bug)
93 self.assertEqual(dupe_two.duplicateof, bug)
94 new_bug = self.factory.makeBug()
95 bug.markAsDuplicate(new_bug)
96 self.assertEqual(bug.duplicateof, new_bug)
97 self.assertEqual(dupe_one.duplicateof, new_bug)
98 self.assertEqual(dupe_two.duplicateof, new_bug)
99
100
101def test_suite():
102 return unittest.TestLoader().loadTestsFromName(__name__)
0103
=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
--- lib/lp/registry/browser/tests/person-views.txt 2010-07-16 16:14:39 +0000
+++ lib/lp/registry/browser/tests/person-views.txt 2010-07-21 12:33:49 +0000
@@ -546,7 +546,7 @@
546546
547But duplicate bugs are never displayed.547But duplicate bugs are never displayed.
548548
549 >>> another_bug.duplicateof = bug549 >>> another_bug.markAsDuplicate(bug)
550550
551 # Create a new view because we're testing some cached properties.551 # Create a new view because we're testing some cached properties.
552552