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
1=== modified file 'lib/canonical/launchpad/fields/__init__.py'
2--- lib/canonical/launchpad/fields/__init__.py 2010-02-17 11:19:42 +0000
3+++ lib/canonical/launchpad/fields/__init__.py 2010-07-21 12:33:49 +0000
4@@ -302,12 +302,8 @@
5 bug isn't a duplicate of itself, otherwise
6 return False.
7 """
8- from lp.bugs.interfaces.bug import IBugSet
9- bugset = getUtility(IBugSet)
10 current_bug = self.context
11 dup_target = value
12- current_bug_has_dup_refs = bool(bugset.searchAsUser(
13- user=getUtility(ILaunchBag).user, duplicateof=current_bug))
14 if current_bug == dup_target:
15 raise LaunchpadValidationError(_(dedent("""
16 You can't mark a bug as a duplicate of itself.""")))
17@@ -318,13 +314,6 @@
18 isn't a duplicate itself.
19 """), mapping={'dup': dup_target.id,
20 'orig': dup_target.duplicateof.id}))
21- elif current_bug_has_dup_refs:
22- raise LaunchpadValidationError(_(dedent("""
23- There are other bugs already marked as duplicates of
24- Bug ${current}. These bugs should be changed to be
25- duplicates of another bug if you are certain you would
26- like to perform this change."""),
27- mapping={'current': current_bug.id}))
28 else:
29 return True
30
31
32=== modified file 'lib/canonical/launchpad/mail/commands.py'
33--- lib/canonical/launchpad/mail/commands.py 2010-05-25 03:11:16 +0000
34+++ lib/canonical/launchpad/mail/commands.py 2010-07-21 12:33:49 +0000
35@@ -382,30 +382,40 @@
36 return {'title': self.string_args[0]}
37
38
39-class DuplicateEmailCommand(EditEmailCommand):
40+class DuplicateEmailCommand(EmailCommand):
41 """Marks a bug as a duplicate of another bug."""
42
43 implements(IBugEditEmailCommand)
44 _numberOfArguments = 1
45
46- def convertArguments(self, context):
47- """See EmailCommand."""
48+ def execute(self, context, current_event):
49+ """See IEmailCommand."""
50+ self._ensureNumberOfArguments()
51 [bug_id] = self.string_args
52- if bug_id == 'no':
53+
54+ if bug_id != 'no':
55+ try:
56+ bug = getUtility(IBugSet).getByNameOrID(bug_id)
57+ except NotFoundError:
58+ raise EmailProcessingError(
59+ get_error_message('no-such-bug.txt', bug_id=bug_id))
60+ else:
61 # 'no' is a special value for unmarking a bug as a duplicate.
62- return {'duplicateof': None}
63- try:
64- bug = getUtility(IBugSet).getByNameOrID(bug_id)
65- except NotFoundError:
66- raise EmailProcessingError(
67- get_error_message('no-such-bug.txt', bug_id=bug_id))
68+ bug = None
69+
70 duplicate_field = IBug['duplicateof'].bind(context)
71 try:
72 duplicate_field.validate(bug)
73 except ValidationError, error:
74 raise EmailProcessingError(error.doc())
75
76- return {'duplicateof': bug}
77+ context_snapshot = Snapshot(
78+ context, providing=providedBy(context))
79+ context.markAsDuplicate(bug)
80+ current_event = ObjectModifiedEvent(
81+ context, context_snapshot, 'duplicateof')
82+ notify(current_event)
83+ return bug, current_event
84
85
86 class CVEEmailCommand(EmailCommand):
87
88=== modified file 'lib/lp/bugs/browser/bug.py'
89--- lib/lp/bugs/browser/bug.py 2010-07-15 08:38:19 +0000
90+++ lib/lp/bugs/browser/bug.py 2010-07-21 12:33:49 +0000
91@@ -49,6 +49,7 @@
92 from canonical.cachedproperty import cachedproperty
93
94 from canonical.launchpad import _
95+from canonical.launchpad.fields import DuplicateBug
96 from canonical.launchpad.webapp.interfaces import ILaunchBag, NotFoundError
97 from lp.bugs.interfaces.bug import IBug, IBugSet
98 from lp.bugs.interfaces.bugattachment import BugAttachmentType
99@@ -658,9 +659,35 @@
100 field_names = ['duplicateof']
101 label = "Mark bug report as a duplicate"
102
103+ def setUpFields(self):
104+ """Make the readonly version of duplicateof available."""
105+ super(BugMarkAsDuplicateView, self).setUpFields()
106+
107+ duplicateof_field = DuplicateBug(
108+ __name__='duplicateof', title=_('Duplicate Of'), required=False)
109+
110+ self.form_fields = self.form_fields.omit('duplicateof')
111+ self.form_fields = formlib.form.Fields(duplicateof_field)
112+
113+ @property
114+ def initial_values(self):
115+ """See `LaunchpadFormView.`"""
116+ return {'duplicateof': self.context.bug.duplicateof}
117+
118 @action('Change', name='change')
119 def change_action(self, action, data):
120 """Update the bug."""
121+ data = dict(data)
122+ # We handle duplicate changes by hand instead of leaving it to
123+ # the usual machinery because we must use bug.markAsDuplicate().
124+ bug = self.context.bug
125+ bug_before_modification = Snapshot(
126+ bug, providing=providedBy(bug))
127+ duplicateof = data.pop('duplicateof')
128+ bug.markAsDuplicate(duplicateof)
129+ notify(
130+ ObjectModifiedEvent(bug, bug_before_modification, 'duplicateof'))
131+ # Apply other changes.
132 self.updateBugFromData(data)
133
134
135
136=== modified file 'lib/lp/bugs/browser/tests/bug-subscription-views.txt'
137--- lib/lp/bugs/browser/tests/bug-subscription-views.txt 2009-11-18 11:53:50 +0000
138+++ lib/lp/bugs/browser/tests/bug-subscription-views.txt 2010-07-21 12:33:49 +0000
139@@ -30,7 +30,7 @@
140 {"name12": "subscriber-12"}
141
142 >>> login('foo.bar@canonical.com')
143- >>> bug_15.duplicateof = bug_13
144+ >>> bug_15.markAsDuplicate(bug_13)
145 >>> bug_13 = getUtility(IBugSet).get(13)
146
147 >>> subscriber_ids_view = create_initialized_view(
148
149=== modified file 'lib/lp/bugs/browser/tests/bug-views.txt'
150--- lib/lp/bugs/browser/tests/bug-views.txt 2010-06-25 20:37:53 +0000
151+++ lib/lp/bugs/browser/tests/bug-views.txt 2010-07-21 12:33:49 +0000
152@@ -249,7 +249,8 @@
153 and see how the returned link changes.
154
155 >>> bug_two = bugset.get(2)
156- >>> bug_two.duplicateof = 5
157+ >>> bug_five = bugset.get(5)
158+ >>> bug_two.markAsDuplicate(bug_five)
159
160 >>> bug_page_view = getMultiAdapter(
161 ... (bug_five_in_firefox.bug, request), name="+portlet-duplicates")
162@@ -380,7 +381,7 @@
163
164 >>> from canonical.launchpad.ftests import syncUpdate
165
166- >>> bug_two.duplicateof = bug_three
167+ >>> bug_two.markAsDuplicate(bug_three)
168 >>> syncUpdate(bug_two)
169
170 >>> bug_menu.subscription().text
171@@ -391,7 +392,7 @@
172 the current user is a member. So, for Foo Bar, bug #3 has a simple
173 Subscribe link initially.
174
175- >>> bug_two.duplicateof = None
176+ >>> bug_two.markAsDuplicate(None)
177 >>> syncUpdate(bug_two)
178
179 >>> login("foo.bar@canonical.com")
180@@ -406,7 +407,7 @@
181 >>> bug_two.subscribe(ubuntu_team, ubuntu_team)
182 <BugSubscription...>
183
184- >>> bug_two.duplicateof = bug_three
185+ >>> bug_two.markAsDuplicate(bug_three)
186 >>> syncUpdate(bug_two)
187
188 >>> bug_menu.subscription().text
189
190=== modified file 'lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt'
191--- lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt 2009-11-27 14:05:09 +0000
192+++ lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt 2010-07-21 12:33:49 +0000
193@@ -125,7 +125,7 @@
194
195 Bugs that are duplicate of other bugs aren't included in the count.
196
197- >>> another_bug.duplicateof = bug
198+ >>> another_bug.markAsDuplicate(bug)
199 >>> syncUpdate(another_bug)
200
201 >>> view.bugs_fixed_elsewhere_count
202
203=== modified file 'lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt'
204--- lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt 2009-06-12 16:36:02 +0000
205+++ lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt 2010-07-21 12:33:49 +0000
206@@ -80,7 +80,7 @@
207 Bugs that are duplicates of other bugs will be omitted from the list as
208 well.
209
210- >>> bug_b.duplicateof = bug_c
211+ >>> bug_b.markAsDuplicate(bug_c)
212 >>> syncUpdate(bug_b)
213
214 >>> for bugtask in portlet_view.getMostRecentlyUpdatedBugTasks()[:2]:
215
216=== modified file 'lib/lp/bugs/configure.zcml'
217--- lib/lp/bugs/configure.zcml 2010-07-12 16:32:31 +0000
218+++ lib/lp/bugs/configure.zcml 2010-07-21 12:33:49 +0000
219@@ -649,7 +649,6 @@
220 getBugTasksByPackageName
221 users_affected_count
222 users_unaffected_count
223- readonly_duplicateof
224 users_affected
225 users_unaffected
226 users_affected_count_with_dupes
227@@ -700,7 +699,6 @@
228 date_last_updated
229 date_made_private
230 datecreated
231- duplicateof
232 hits
233 hitstimestamp
234 name
235
236=== modified file 'lib/lp/bugs/doc/bug.txt'
237--- lib/lp/bugs/doc/bug.txt 2010-06-29 15:48:57 +0000
238+++ lib/lp/bugs/doc/bug.txt 2010-07-21 12:33:49 +0000
239@@ -184,18 +184,22 @@
240
241 >>> login("foo.bar@canonical.com")
242
243+ >>> from lp.registry.interfaces.product import IProductSet
244+ >>> productset = getUtility(IProductSet)
245+ >>> firefox = productset.get(4)
246 >>> firefox_test_bug = bugset.get(3)
247- >>> firefox_test_bug.duplicateof = firefox_crashes.id
248+ >>> firefox_master_bug = factory.makeBug(product=firefox)
249+ >>> firefox_test_bug.markAsDuplicate(firefox_master_bug)
250 >>> flush_database_updates()
251
252 >>> dups_of_bug_six = bugset.searchAsUser(
253- ... duplicateof=firefox_crashes, user=current_user())
254+ ... duplicateof=firefox_master_bug, user=current_user())
255 >>> print dups_of_bug_six.count()
256 1
257 >>> dups_of_bug_six[0].id
258 3
259
260- >>> firefox_test_bug.duplicateof = None
261+ >>> firefox_test_bug.markAsDuplicate(None)
262 >>> flush_database_updates()
263 >>> dups_of_bug_six = bugset.searchAsUser(
264 ... duplicateof=firefox_crashes, user=current_user())
265@@ -426,11 +430,6 @@
266
267 When a public bug is filed:
268
269- >>> from lp.registry.interfaces.product import IProductSet
270- >>> productset = getUtility(IProductSet)
271- >>> bugset = getUtility(IBugSet)
272-
273- >>> firefox = productset.get(4)
274 >>> params = CreateBugParams(
275 ... title="test firefox bug", comment="blah blah blah", owner=foobar)
276 >>> params.setBugTarget(product=firefox)
277@@ -765,7 +764,7 @@
278
279 >>> print firefox_bug.duplicateof
280 None
281- >>> firefox_bug.duplicateof = firefox_crashes
282+ >>> firefox_bug.markAsDuplicate(firefox_master_bug)
283
284 >>> bug_duplicateof_changed = ObjectModifiedEvent(
285 ... firefox_bug, bug_before_modification, ["duplicateof"])
286@@ -1274,7 +1273,7 @@
287
288 >>> dupe_affected_user = factory.makePerson(name='sheila-shakespeare')
289 >>> dupe_one = factory.makeBug(owner=dupe_affected_user)
290- >>> dupe_one.duplicateof = test_bug
291+ >>> dupe_one.markAsDuplicate(test_bug)
292 >>> test_bug.users_affected_count_with_dupes
293 3
294
295@@ -1308,7 +1307,7 @@
296
297 >>> dupe_affected_other_user = factory.makePerson(name='napoleon-bonaparte')
298 >>> dupe_three = factory.makeBug(owner=dupe_affected_other_user)
299- >>> dupe_three.duplicateof = test_bug
300+ >>> dupe_three.markAsDuplicate(test_bug)
301 >>> test_bug.users_affected_count_with_dupes
302 4
303
304@@ -1317,7 +1316,7 @@
305 user_affected_count still only increments by 1 for that user.
306
307 >>> dupe_two = factory.makeBug(owner=dupe_affected_user)
308- >>> dupe_two.duplicateof = test_bug
309+ >>> dupe_two.markAsDuplicate(test_bug)
310 >>> test_bug.users_affected_count_with_dupes
311 4
312
313@@ -1471,7 +1470,7 @@
314
315 >>> new_bug_3 = bugs[3]
316 >>> new_bug_4 = bugs[4]
317- >>> new_bug_3.duplicateof = new_bug_4
318+ >>> new_bug_3.markAsDuplicate(new_bug_4)
319 >>> matching_bugs = getUtility(IBugSet).getDistinctBugsForBugTasks(
320 ... bug_tasks, user=no_priv)
321
322@@ -1599,7 +1598,7 @@
323 >>> dupe = factory.makeBug()
324 >>> subscription = dupe.subscribe(person, person)
325
326- >>> dupe.duplicateof = bug
327+ >>> dupe.markAsDuplicate(bug)
328
329 # Re-fetch the bug so that the fact that it's a duplicate definitely
330 # registers.
331
332=== modified file 'lib/lp/bugs/doc/bugactivity.txt'
333--- lib/lp/bugs/doc/bugactivity.txt 2009-06-12 16:36:02 +0000
334+++ lib/lp/bugs/doc/bugactivity.txt 2010-07-21 12:33:49 +0000
335@@ -123,7 +123,8 @@
336 ... "id", "title", "description", "name",
337 ... "private", "duplicateof", "security_related"]
338 >>> old_bug = Snapshot(bug, providing=IBug)
339- >>> bug.duplicateof = 1
340+ >>> latest_bug = factory.makeBug()
341+ >>> bug.markAsDuplicate(latest_bug)
342 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
343 >>> notify(bug_edited)
344 >>> latest_activity = bug.activity[-1]
345@@ -131,7 +132,7 @@
346 u'marked as duplicate'
347 >>> latest_activity.oldvalue is None
348 True
349- >>> latest_activity.newvalue == u'1'
350+ >>> latest_activity.newvalue == unicode(latest_bug.id)
351 True
352
353
354@@ -141,15 +142,16 @@
355 ... "id", "title", "description", "name", "private", "duplicateof",
356 ... "security_related"]
357 >>> old_bug = Snapshot(bug, providing=IBug)
358- >>> bug.duplicateof = 2
359+ >>> another_bug = factory.makeBug()
360+ >>> bug.markAsDuplicate(another_bug)
361 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
362 >>> notify(bug_edited)
363 >>> latest_activity = bug.activity[-1]
364 >>> latest_activity.whatchanged
365 u'changed duplicate marker'
366- >>> latest_activity.oldvalue == u'1'
367+ >>> latest_activity.oldvalue == unicode(latest_bug.id)
368 True
369- >>> latest_activity.newvalue == u'2'
370+ >>> latest_activity.newvalue == unicode(another_bug.id)
371 True
372
373
374@@ -159,13 +161,13 @@
375 ... "id", "title", "description", "name", "private", "duplicateof",
376 ... "security_related"]
377 >>> old_bug = Snapshot(bug, providing=IBug)
378- >>> bug.duplicateof = None
379+ >>> bug.markAsDuplicate(None)
380 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
381 >>> notify(bug_edited)
382 >>> latest_activity = bug.activity[-1]
383 >>> latest_activity.whatchanged
384 u'removed duplicate marker'
385- >>> latest_activity.oldvalue == u'2'
386+ >>> latest_activity.oldvalue == unicode(another_bug.id)
387 True
388 >>> latest_activity.newvalue is None
389 True
390
391=== modified file 'lib/lp/bugs/doc/bugnotification-sending.txt'
392--- lib/lp/bugs/doc/bugnotification-sending.txt 2010-06-23 21:24:13 +0000
393+++ lib/lp/bugs/doc/bugnotification-sending.txt 2010-07-21 12:33:49 +0000
394@@ -760,13 +760,14 @@
395 >>> switch_db_to_launchpad()
396 >>> new_bug = ubuntu.createBug(params)
397 >>> switch_db_to_bugnotification()
398-
399 >>> flush_notifications()
400
401 If a bug is a duplicate of another bug, a marker gets inserted at the
402 top of the email:
403
404- >>> new_bug.duplicateof = bug_one
405+ >>> switch_db_to_launchpad()
406+ >>> new_bug.markAsDuplicate(bug_one)
407+ >>> switch_db_to_bugnotification()
408 >>> comment = getUtility(IMessageSet).fromText(
409 ... 'subject', 'a comment.', sample_person,
410 ... datecreated=ten_minutes_ago)
411
412=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
413--- lib/lp/bugs/doc/bugsubscription.txt 2010-05-25 15:43:59 +0000
414+++ lib/lp/bugs/doc/bugsubscription.txt 2010-07-21 12:33:49 +0000
415@@ -341,7 +341,7 @@
416 Stuart Bishop
417 Ubuntu Team
418
419- >>> linux_source_bug_dupe.duplicateof = linux_source_bug
420+ >>> linux_source_bug_dupe.markAsDuplicate(linux_source_bug)
421 >>> linux_source_bug_dupe.syncUpdate()
422
423 >>> print_displayname(linux_source_bug.getIndirectSubscribers())
424@@ -868,7 +868,7 @@
425 ... comment="one more dupe test description",
426 ... owner=keybuk)
427 >>> dupe_ff_bug = firefox.createBug(params)
428- >>> dupe_ff_bug.duplicateof = ff_bug
429+ >>> dupe_ff_bug.markAsDuplicate(ff_bug)
430 >>> dupe_ff_bug.syncUpdate()
431 >>> dupe_ff_bug.subscribe(foobar, lifeless)
432 <BugSubscription at ...>
433
434=== modified file 'lib/lp/bugs/doc/bugtask-package-bugcounts.txt'
435--- lib/lp/bugs/doc/bugtask-package-bugcounts.txt 2010-04-15 21:13:29 +0000
436+++ lib/lp/bugs/doc/bugtask-package-bugcounts.txt 2010-07-21 12:33:49 +0000
437@@ -170,7 +170,7 @@
438 Duplicates bugs are omitted from the counts.
439
440 >>> from canonical.launchpad.interfaces import IBugSet
441- >>> bug.duplicateof = getUtility(IBugSet).get(1)
442+ >>> bug.markAsDuplicate(getUtility(IBugSet).get(1))
443 >>> syncUpdate(bug)
444 >>> package_counts = getUtility(IBugTaskSet).getBugCountsForPackages(
445 ... user=foo_bar, packages=packages)
446
447=== modified file 'lib/lp/bugs/doc/bugtask-search.txt'
448--- lib/lp/bugs/doc/bugtask-search.txt 2010-05-28 14:53:57 +0000
449+++ lib/lp/bugs/doc/bugtask-search.txt 2010-07-21 12:33:49 +0000
450@@ -1037,7 +1037,7 @@
451
452 >>> bug_nine = getUtility(IBugSet).get(9)
453 >>> bug_ten = getUtility(IBugSet).get(10)
454- >>> bug_ten.duplicateof = bug_nine
455+ >>> bug_ten.markAsDuplicate(bug_nine)
456 >>> flush_database_updates()
457
458 Searching again reveals bug #9 at the top of the list, since it now has a duplicate.
459
460=== modified file 'lib/lp/bugs/doc/bugzilla-import.txt'
461--- lib/lp/bugs/doc/bugzilla-import.txt 2010-04-08 14:34:58 +0000
462+++ lib/lp/bugs/doc/bugzilla-import.txt 2010-07-21 12:33:49 +0000
463@@ -87,8 +87,7 @@
464
465 >>> duplicates = [
466 ... (1, 2),
467- ... (2, 3),
468- ... (2, 4),
469+ ... (3, 4),
470 ... ]
471
472 >>> class FakeBackend:
473@@ -536,9 +535,9 @@
474 None
475 >>> bug2.duplicateof == bug1
476 True
477- >>> bug3.duplicateof == bug2
478+ >>> bug3.duplicateof == None
479 True
480- >>> bug4.duplicateof == bug2
481+ >>> bug4.duplicateof == bug3
482 True
483
484
485
486=== modified file 'lib/lp/bugs/doc/checkwatches.txt'
487--- lib/lp/bugs/doc/checkwatches.txt 2010-04-23 11:19:49 +0000
488+++ lib/lp/bugs/doc/checkwatches.txt 2010-07-21 12:33:49 +0000
489@@ -490,8 +490,8 @@
490 another bug, comments won't be synced and the bug won't be linked back
491 to the remote bug.
492
493+ >>> LaunchpadZopelessLayer.switchDbUser('launchpad')
494 >>> bug_15 = getUtility(IBugSet).get(15)
495- >>> bug_watch.bug.duplicateof = bug_15
496- >>> transaction.commit()
497+ >>> bug_watch.bug.markAsDuplicate(bug_15)
498
499 >>> updater.updateBugWatches(remote_system, [bug_watch], now=nowish)
500
501=== modified file 'lib/lp/bugs/doc/malone-karma.txt'
502--- lib/lp/bugs/doc/malone-karma.txt 2010-02-16 20:36:48 +0000
503+++ lib/lp/bugs/doc/malone-karma.txt 2010-07-21 12:33:49 +0000
504@@ -204,7 +204,7 @@
505
506 >>> bug_one = getUtility(IBugSet).get(1)
507 >>> old_bug = Snapshot(bug, providing=IBug)
508- >>> bug.duplicateof = bug_one
509+ >>> bug.markAsDuplicate(bug_one)
510
511 (Notice how changing a bug with multiple bugtasks will assign karma to you
512 once for each bugtask. This is so because we consider changes in a bug to
513
514=== modified file 'lib/lp/bugs/interfaces/bug.py'
515--- lib/lp/bugs/interfaces/bug.py 2010-06-30 21:19:36 +0000
516+++ lib/lp/bugs/interfaces/bug.py 2010-07-21 12:33:49 +0000
517@@ -183,8 +183,7 @@
518 ownerID = Int(title=_('Owner'), required=True, readonly=True)
519 owner = exported(
520 Reference(IPerson, title=_("The owner's IPerson"), readonly=True))
521- duplicateof = DuplicateBug(title=_('Duplicate Of'), required=False)
522- readonly_duplicateof = exported(
523+ duplicateof = exported(
524 DuplicateBug(title=_('Duplicate Of'), required=False, readonly=True),
525 exported_as='duplicate_of')
526 # This is redefined from IPrivacy.private because the attribute is
527@@ -754,8 +753,8 @@
528 def markUserAffected(user, affected=True):
529 """Mark :user: as affected by this bug."""
530
531- @mutator_for(readonly_duplicateof)
532- @operation_parameters(duplicate_of=copy_field(readonly_duplicateof))
533+ @mutator_for(duplicateof)
534+ @operation_parameters(duplicate_of=copy_field(duplicateof))
535 @export_write_operation()
536 def markAsDuplicate(duplicate_of):
537 """Mark this bug as a duplicate of another."""
538
539=== modified file 'lib/lp/bugs/model/bug.py'
540--- lib/lp/bugs/model/bug.py 2010-07-19 12:08:05 +0000
541+++ lib/lp/bugs/model/bug.py 2010-07-21 12:33:49 +0000
542@@ -1493,11 +1493,6 @@
543
544 self.updateHeat()
545
546- @property
547- def readonly_duplicateof(self):
548- """See `IBug`."""
549- return self.duplicateof
550-
551 def markAsDuplicate(self, duplicate_of):
552 """See `IBug`."""
553 field = DuplicateBug()
554@@ -1506,6 +1501,9 @@
555 try:
556 if duplicate_of is not None:
557 field._validate(duplicate_of)
558+ if self.duplicates:
559+ for duplicate in self.duplicates:
560+ duplicate.markAsDuplicate(duplicate_of)
561 self.duplicateof = duplicate_of
562 except LaunchpadValidationError, validation_error:
563 raise InvalidDuplicateValue(validation_error)
564
565=== modified file 'lib/lp/bugs/scripts/bugimport.py'
566--- lib/lp/bugs/scripts/bugimport.py 2010-04-14 12:55:44 +0000
567+++ lib/lp/bugs/scripts/bugimport.py 2010-07-21 12:33:49 +0000
568@@ -466,7 +466,7 @@
569 self.logger.info(
570 'Marking bug %d as duplicate of bug %d',
571 other_bug.id, bug.id)
572- other_bug.duplicateof = bug
573+ other_bug.markAsDuplicate(bug)
574 del self.pending_duplicates[bug_id]
575 # Process this bug as a duplicate
576 if duplicateof is not None:
577@@ -478,7 +478,7 @@
578 self.logger.info(
579 'Marking bug %d as duplicate of bug %d',
580 bug.id, other_bug.id)
581- bug.duplicateof = other_bug
582+ bug.markAsDuplicate(other_bug)
583 else:
584 self.pending_duplicates.setdefault(
585 duplicateof, []).append(bug.id)
586
587=== modified file 'lib/lp/bugs/scripts/bugzilla.py'
588--- lib/lp/bugs/scripts/bugzilla.py 2010-05-27 13:51:06 +0000
589+++ lib/lp/bugs/scripts/bugzilla.py 2010-07-21 12:33:49 +0000
590@@ -638,7 +638,7 @@
591 lpdupe.duplicateof is None):
592 logger.info('Marking %d as a duplicate of %d',
593 lpdupe.id, lpdupe_of.id)
594- lpdupe.duplicateof = lpdupe_of
595+ lpdupe.markAsDuplicate(lpdupe_of)
596 trans.commit()
597
598 def importBugs(self, trans, product=None, component=None, status=None):
599
600=== removed directory 'lib/lp/bugs/stories/duplicate-bug-handling'
601=== removed file 'lib/lp/bugs/stories/duplicate-bug-handling/10-mark-bug-as-duplicate.txt'
602--- lib/lp/bugs/stories/duplicate-bug-handling/10-mark-bug-as-duplicate.txt 2009-10-22 18:13:42 +0000
603+++ lib/lp/bugs/stories/duplicate-bug-handling/10-mark-bug-as-duplicate.txt 1970-01-01 00:00:00 +0000
604@@ -1,48 +0,0 @@
605-First, visit the +duplicate page on the bug you're looking at:
606-
607- >>> print http(r"""
608- ... GET /debian/+source/mozilla-firefox/+bug/2/+duplicate HTTP/1.1
609- ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
610- ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
611- ... """)
612- HTTP/1.1 200 Ok
613- ...
614-
615-Now, let's go ahead and mark bug 2 as a duplicate of bug 1.
616-
617- >>> print http(r"""
618- ... POST /debian/+source/mozilla-firefox/+bug/2/+duplicate HTTP/1.1
619- ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
620- ... Content-Length: 309
621- ... Content-Type: multipart/form-data; boundary=---------------------------17976867087135254391306687757
622- ...
623- ... -----------------------------17976867087135254391306687757
624- ... Content-Disposition: form-data; name="field.duplicateof"
625- ...
626- ... 1
627- ... -----------------------------17976867087135254391306687757
628- ... Content-Disposition: form-data; name="field.actions.change"
629- ...
630- ... Change
631- ... -----------------------------17976867087135254391306687757--
632- ... """)
633- HTTP/1.1 303 See Other
634- ...
635- Content-Length: 0
636- ...
637- Location: http://.../debian/+source/mozilla-firefox/+bug/2
638- ...
639-
640-When the bug is marked as a duplicate, a notification was generated, to
641-tell bug 2's subscribers that their bug is a dupe of bug 1:
642-
643- >>> from canonical.launchpad.database import BugNotification
644- >>> BugNotification.select().count()
645- 1
646-
647- >>> notification = BugNotification.select(orderBy='-id')[0]
648- >>> notification.bug.id
649- 2
650- >>> print notification.message.text_contents
651- ** This bug has been marked a duplicate of bug 1
652- ...
653
654=== removed file 'lib/lp/bugs/stories/duplicate-bug-handling/20-show-bug-is-duplicate.txt'
655--- lib/lp/bugs/stories/duplicate-bug-handling/20-show-bug-is-duplicate.txt 2010-01-04 15:01:34 +0000
656+++ lib/lp/bugs/stories/duplicate-bug-handling/20-show-bug-is-duplicate.txt 1970-01-01 00:00:00 +0000
657@@ -1,41 +0,0 @@
658-When a bug is marked as a duplicate of another bug, the duplicate bug's
659-page shows this prominently, and the comment field is accompanied by an extra
660-warning that it's a duplicate.
661-
662- >>> user_browser.open(
663- ... 'http://launchpad.dev/debian/+source/mozilla-firefox/+bug/2')
664- >>> print find_tag_by_id(
665- ... user_browser.contents, 'duplicate-of').renderContents()
666- bug #1...
667- >>> for message in find_tags_by_class(user_browser.contents, 'message'):
668- ... print extract_text(message)
669- Remember, this bug report is a duplicate of bug #1. Comment here only if...
670-
671-The "Affects" lines are also not expandable, preventing people from changing
672-the bug's status to no effect.
673-
674- >>> affects_table = find_tags_by_class(user_browser.contents, 'listing')[0]
675- >>> "toggleFormVisibility('task" in affects_table
676- False
677-
678-If the bug is no longer marked as a duplicate,
679-
680- >>> user_browser.getLink(id="change_duplicate_bug").click()
681- >>> user_browser.url
682- 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2/+duplicate'
683- >>> user_browser.getControl(name='field.duplicateof').value = ''
684- >>> user_browser.getControl('Change').click()
685- >>> user_browser.url
686- 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2'
687-
688-the duplicate notifications disappear,
689-
690- >>> find_tags_by_class(user_browser.contents, 'message')
691- []
692-
693-and the "Affects" lines become expandable once more.
694-
695- >>> affects_table = find_tags_by_class(user_browser.contents, 'listing')[0]
696- >>> print affects_table
697- <...
698- ...toggleFormVisibility('task...
699
700=== removed file 'lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt'
701--- lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt 2009-08-21 18:40:36 +0000
702+++ lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt 1970-01-01 00:00:00 +0000
703@@ -1,76 +0,0 @@
704-=================================
705-Mark as duplicate form validation
706-=================================
707-
708- >>> bg3 = 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/3'
709- >>> bug_3_dup_url = '%s/+duplicate' % bg3
710- >>> user_browser.open(bug_3_dup_url)
711-
712-Tests the marking of a bug that is a duplicate of a duplicate.
713-
714- >>> user_browser.getControl('Duplicate Of').value = '6'
715- >>> user_browser.getControl('Change').click()
716- >>> print user_browser.contents
717- <...
718- <p class="error message">There is 1 error.</p>
719- ...
720- ...already a duplicate...
721-
722-Tests the marking of a bug that is a duplicate of itself.
723-
724- >>> user_browser.getControl('Duplicate Of').value = '3'
725- >>> user_browser.getControl('Change').click()
726- >>> print user_browser.contents
727- <...
728- <p class="error message">There is 1 error.</p>
729- ...
730- ...can't mark a bug as a duplicate of itself...
731- ...
732-
733-Tests the marking of a bug that is already marked as duplicate with
734-the same value. In this case, nothing should happen, since nothing
735-changed.
736-
737- >>> user_browser.open(
738- ... 'http://bugs.launchpad.dev/firefox/+bug/6/+duplicate')
739- >>> user_browser.getControl('Duplicate Of').value
740- '5'
741- >>> user_browser.getControl('Change').click()
742- >>> user_browser.url
743- 'http://bugs.launchpad.dev/firefox/+bug/6'
744-
745-Tests the input of data that is not a valid value.
746-
747- >>> user_browser.open(
748- ... 'http://bugs.launchpad.dev/firefox/+bug/6/+duplicate')
749- >>> user_browser.getControl('Duplicate Of').value = 'xajskd'
750- >>> user_browser.getControl('Change').click()
751- >>> print user_browser.contents
752- <...
753- ...Not a valid bug number or nickname...
754- ...
755-
756-Tests using a bug nickname as the duplicate value
757-
758- >>> user_browser.open(
759- ... 'http://bugs.launchpad.dev/evolution/+bug/7/+duplicate')
760- >>> user_browser.getControl('Duplicate Of').value = 'blackhole'
761- >>> user_browser.getControl('Change').click()
762- >>> user_browser.url
763- 'http://bugs.launchpad.dev/evolution/+bug/7'
764-
765-Marking a bugX as a duplicate of bugY is not allowed if bugX has dupes.
766-
767- >>> user_browser.open(
768- ... 'http://bugs.launchpad.dev/firefox/+bug/5/+duplicate')
769- >>> user_browser.getControl('Duplicate Of').value = '4'
770- >>> user_browser.getControl('Change').click()
771- >>> print user_browser.contents
772- <...
773- ...There are other bugs already...
774- ...
775-
776- >>> from canonical.launchpad.database import Bug
777- >>> Bug.get(5).duplicateof is None
778- True
779-
780
781=== modified file 'lib/lp/bugs/tests/bug.py'
782--- lib/lp/bugs/tests/bug.py 2010-06-29 15:48:57 +0000
783+++ lib/lp/bugs/tests/bug.py 2010-07-21 12:33:49 +0000
784@@ -196,7 +196,8 @@
785 params = CreateBugParams(
786 owner=no_priv, title=title, comment='Something is broken.')
787 bug = target.createBug(params)
788- bug.duplicateof = duplicateof
789+ if duplicateof is not None:
790+ bug.markAsDuplicate(duplicateof)
791 sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
792 if with_message is True:
793 bug.newMessage(
794
795=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
796--- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-06-05 10:41:52 +0000
797+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-07-21 12:33:49 +0000
798@@ -926,32 +926,6 @@
799 duplicate itself.
800 ...
801
802-If the specified bug has other bugs marked as a duplicate of it, an
803-error message is sent telling you this.
804-
805- >>> bug_four = getUtility(IBugSet).get(4)
806- >>> bug_four.duplicateof.id
807- 2
808-
809- >>> from canonical.launchpad.ftests import syncUpdate
810- >>> syncUpdate(bug_four)
811- >>> submit_commands(bug_four.duplicateof, 'duplicate 1')
812- >>> bug_four.duplicateof.id
813- 2
814-
815- >>> print_latest_email()
816- Subject: Submit Request Failure
817- To: test@canonical.com
818- <BLANKLINE>
819- ...
820- Failing command:
821- duplicate 1
822- ...
823- There are other bugs already marked as duplicates of Bug 2.
824- These bugs should be changed to be duplicates of another bug
825- if you are certain you would like to perform this change.
826- ...
827-
828
829 === cve $cve ===
830
831@@ -1916,7 +1890,7 @@
832 ... Subject: A bug with no affects
833 ...
834 ... private yes
835- ... duplicate 1
836+ ... unsubscribe
837 ... cve 1999-8979
838 ... """
839
840
841=== modified file 'lib/lp/bugs/tests/bugtarget-bugcount.txt'
842--- lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-06-01 10:12:15 +0000
843+++ lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-07-21 12:33:49 +0000
844@@ -129,7 +129,7 @@
845 ... bugtarget, "Another Test Bug", status=BugTaskStatus.NEW)
846 >>> Store.of(another_bug).flush()
847 >>> old_counts = bugtarget.getBugCounts(None)
848- >>> another_bug.duplicateof = bug
849+ >>> another_bug.markAsDuplicate(bug)
850 >>> syncUpdate(another_bug)
851 >>> new_counts = bugtarget.getBugCounts(None)
852 >>> print_count_difference(
853
854=== modified file 'lib/lp/bugs/tests/test_bugchanges.py'
855--- lib/lp/bugs/tests/test_bugchanges.py 2010-06-23 01:15:11 +0000
856+++ lib/lp/bugs/tests/test_bugchanges.py 2010-07-21 12:33:49 +0000
857@@ -90,7 +90,10 @@
858 :return: The value of `attribute` before modification.
859 """
860 obj_before_modification = Snapshot(obj, providing=providedBy(obj))
861- setattr(obj, attribute, new_value)
862+ if attribute == 'duplicateof':
863+ obj.markAsDuplicate(new_value)
864+ else:
865+ setattr(obj, attribute, new_value)
866 notify(ObjectModifiedEvent(
867 obj, obj_before_modification, [attribute], self.user))
868
869@@ -1304,7 +1307,7 @@
870 duplicate_bug = self.factory.makeBug()
871 duplicate_bug_recipients = duplicate_bug.getBugNotificationRecipients(
872 level=BugNotificationLevel.METADATA).getRecipients()
873- duplicate_bug.duplicateof = self.bug
874+ duplicate_bug.markAsDuplicate(self.bug)
875 self.saveOldChanges(duplicate_bug)
876 self.changeAttribute(duplicate_bug, 'duplicateof', None)
877
878@@ -1335,7 +1338,7 @@
879 bug_two = self.factory.makeBug()
880 bug_recipients = self.bug.getBugNotificationRecipients(
881 level=BugNotificationLevel.METADATA).getRecipients()
882- self.bug.duplicateof = bug_one
883+ self.bug.markAsDuplicate(bug_one)
884 self.saveOldChanges()
885 self.changeAttribute(self.bug, 'duplicateof', bug_two)
886
887
888=== modified file 'lib/lp/bugs/tests/test_bugnotification.py'
889--- lib/lp/bugs/tests/test_bugnotification.py 2010-06-03 13:53:29 +0000
890+++ lib/lp/bugs/tests/test_bugnotification.py 2010-07-21 12:33:49 +0000
891@@ -155,7 +155,7 @@
892 user='test@canonical.com')
893 self.bug = self.factory.makeBug()
894 self.dupe_bug = self.factory.makeBug()
895- self.dupe_bug.duplicateof = self.bug
896+ self.dupe_bug.markAsDuplicate(self.bug)
897 self.dupe_subscribers = set(
898 self.dupe_bug.getDirectSubscribers() +
899 self.dupe_bug.getIndirectSubscribers())
900
901=== added file 'lib/lp/bugs/tests/test_duplicate_handling.py'
902--- lib/lp/bugs/tests/test_duplicate_handling.py 1970-01-01 00:00:00 +0000
903+++ lib/lp/bugs/tests/test_duplicate_handling.py 2010-07-21 12:33:49 +0000
904@@ -0,0 +1,102 @@
905+# Copyright 2010 Canonical Ltd. This software is licensed under the
906+# GNU Affero General Public License version 3 (see the file LICENSE).
907+
908+"""Tests for bug duplicate validation."""
909+
910+from textwrap import dedent
911+import unittest
912+
913+from zope.security.interfaces import ForbiddenAttribute
914+
915+from canonical.testing import DatabaseFunctionalLayer
916+
917+from lp.bugs.interfaces.bug import InvalidDuplicateValue
918+from lp.testing import TestCaseWithFactory
919+
920+
921+class TestDuplicateAttributes(TestCaseWithFactory):
922+ """Test bug attributes related to duplicate handling."""
923+
924+ layer = DatabaseFunctionalLayer
925+
926+ def setUp(self):
927+ super(TestDuplicateAttributes, self).setUp(user='test@canonical.com')
928+
929+ def setDuplicateofDirectly(self, bug, duplicateof):
930+ """Helper method to set duplicateof directly."""
931+ bug.duplicateof = duplicateof
932+
933+ def test_duplicateof_readonly(self):
934+ # Test that no one can set duplicateof directly.
935+ bug = self.factory.makeBug()
936+ dupe_bug = self.factory.makeBug()
937+ self.assertRaises(
938+ ForbiddenAttribute, self.setDuplicateofDirectly, bug, dupe_bug)
939+
940+
941+class TestMarkDuplicateValidation(TestCaseWithFactory):
942+ """Test for validation around marking bug duplicates."""
943+
944+ layer = DatabaseFunctionalLayer
945+
946+ def setUp(self):
947+ super(TestMarkDuplicateValidation, self).setUp(
948+ user='test@canonical.com')
949+ self.bug = self.factory.makeBug()
950+ self.dupe_bug = self.factory.makeBug()
951+ self.dupe_bug.markAsDuplicate(self.bug)
952+ self.possible_dupe = self.factory.makeBug()
953+
954+ def assertDuplicateError(self, bug, duplicateof, msg):
955+ try:
956+ bug.markAsDuplicate(duplicateof)
957+ except InvalidDuplicateValue, err:
958+ self.assertEqual(str(err), msg)
959+
960+ def test_error_on_duplicate_to_duplicate(self):
961+ # Test that a bug cannot be marked a duplicate of
962+ # a bug that is already itself a duplicate.
963+ msg = dedent(u"""
964+ Bug %s is already a duplicate of bug %s. You
965+ can only mark a bug report as duplicate of one that
966+ isn't a duplicate itself.
967+ """ % (
968+ self.dupe_bug.id, self.dupe_bug.duplicateof.id))
969+ self.assertDuplicateError(
970+ self.possible_dupe, self.dupe_bug, msg)
971+
972+ def test_error_duplicate_to_itself(self):
973+ # Test that a bug cannot be marked its own duplicate
974+ msg = dedent(u"""
975+ You can't mark a bug as a duplicate of itself.""")
976+ self.assertDuplicateError(self.bug, self.bug, msg)
977+
978+
979+class TestMoveDuplicates(TestCaseWithFactory):
980+ """Test duplicates are moved when master bug is marked a duplicate."""
981+
982+ layer = DatabaseFunctionalLayer
983+
984+ def setUp(self):
985+ super(TestMoveDuplicates, self).setUp(user='test@canonical.com')
986+
987+ def test_duplicates_are_moved(self):
988+ # Confirm that a bug with two duplicates can be marked
989+ # a duplicate of a new bug and that the duplicates will
990+ # be re-marked as duplicates of the new bug, too.
991+ bug = self.factory.makeBug()
992+ dupe_one = self.factory.makeBug()
993+ dupe_two = self.factory.makeBug()
994+ dupe_one.markAsDuplicate(bug)
995+ dupe_two.markAsDuplicate(bug)
996+ self.assertEqual(dupe_one.duplicateof, bug)
997+ self.assertEqual(dupe_two.duplicateof, bug)
998+ new_bug = self.factory.makeBug()
999+ bug.markAsDuplicate(new_bug)
1000+ self.assertEqual(bug.duplicateof, new_bug)
1001+ self.assertEqual(dupe_one.duplicateof, new_bug)
1002+ self.assertEqual(dupe_two.duplicateof, new_bug)
1003+
1004+
1005+def test_suite():
1006+ return unittest.TestLoader().loadTestsFromName(__name__)
1007
1008=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
1009--- lib/lp/registry/browser/tests/person-views.txt 2010-07-16 16:14:39 +0000
1010+++ lib/lp/registry/browser/tests/person-views.txt 2010-07-21 12:33:49 +0000
1011@@ -546,7 +546,7 @@
1012
1013 But duplicate bugs are never displayed.
1014
1015- >>> another_bug.duplicateof = bug
1016+ >>> another_bug.markAsDuplicate(bug)
1017
1018 # Create a new view because we're testing some cached properties.
1019