Merge lp:~deryck/launchpad/update-storm-17-add-back-dupe-changes into lp:launchpad

Proposed by Deryck Hodge
Status: Merged
Approved by: Deryck Hodge
Approved revision: no longer in the source branch.
Merged at revision: 11313
Proposed branch: lp:~deryck/launchpad/update-storm-17-add-back-dupe-changes
Merge into: lp:launchpad
Diff against target: 1296 lines (+328/-297)
36 files modified
lib/canonical/launchpad/fields/__init__.py (+0/-11)
lib/canonical/launchpad/icing/style-3-0.css.in (+6/-0)
lib/canonical/launchpad/mail/commands.py (+21/-11)
lib/lp/bugs/browser/bug.py (+28/-1)
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 (+50/-8)
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/javascript/bugtask_index.js (+38/-10)
lib/lp/bugs/model/bug.py (+10/-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/templates/bug-portlet-actions.pt (+3/-3)
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/bugs/windmill/tests/test_mark_duplicate.py (+17/-3)
lib/lp/registry/browser/tests/person-views.txt (+1/-1)
versions.cfg (+1/-3)
To merge this branch: bzr merge lp:~deryck/launchpad/update-storm-17-add-back-dupe-changes
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+31983@code.launchpad.net

Commit message

Update to latest Storm version, 0.17.

Also, this adds back some work to get bugs consistently using Bug.markAsDuplicate, as well as other duplicate-related fixes, that were reverted until the new Storm release was available.

Description of the change

My previous approved (and landed) work to converge on markAsDuplicate
rather than setting duplicateof directly was reverted due to a test
failure from a Storm bug. This branch includes my already approved
changes with a change to versions.cfg to get us on the Storm version
that was just released, 0.17.

I have already committed the new Storm to download-cache. I have run
tests. All that needs approval here is the change in Storm version.

Everything in the diff other than this change to versions.cfg is
approved in the MPs here:

https://code.edge.launchpad.net/~deryck/launchpad/do-the-right-thing-dupe-move-78596/+merge/27144
https://code.edge.launchpad.net/~deryck/launchpad/better-dupe-handling-ui/+merge/30309

And I have approval from Robert to change the version of Storm here:

https://code.edge.launchpad.net/~deryck/launchpad/update-storm-r365/+merge/31881

However, I didn't get this landed yesterday before the new Storm
today, so I thought I would do a versions.cfg update alongside
restoring my reverted duplicate handling work.

Cheers,
deryck

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/fields/__init__.py'
2--- lib/canonical/launchpad/fields/__init__.py 2010-07-22 12:17:41 +0000
3+++ lib/canonical/launchpad/fields/__init__.py 2010-08-06 18:42:48 +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/icing/style-3-0.css.in'
33--- lib/canonical/launchpad/icing/style-3-0.css.in 2010-07-26 21:00:39 +0000
34+++ lib/canonical/launchpad/icing/style-3-0.css.in 2010-08-06 18:42:48 +0000
35@@ -1828,6 +1828,12 @@
36 */
37 display: inline;
38 }
39+body.tab-bugs #duplicate-actions .sprite {
40+ /* Override sprite style for edit icon on "Mark as duplicate"
41+ to make the text not appear on a second line.
42+ */
43+ display:inline;
44+ }
45 .yui-picker-results li.sprite {
46 /* XXX: EdwinGrubbs 2009-07-27 bug=405476
47 * Override the .yui-picker-results style, so that the next icon
48
49=== modified file 'lib/canonical/launchpad/mail/commands.py'
50--- lib/canonical/launchpad/mail/commands.py 2010-08-02 02:13:52 +0000
51+++ lib/canonical/launchpad/mail/commands.py 2010-08-06 18:42:48 +0000
52@@ -382,30 +382,40 @@
53 return {'title': self.string_args[0]}
54
55
56-class DuplicateEmailCommand(EditEmailCommand):
57+class DuplicateEmailCommand(EmailCommand):
58 """Marks a bug as a duplicate of another bug."""
59
60 implements(IBugEditEmailCommand)
61 _numberOfArguments = 1
62
63- def convertArguments(self, context):
64- """See EmailCommand."""
65+ def execute(self, context, current_event):
66+ """See IEmailCommand."""
67+ self._ensureNumberOfArguments()
68 [bug_id] = self.string_args
69- if bug_id == 'no':
70+
71+ if bug_id != 'no':
72+ try:
73+ bug = getUtility(IBugSet).getByNameOrID(bug_id)
74+ except NotFoundError:
75+ raise EmailProcessingError(
76+ get_error_message('no-such-bug.txt', bug_id=bug_id))
77+ else:
78 # 'no' is a special value for unmarking a bug as a duplicate.
79- return {'duplicateof': None}
80- try:
81- bug = getUtility(IBugSet).getByNameOrID(bug_id)
82- except NotFoundError:
83- raise EmailProcessingError(
84- get_error_message('no-such-bug.txt', bug_id=bug_id))
85+ bug = None
86+
87 duplicate_field = IBug['duplicateof'].bind(context)
88 try:
89 duplicate_field.validate(bug)
90 except ValidationError, error:
91 raise EmailProcessingError(error.doc())
92
93- return {'duplicateof': bug}
94+ context_snapshot = Snapshot(
95+ context, providing=providedBy(context))
96+ context.markAsDuplicate(bug)
97+ current_event = ObjectModifiedEvent(
98+ context, context_snapshot, 'duplicateof')
99+ notify(current_event)
100+ return bug, current_event
101
102
103 class CVEEmailCommand(EmailCommand):
104
105=== modified file 'lib/lp/bugs/browser/bug.py'
106--- lib/lp/bugs/browser/bug.py 2010-08-02 02:13:52 +0000
107+++ lib/lp/bugs/browser/bug.py 2010-08-06 18:42:48 +0000
108@@ -49,8 +49,9 @@
109 from canonical.cachedproperty import cachedproperty
110
111 from canonical.launchpad import _
112+from canonical.launchpad.fields import DuplicateBug
113+from canonical.launchpad.webapp.interfaces import ILaunchBag
114 from lp.app.errors import NotFoundError
115-from canonical.launchpad.webapp.interfaces import ILaunchBag
116 from lp.bugs.interfaces.bug import IBug, IBugSet
117 from lp.bugs.interfaces.bugattachment import BugAttachmentType
118 from lp.bugs.interfaces.bugtask import (
119@@ -659,9 +660,35 @@
120 field_names = ['duplicateof']
121 label = "Mark bug report as a duplicate"
122
123+ def setUpFields(self):
124+ """Make the readonly version of duplicateof available."""
125+ super(BugMarkAsDuplicateView, self).setUpFields()
126+
127+ duplicateof_field = DuplicateBug(
128+ __name__='duplicateof', title=_('Duplicate Of'), required=False)
129+
130+ self.form_fields = self.form_fields.omit('duplicateof')
131+ self.form_fields = formlib.form.Fields(duplicateof_field)
132+
133+ @property
134+ def initial_values(self):
135+ """See `LaunchpadFormView.`"""
136+ return {'duplicateof': self.context.bug.duplicateof}
137+
138 @action('Change', name='change')
139 def change_action(self, action, data):
140 """Update the bug."""
141+ data = dict(data)
142+ # We handle duplicate changes by hand instead of leaving it to
143+ # the usual machinery because we must use bug.markAsDuplicate().
144+ bug = self.context.bug
145+ bug_before_modification = Snapshot(
146+ bug, providing=providedBy(bug))
147+ duplicateof = data.pop('duplicateof')
148+ bug.markAsDuplicate(duplicateof)
149+ notify(
150+ ObjectModifiedEvent(bug, bug_before_modification, 'duplicateof'))
151+ # Apply other changes.
152 self.updateBugFromData(data)
153
154
155
156=== modified file 'lib/lp/bugs/browser/tests/bug-subscription-views.txt'
157--- lib/lp/bugs/browser/tests/bug-subscription-views.txt 2010-07-22 12:17:41 +0000
158+++ lib/lp/bugs/browser/tests/bug-subscription-views.txt 2010-08-06 18:42:48 +0000
159@@ -30,7 +30,7 @@
160 {"name12": "subscriber-12"}
161
162 >>> login('foo.bar@canonical.com')
163- >>> bug_15.duplicateof = bug_13
164+ >>> bug_15.markAsDuplicate(bug_13)
165 >>> bug_13 = getUtility(IBugSet).get(13)
166
167 >>> subscriber_ids_view = create_initialized_view(
168
169=== modified file 'lib/lp/bugs/browser/tests/bug-views.txt'
170--- lib/lp/bugs/browser/tests/bug-views.txt 2010-07-22 12:17:41 +0000
171+++ lib/lp/bugs/browser/tests/bug-views.txt 2010-08-06 18:42:48 +0000
172@@ -249,7 +249,8 @@
173 and see how the returned link changes.
174
175 >>> bug_two = bugset.get(2)
176- >>> bug_two.duplicateof = 5
177+ >>> bug_five = bugset.get(5)
178+ >>> bug_two.markAsDuplicate(bug_five)
179
180 >>> bug_page_view = getMultiAdapter(
181 ... (bug_five_in_firefox.bug, request), name="+portlet-duplicates")
182@@ -380,7 +381,7 @@
183
184 >>> from canonical.launchpad.ftests import syncUpdate
185
186- >>> bug_two.duplicateof = bug_three
187+ >>> bug_two.markAsDuplicate(bug_three)
188 >>> syncUpdate(bug_two)
189
190 >>> bug_menu.subscription().text
191@@ -391,7 +392,7 @@
192 the current user is a member. So, for Foo Bar, bug #3 has a simple
193 Subscribe link initially.
194
195- >>> bug_two.duplicateof = None
196+ >>> bug_two.markAsDuplicate(None)
197 >>> syncUpdate(bug_two)
198
199 >>> login("foo.bar@canonical.com")
200@@ -406,7 +407,7 @@
201 >>> bug_two.subscribe(ubuntu_team, ubuntu_team)
202 <BugSubscription...>
203
204- >>> bug_two.duplicateof = bug_three
205+ >>> bug_two.markAsDuplicate(bug_three)
206 >>> syncUpdate(bug_two)
207
208 >>> bug_menu.subscription().text
209
210=== modified file 'lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt'
211--- lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt 2010-07-22 12:17:41 +0000
212+++ lib/lp/bugs/browser/tests/special/bugs-fixed-elsewhere.txt 2010-08-06 18:42:48 +0000
213@@ -125,7 +125,7 @@
214
215 Bugs that are duplicate of other bugs aren't included in the count.
216
217- >>> another_bug.duplicateof = bug
218+ >>> another_bug.markAsDuplicate(bug)
219 >>> syncUpdate(another_bug)
220
221 >>> view.bugs_fixed_elsewhere_count
222
223=== modified file 'lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt'
224--- lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt 2010-07-22 12:17:41 +0000
225+++ lib/lp/bugs/browser/tests/special/bugtarget-recently-touched-bugs.txt 2010-08-06 18:42:48 +0000
226@@ -80,7 +80,7 @@
227 Bugs that are duplicates of other bugs will be omitted from the list as
228 well.
229
230- >>> bug_b.duplicateof = bug_c
231+ >>> bug_b.markAsDuplicate(bug_c)
232 >>> syncUpdate(bug_b)
233
234 >>> for bugtask in portlet_view.getMostRecentlyUpdatedBugTasks()[:2]:
235
236=== modified file 'lib/lp/bugs/configure.zcml'
237--- lib/lp/bugs/configure.zcml 2010-07-29 19:48:15 +0000
238+++ lib/lp/bugs/configure.zcml 2010-08-06 18:42:48 +0000
239@@ -652,7 +652,6 @@
240 getBugTasksByPackageName
241 users_affected_count
242 users_unaffected_count
243- readonly_duplicateof
244 users_affected
245 users_unaffected
246 users_affected_count_with_dupes
247@@ -703,7 +702,6 @@
248 date_last_updated
249 date_made_private
250 datecreated
251- duplicateof
252 hits
253 hitstimestamp
254 name
255
256=== modified file 'lib/lp/bugs/doc/bug.txt'
257--- lib/lp/bugs/doc/bug.txt 2010-07-29 19:12:10 +0000
258+++ lib/lp/bugs/doc/bug.txt 2010-08-06 18:42:48 +0000
259@@ -184,18 +184,22 @@
260
261 >>> login("foo.bar@canonical.com")
262
263+ >>> from lp.registry.interfaces.product import IProductSet
264+ >>> productset = getUtility(IProductSet)
265+ >>> firefox = productset.get(4)
266 >>> firefox_test_bug = bugset.get(3)
267- >>> firefox_test_bug.duplicateof = firefox_crashes.id
268+ >>> firefox_master_bug = factory.makeBug(product=firefox)
269+ >>> firefox_test_bug.markAsDuplicate(firefox_master_bug)
270 >>> flush_database_updates()
271
272 >>> dups_of_bug_six = bugset.searchAsUser(
273- ... duplicateof=firefox_crashes, user=current_user())
274+ ... duplicateof=firefox_master_bug, user=current_user())
275 >>> print dups_of_bug_six.count()
276 1
277 >>> dups_of_bug_six[0].id
278 3
279
280- >>> firefox_test_bug.duplicateof = None
281+ >>> firefox_test_bug.markAsDuplicate(None)
282 >>> flush_database_updates()
283 >>> dups_of_bug_six = bugset.searchAsUser(
284 ... duplicateof=firefox_crashes, user=current_user())
285@@ -426,11 +430,6 @@
286
287 When a public bug is filed:
288
289- >>> from lp.registry.interfaces.product import IProductSet
290- >>> productset = getUtility(IProductSet)
291- >>> bugset = getUtility(IBugSet)
292-
293- >>> firefox = productset.get(4)
294 >>> params = CreateBugParams(
295 ... title="test firefox bug", comment="blah blah blah", owner=foobar)
296 >>> params.setBugTarget(product=firefox)
297@@ -765,7 +764,7 @@
298
299 >>> print firefox_bug.duplicateof
300 None
301- >>> firefox_bug.duplicateof = firefox_crashes
302+ >>> firefox_bug.markAsDuplicate(firefox_master_bug)
303
304 >>> bug_duplicateof_changed = ObjectModifiedEvent(
305 ... firefox_bug, bug_before_modification, ["duplicateof"])
306@@ -1241,7 +1240,7 @@
307
308 >>> dupe_affected_user = factory.makePerson(name='sheila-shakespeare')
309 >>> dupe_one = factory.makeBug(owner=dupe_affected_user)
310- >>> dupe_one.duplicateof = test_bug
311+ >>> dupe_one.markAsDuplicate(test_bug)
312 >>> test_bug.users_affected_count_with_dupes
313 3
314
315@@ -1275,7 +1274,7 @@
316
317 >>> dupe_affected_other_user = factory.makePerson(name='napoleon-bonaparte')
318 >>> dupe_three = factory.makeBug(owner=dupe_affected_other_user)
319- >>> dupe_three.duplicateof = test_bug
320+ >>> dupe_three.markAsDuplicate(test_bug)
321 >>> test_bug.users_affected_count_with_dupes
322 4
323
324@@ -1284,7 +1283,7 @@
325 user_affected_count still only increments by 1 for that user.
326
327 >>> dupe_two = factory.makeBug(owner=dupe_affected_user)
328- >>> dupe_two.duplicateof = test_bug
329+ >>> dupe_two.markAsDuplicate(test_bug)
330 >>> test_bug.users_affected_count_with_dupes
331 4
332
333@@ -1438,7 +1437,7 @@
334
335 >>> new_bug_3 = bugs[3]
336 >>> new_bug_4 = bugs[4]
337- >>> new_bug_3.duplicateof = new_bug_4
338+ >>> new_bug_3.markAsDuplicate(new_bug_4)
339 >>> matching_bugs = getUtility(IBugSet).getDistinctBugsForBugTasks(
340 ... bug_tasks, user=no_priv)
341
342@@ -1566,7 +1565,7 @@
343 >>> dupe = factory.makeBug()
344 >>> subscription = dupe.subscribe(person, person)
345
346- >>> dupe.duplicateof = bug
347+ >>> dupe.markAsDuplicate(bug)
348
349 # Re-fetch the bug so that the fact that it's a duplicate definitely
350 # registers.
351
352=== modified file 'lib/lp/bugs/doc/bugactivity.txt'
353--- lib/lp/bugs/doc/bugactivity.txt 2010-07-22 16:27:12 +0000
354+++ lib/lp/bugs/doc/bugactivity.txt 2010-08-06 18:42:48 +0000
355@@ -123,7 +123,8 @@
356 ... "id", "title", "description", "name",
357 ... "private", "duplicateof", "security_related"]
358 >>> old_bug = Snapshot(bug, providing=IBug)
359- >>> bug.duplicateof = 1
360+ >>> latest_bug = factory.makeBug()
361+ >>> bug.markAsDuplicate(latest_bug)
362 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
363 >>> notify(bug_edited)
364 >>> latest_activity = bug.activity[-1]
365@@ -131,25 +132,26 @@
366 u'marked as duplicate'
367 >>> latest_activity.oldvalue is None
368 True
369- >>> latest_activity.newvalue == u'1'
370+ >>> latest_activity.newvalue == unicode(latest_bug.id)
371 True
372
373
374-== Bug report has itss duplicate marker changed to another bug report ==
375+== Bug report has its duplicate marker changed to another bug report ==
376
377 >>> edit_fields = [
378 ... "id", "title", "description", "name", "private", "duplicateof",
379 ... "security_related"]
380 >>> old_bug = Snapshot(bug, providing=IBug)
381- >>> bug.duplicateof = 2
382+ >>> another_bug = factory.makeBug()
383+ >>> bug.markAsDuplicate(another_bug)
384 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
385 >>> notify(bug_edited)
386 >>> latest_activity = bug.activity[-1]
387 >>> latest_activity.whatchanged
388 u'changed duplicate marker'
389- >>> latest_activity.oldvalue == u'1'
390+ >>> latest_activity.oldvalue == unicode(latest_bug.id)
391 True
392- >>> latest_activity.newvalue == u'2'
393+ >>> latest_activity.newvalue == unicode(another_bug.id)
394 True
395
396
397@@ -159,18 +161,58 @@
398 ... "id", "title", "description", "name", "private", "duplicateof",
399 ... "security_related"]
400 >>> old_bug = Snapshot(bug, providing=IBug)
401- >>> bug.duplicateof = None
402+ >>> bug.markAsDuplicate(None)
403 >>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
404 >>> notify(bug_edited)
405 >>> latest_activity = bug.activity[-1]
406 >>> latest_activity.whatchanged
407 u'removed duplicate marker'
408- >>> latest_activity.oldvalue == u'2'
409+ >>> latest_activity.oldvalue == unicode(another_bug.id)
410 True
411 >>> latest_activity.newvalue is None
412 True
413
414
415+== A bug with multiple duplicates ==
416+
417+When a bug has multiple duplicates and is itself marked a duplicate,
418+the duplicates are automatically duped to the same master bug. These changes
419+are then reflected in the activity log for each bug itself.
420+
421+ >>> edit_fields = [
422+ ... "id", "title", "description", "name", "private", "duplicateof",
423+ ... "security_related"]
424+ >>> initial_bug = factory.makeBug()
425+ >>> dupe_one = factory.makeBug()
426+ >>> dupe_two = factory.makeBug()
427+ >>> dupe_one.markAsDuplicate(initial_bug)
428+ >>> dupe_two.markAsDuplicate(initial_bug)
429+
430+After creating a few bugs to work with, we create a final bug and duplicate
431+the initial bug against it.
432+
433+ >>> final_bug = factory.makeBug()
434+ >>> initial_bug.markAsDuplicate(final_bug)
435+
436+Now, we confirm the activity log for the other bugs correctly list the
437+final_bug as their master bug.
438+
439+ >>> latest_activity = dupe_one.activity[-1]
440+ >>> print latest_activity.whatchanged
441+ changed duplicate marker
442+ >>> latest_activity.oldvalue == unicode(initial_bug.id)
443+ True
444+ >>> latest_activity.newvalue == unicode(final_bug.id)
445+ True
446+ >>> latest_activity = dupe_two.activity[-1]
447+ >>> print latest_activity.whatchanged
448+ changed duplicate marker
449+ >>> latest_activity.oldvalue == unicode(initial_bug.id)
450+ True
451+ >>> latest_activity.newvalue == unicode(final_bug.id)
452+ True
453+
454+
455 == BugActivityItem ==
456
457 BugActivityItem implements the stuff that BugActivity doesn't need to
458
459=== modified file 'lib/lp/bugs/doc/bugnotification-sending.txt'
460--- lib/lp/bugs/doc/bugnotification-sending.txt 2010-07-22 20:30:26 +0000
461+++ lib/lp/bugs/doc/bugnotification-sending.txt 2010-08-06 18:42:48 +0000
462@@ -760,13 +760,14 @@
463 >>> switch_db_to_launchpad()
464 >>> new_bug = ubuntu.createBug(params)
465 >>> switch_db_to_bugnotification()
466-
467 >>> flush_notifications()
468
469 If a bug is a duplicate of another bug, a marker gets inserted at the
470 top of the email:
471
472- >>> new_bug.duplicateof = bug_one
473+ >>> switch_db_to_launchpad()
474+ >>> new_bug.markAsDuplicate(bug_one)
475+ >>> switch_db_to_bugnotification()
476 >>> comment = getUtility(IMessageSet).fromText(
477 ... 'subject', 'a comment.', sample_person,
478 ... datecreated=ten_minutes_ago)
479
480=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
481--- lib/lp/bugs/doc/bugsubscription.txt 2010-07-22 12:17:41 +0000
482+++ lib/lp/bugs/doc/bugsubscription.txt 2010-08-06 18:42:48 +0000
483@@ -341,7 +341,7 @@
484 Stuart Bishop
485 Ubuntu Team
486
487- >>> linux_source_bug_dupe.duplicateof = linux_source_bug
488+ >>> linux_source_bug_dupe.markAsDuplicate(linux_source_bug)
489 >>> linux_source_bug_dupe.syncUpdate()
490
491 >>> print_displayname(linux_source_bug.getIndirectSubscribers())
492@@ -868,7 +868,7 @@
493 ... comment="one more dupe test description",
494 ... owner=keybuk)
495 >>> dupe_ff_bug = firefox.createBug(params)
496- >>> dupe_ff_bug.duplicateof = ff_bug
497+ >>> dupe_ff_bug.markAsDuplicate(ff_bug)
498 >>> dupe_ff_bug.syncUpdate()
499 >>> dupe_ff_bug.subscribe(foobar, lifeless)
500 <BugSubscription at ...>
501
502=== modified file 'lib/lp/bugs/doc/bugtask-package-bugcounts.txt'
503--- lib/lp/bugs/doc/bugtask-package-bugcounts.txt 2010-07-22 12:17:41 +0000
504+++ lib/lp/bugs/doc/bugtask-package-bugcounts.txt 2010-08-06 18:42:48 +0000
505@@ -170,7 +170,7 @@
506 Duplicates bugs are omitted from the counts.
507
508 >>> from canonical.launchpad.interfaces import IBugSet
509- >>> bug.duplicateof = getUtility(IBugSet).get(1)
510+ >>> bug.markAsDuplicate(getUtility(IBugSet).get(1))
511 >>> syncUpdate(bug)
512 >>> package_counts = getUtility(IBugTaskSet).getBugCountsForPackages(
513 ... user=foo_bar, packages=packages)
514
515=== modified file 'lib/lp/bugs/doc/bugtask-search.txt'
516--- lib/lp/bugs/doc/bugtask-search.txt 2010-07-22 12:17:41 +0000
517+++ lib/lp/bugs/doc/bugtask-search.txt 2010-08-06 18:42:48 +0000
518@@ -1037,7 +1037,7 @@
519
520 >>> bug_nine = getUtility(IBugSet).get(9)
521 >>> bug_ten = getUtility(IBugSet).get(10)
522- >>> bug_ten.duplicateof = bug_nine
523+ >>> bug_ten.markAsDuplicate(bug_nine)
524 >>> flush_database_updates()
525
526 Searching again reveals bug #9 at the top of the list, since it now has a duplicate.
527
528=== modified file 'lib/lp/bugs/doc/bugzilla-import.txt'
529--- lib/lp/bugs/doc/bugzilla-import.txt 2010-07-22 12:17:41 +0000
530+++ lib/lp/bugs/doc/bugzilla-import.txt 2010-08-06 18:42:48 +0000
531@@ -87,8 +87,7 @@
532
533 >>> duplicates = [
534 ... (1, 2),
535- ... (2, 3),
536- ... (2, 4),
537+ ... (3, 4),
538 ... ]
539
540 >>> class FakeBackend:
541@@ -536,9 +535,9 @@
542 None
543 >>> bug2.duplicateof == bug1
544 True
545- >>> bug3.duplicateof == bug2
546+ >>> bug3.duplicateof == None
547 True
548- >>> bug4.duplicateof == bug2
549+ >>> bug4.duplicateof == bug3
550 True
551
552
553
554=== modified file 'lib/lp/bugs/doc/checkwatches.txt'
555--- lib/lp/bugs/doc/checkwatches.txt 2010-07-22 12:17:41 +0000
556+++ lib/lp/bugs/doc/checkwatches.txt 2010-08-06 18:42:48 +0000
557@@ -490,8 +490,8 @@
558 another bug, comments won't be synced and the bug won't be linked back
559 to the remote bug.
560
561+ >>> LaunchpadZopelessLayer.switchDbUser('launchpad')
562 >>> bug_15 = getUtility(IBugSet).get(15)
563- >>> bug_watch.bug.duplicateof = bug_15
564- >>> transaction.commit()
565+ >>> bug_watch.bug.markAsDuplicate(bug_15)
566
567 >>> updater.updateBugWatches(remote_system, [bug_watch], now=nowish)
568
569=== modified file 'lib/lp/bugs/doc/malone-karma.txt'
570--- lib/lp/bugs/doc/malone-karma.txt 2010-07-22 12:17:41 +0000
571+++ lib/lp/bugs/doc/malone-karma.txt 2010-08-06 18:42:48 +0000
572@@ -204,7 +204,7 @@
573
574 >>> bug_one = getUtility(IBugSet).get(1)
575 >>> old_bug = Snapshot(bug, providing=IBug)
576- >>> bug.duplicateof = bug_one
577+ >>> bug.markAsDuplicate(bug_one)
578
579 (Notice how changing a bug with multiple bugtasks will assign karma to you
580 once for each bugtask. This is so because we consider changes in a bug to
581
582=== modified file 'lib/lp/bugs/interfaces/bug.py'
583--- lib/lp/bugs/interfaces/bug.py 2010-08-02 02:13:52 +0000
584+++ lib/lp/bugs/interfaces/bug.py 2010-08-06 18:42:48 +0000
585@@ -184,8 +184,7 @@
586 ownerID = Int(title=_('Owner'), required=True, readonly=True)
587 owner = exported(
588 Reference(IPerson, title=_("The owner's IPerson"), readonly=True))
589- duplicateof = DuplicateBug(title=_('Duplicate Of'), required=False)
590- readonly_duplicateof = exported(
591+ duplicateof = exported(
592 DuplicateBug(title=_('Duplicate Of'), required=False, readonly=True),
593 exported_as='duplicate_of')
594 # This is redefined from IPrivacy.private because the attribute is
595@@ -755,8 +754,8 @@
596 def markUserAffected(user, affected=True):
597 """Mark :user: as affected by this bug."""
598
599- @mutator_for(readonly_duplicateof)
600- @operation_parameters(duplicate_of=copy_field(readonly_duplicateof))
601+ @mutator_for(duplicateof)
602+ @operation_parameters(duplicate_of=copy_field(duplicateof))
603 @export_write_operation()
604 def markAsDuplicate(duplicate_of):
605 """Mark this bug as a duplicate of another."""
606
607=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
608--- lib/lp/bugs/javascript/bugtask_index.js 2010-07-22 16:27:12 +0000
609+++ lib/lp/bugs/javascript/bugtask_index.js 2010-08-06 18:42:48 +0000
610@@ -165,10 +165,23 @@
611 update_dupe_url = update_dupe_link.get('href');
612 var mark_dupe_form_url = update_dupe_url + '/++form++';
613
614+ var form_header = '<p>Marking this bug as a duplicate will, by default, ' +
615+ 'hide it from search results listings.</p>';
616+
617+ var has_dupes = Y.one('#portlet-duplicates');
618+ if (has_dupes !== null) {
619+ form_header = form_header +
620+ '<p style="padding:2px 2px 0 36px;" ' +
621+ 'class="large-warning"><strong>Note:</strong> ' +
622+ 'This bug has duplicates of its own. ' +
623+ 'If you go ahead, they too will become duplicates of ' +
624+ 'the bug you specify here. This cannot be undone.' +
625+ '</p></div>';
626+ }
627+
628 duplicate_form_overlay = new Y.lazr.FormOverlay({
629 headerContent: '<h2>Mark bug report as duplicate</h2>',
630- form_header: 'Marking the bug as a duplicate will, by default, ' +
631- 'hide it from search results listings.',
632+ form_header: form_header,
633 form_submit_button: Y.Node.create(submit_button_html),
634 form_cancel_button: Y.Node.create(cancel_button_html),
635 centered: true,
636@@ -187,6 +200,7 @@
637 if (duplicate_form_overlay){
638 e.preventDefault();
639 duplicate_form_overlay.show();
640+ Y.DOM.byId('field.duplicateof').focus();
641 }
642 });
643 // Add a class denoting them as js-action links.
644@@ -366,8 +380,15 @@
645 // Hide the formoverlay:
646 duplicate_form_overlay.hide();
647
648+ // Hide the dupe edit icon if it exists.
649+ var dupe_edit_icon = Y.one('#change_duplicate_bug');
650+ if (dupe_edit_icon !== null) {
651+ dupe_edit_icon.setStyle('display', 'none');
652+ }
653+
654 // Add the spinner...
655 var dupe_span = Y.one('#mark-duplicate-text');
656+ dupe_span.removeClass('sprite bug-dupe');
657 dupe_span.addClass('update-in-progress-message');
658
659 // Set the new duplicate link on the bug entry.
660@@ -390,28 +411,35 @@
661
662 if (new_dup_url !== null) {
663 dupe_span.set('innerHTML', [
664- 'Duplicate of <a>bug #</a> ',
665- '<a class="menu-link-mark-dupe js-action sprite edit">',
666- '<span class="invisible-link">edit</span></a>'].join(""));
667+ '<a id="change_duplicate_bug" ',
668+ 'title="Edit or remove linked duplicate bug" ',
669+ 'class="sprite edit"></a>',
670+ 'Duplicate of <a>bug #</a>'].join(""));
671 dupe_span.all('a').item(0)
672+ .set('href', update_dupe_url);
673+ dupe_span.all('a').item(1)
674 .set('href', '/bugs/' + new_dup_id)
675 .appendChild(document.createTextNode(new_dup_id));
676- dupe_span.all('a').item(1)
677- .set('href', update_dupe_url);
678+ var has_dupes = Y.one('#portlet-duplicates');
679+ if (has_dupes !== null) {
680+ has_dupes.get('parentNode').removeChild(has_dupes);
681+ }
682 show_comment_on_duplicate_warning();
683 } else {
684+ dupe_span.addClass('sprite bug-dupe');
685 dupe_span.set('innerHTML', [
686- '<a class="menu-link-mark-dupe js-action ',
687- 'sprite bug-dupe">Mark as duplicate</a>'].join(""));
688+ '<a class="menu-link-mark-dupe js-action">',
689+ 'Mark as duplicate</a>'].join(""));
690 dupe_span.one('a').set('href', update_dupe_url);
691 hide_comment_on_duplicate_warning();
692 }
693 Y.lazr.anim.green_flash({node: dupe_span}).run();
694 // ensure the new link is hooked up correctly:
695- dupe_span.one('a.menu-link-mark-dupe').on(
696+ dupe_span.one('a').on(
697 'click', function(e){
698 e.preventDefault();
699 duplicate_form_overlay.show();
700+ Y.DOM.byId('field.duplicateof').focus();
701 });
702 },
703 failure: function(id, request) {
704
705=== modified file 'lib/lp/bugs/model/bug.py'
706--- lib/lp/bugs/model/bug.py 2010-08-02 02:13:52 +0000
707+++ lib/lp/bugs/model/bug.py 2010-08-06 18:42:48 +0000
708@@ -1493,11 +1493,6 @@
709
710 self.updateHeat()
711
712- @property
713- def readonly_duplicateof(self):
714- """See `IBug`."""
715- return self.duplicateof
716-
717 def markAsDuplicate(self, duplicate_of):
718 """See `IBug`."""
719 field = DuplicateBug()
720@@ -1506,6 +1501,16 @@
721 try:
722 if duplicate_of is not None:
723 field._validate(duplicate_of)
724+ if self.duplicates:
725+ for duplicate in self.duplicates:
726+ # Fire a notify event in model code since moving
727+ # duplicates of a duplicate does not normally fire an
728+ # event.
729+ dupe_before = Snapshot(
730+ duplicate, providing=providedBy(duplicate))
731+ duplicate.markAsDuplicate(duplicate_of)
732+ notify(ObjectModifiedEvent(
733+ duplicate, dupe_before, 'duplicateof'))
734 self.duplicateof = duplicate_of
735 except LaunchpadValidationError, validation_error:
736 raise InvalidDuplicateValue(validation_error)
737
738=== modified file 'lib/lp/bugs/scripts/bugimport.py'
739--- lib/lp/bugs/scripts/bugimport.py 2010-07-22 12:17:41 +0000
740+++ lib/lp/bugs/scripts/bugimport.py 2010-08-06 18:42:48 +0000
741@@ -466,7 +466,7 @@
742 self.logger.info(
743 'Marking bug %d as duplicate of bug %d',
744 other_bug.id, bug.id)
745- other_bug.duplicateof = bug
746+ other_bug.markAsDuplicate(bug)
747 del self.pending_duplicates[bug_id]
748 # Process this bug as a duplicate
749 if duplicateof is not None:
750@@ -478,7 +478,7 @@
751 self.logger.info(
752 'Marking bug %d as duplicate of bug %d',
753 bug.id, other_bug.id)
754- bug.duplicateof = other_bug
755+ bug.markAsDuplicate(other_bug)
756 else:
757 self.pending_duplicates.setdefault(
758 duplicateof, []).append(bug.id)
759
760=== modified file 'lib/lp/bugs/scripts/bugzilla.py'
761--- lib/lp/bugs/scripts/bugzilla.py 2010-08-02 02:13:52 +0000
762+++ lib/lp/bugs/scripts/bugzilla.py 2010-08-06 18:42:48 +0000
763@@ -638,7 +638,7 @@
764 lpdupe.duplicateof is None):
765 logger.info('Marking %d as a duplicate of %d',
766 lpdupe.id, lpdupe_of.id)
767- lpdupe.duplicateof = lpdupe_of
768+ lpdupe.markAsDuplicate(lpdupe_of)
769 trans.commit()
770
771 def importBugs(self, trans, product=None, component=None, status=None):
772
773=== removed directory 'lib/lp/bugs/stories/duplicate-bug-handling'
774=== removed file 'lib/lp/bugs/stories/duplicate-bug-handling/10-mark-bug-as-duplicate.txt'
775--- lib/lp/bugs/stories/duplicate-bug-handling/10-mark-bug-as-duplicate.txt 2010-07-22 19:59:00 +0000
776+++ lib/lp/bugs/stories/duplicate-bug-handling/10-mark-bug-as-duplicate.txt 1970-01-01 00:00:00 +0000
777@@ -1,48 +0,0 @@
778-First, visit the +duplicate page on the bug you're looking at:
779-
780- >>> print http(r"""
781- ... GET /debian/+source/mozilla-firefox/+bug/2/+duplicate HTTP/1.1
782- ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
783- ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
784- ... """)
785- HTTP/1.1 200 Ok
786- ...
787-
788-Now, let's go ahead and mark bug 2 as a duplicate of bug 1.
789-
790- >>> print http(r"""
791- ... POST /debian/+source/mozilla-firefox/+bug/2/+duplicate HTTP/1.1
792- ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
793- ... Content-Length: 309
794- ... Content-Type: multipart/form-data; boundary=---------------------------17976867087135254391306687757
795- ...
796- ... -----------------------------17976867087135254391306687757
797- ... Content-Disposition: form-data; name="field.duplicateof"
798- ...
799- ... 1
800- ... -----------------------------17976867087135254391306687757
801- ... Content-Disposition: form-data; name="field.actions.change"
802- ...
803- ... Change
804- ... -----------------------------17976867087135254391306687757--
805- ... """)
806- HTTP/1.1 303 See Other
807- ...
808- Content-Length: 0
809- ...
810- Location: http://.../debian/+source/mozilla-firefox/+bug/2
811- ...
812-
813-When the bug is marked as a duplicate, a notification was generated, to
814-tell bug 2's subscribers that their bug is a dupe of bug 1:
815-
816- >>> from canonical.launchpad.database import BugNotification
817- >>> BugNotification.select().count()
818- 1
819-
820- >>> notification = BugNotification.select(orderBy='-id')[0]
821- >>> notification.bug.id
822- 2
823- >>> print notification.message.text_contents
824- ** This bug has been marked a duplicate of bug 1
825- ...
826
827=== removed file 'lib/lp/bugs/stories/duplicate-bug-handling/20-show-bug-is-duplicate.txt'
828--- lib/lp/bugs/stories/duplicate-bug-handling/20-show-bug-is-duplicate.txt 2010-07-22 19:59:00 +0000
829+++ lib/lp/bugs/stories/duplicate-bug-handling/20-show-bug-is-duplicate.txt 1970-01-01 00:00:00 +0000
830@@ -1,41 +0,0 @@
831-When a bug is marked as a duplicate of another bug, the duplicate bug's
832-page shows this prominently, and the comment field is accompanied by an extra
833-warning that it's a duplicate.
834-
835- >>> user_browser.open(
836- ... 'http://launchpad.dev/debian/+source/mozilla-firefox/+bug/2')
837- >>> print find_tag_by_id(
838- ... user_browser.contents, 'duplicate-of').renderContents()
839- bug #1...
840- >>> for message in find_tags_by_class(user_browser.contents, 'message'):
841- ... print extract_text(message)
842- Remember, this bug report is a duplicate of bug #1. Comment here only if...
843-
844-The "Affects" lines are also not expandable, preventing people from changing
845-the bug's status to no effect.
846-
847- >>> affects_table = find_tags_by_class(user_browser.contents, 'listing')[0]
848- >>> "toggleFormVisibility('task" in affects_table
849- False
850-
851-If the bug is no longer marked as a duplicate,
852-
853- >>> user_browser.getLink(id="change_duplicate_bug").click()
854- >>> user_browser.url
855- 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2/+duplicate'
856- >>> user_browser.getControl(name='field.duplicateof').value = ''
857- >>> user_browser.getControl('Change').click()
858- >>> user_browser.url
859- 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2'
860-
861-the duplicate notifications disappear,
862-
863- >>> find_tags_by_class(user_browser.contents, 'message')
864- []
865-
866-and the "Affects" lines become expandable once more.
867-
868- >>> affects_table = find_tags_by_class(user_browser.contents, 'listing')[0]
869- >>> print affects_table
870- <...
871- ...toggleFormVisibility('task...
872
873=== removed file 'lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt'
874--- lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt 2010-07-22 19:59:00 +0000
875+++ lib/lp/bugs/stories/duplicate-bug-handling/xx-mark-duplicate-validation.txt 1970-01-01 00:00:00 +0000
876@@ -1,76 +0,0 @@
877-=================================
878-Mark as duplicate form validation
879-=================================
880-
881- >>> bg3 = 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/3'
882- >>> bug_3_dup_url = '%s/+duplicate' % bg3
883- >>> user_browser.open(bug_3_dup_url)
884-
885-Tests the marking of a bug that is a duplicate of a duplicate.
886-
887- >>> user_browser.getControl('Duplicate Of').value = '6'
888- >>> user_browser.getControl('Change').click()
889- >>> print user_browser.contents
890- <...
891- <p class="error message">There is 1 error.</p>
892- ...
893- ...already a duplicate...
894-
895-Tests the marking of a bug that is a duplicate of itself.
896-
897- >>> user_browser.getControl('Duplicate Of').value = '3'
898- >>> user_browser.getControl('Change').click()
899- >>> print user_browser.contents
900- <...
901- <p class="error message">There is 1 error.</p>
902- ...
903- ...can't mark a bug as a duplicate of itself...
904- ...
905-
906-Tests the marking of a bug that is already marked as duplicate with
907-the same value. In this case, nothing should happen, since nothing
908-changed.
909-
910- >>> user_browser.open(
911- ... 'http://bugs.launchpad.dev/firefox/+bug/6/+duplicate')
912- >>> user_browser.getControl('Duplicate Of').value
913- '5'
914- >>> user_browser.getControl('Change').click()
915- >>> user_browser.url
916- 'http://bugs.launchpad.dev/firefox/+bug/6'
917-
918-Tests the input of data that is not a valid value.
919-
920- >>> user_browser.open(
921- ... 'http://bugs.launchpad.dev/firefox/+bug/6/+duplicate')
922- >>> user_browser.getControl('Duplicate Of').value = 'xajskd'
923- >>> user_browser.getControl('Change').click()
924- >>> print user_browser.contents
925- <...
926- ...Not a valid bug number or nickname...
927- ...
928-
929-Tests using a bug nickname as the duplicate value
930-
931- >>> user_browser.open(
932- ... 'http://bugs.launchpad.dev/evolution/+bug/7/+duplicate')
933- >>> user_browser.getControl('Duplicate Of').value = 'blackhole'
934- >>> user_browser.getControl('Change').click()
935- >>> user_browser.url
936- 'http://bugs.launchpad.dev/evolution/+bug/7'
937-
938-Marking a bugX as a duplicate of bugY is not allowed if bugX has dupes.
939-
940- >>> user_browser.open(
941- ... 'http://bugs.launchpad.dev/firefox/+bug/5/+duplicate')
942- >>> user_browser.getControl('Duplicate Of').value = '4'
943- >>> user_browser.getControl('Change').click()
944- >>> print user_browser.contents
945- <...
946- ...There are other bugs already...
947- ...
948-
949- >>> from canonical.launchpad.database import Bug
950- >>> Bug.get(5).duplicateof is None
951- True
952-
953
954=== modified file 'lib/lp/bugs/templates/bug-portlet-actions.pt'
955--- lib/lp/bugs/templates/bug-portlet-actions.pt 2010-07-22 16:27:12 +0000
956+++ lib/lp/bugs/templates/bug-portlet-actions.pt 2010-08-06 18:42:48 +0000
957@@ -11,10 +11,11 @@
958 tal:define="link context_menu/markduplicate"
959 tal:condition="python: link.enabled and not
960 context.duplicateof"
961+ class="sprite bug-dupe"
962 >
963 <a
964 tal:attributes="href link/path"
965- class="menu-link-mark-dupe sprite bug-dupe">Mark as duplicate</a>
966+ class="menu-link-mark-dupe">Mark as duplicate</a>
967 </span>
968 <tal:block
969 tal:condition="context/duplicates"
970@@ -31,8 +32,7 @@
971 id="change_duplicate_bug"
972 title="Edit or remove linked duplicate bug"
973 class="sprite edit"
974- tal:attributes="href link/url"></a>
975- <span id="mark-duplicate-text">Duplicate of
976+ tal:attributes="href link/url"></a><span id="mark-duplicate-text">Duplicate of
977 <a
978 tal:condition="duplicateof/required:launchpad.View"
979 tal:attributes="href duplicateof/fmt:url; title
980
981=== modified file 'lib/lp/bugs/tests/bug.py'
982--- lib/lp/bugs/tests/bug.py 2010-07-22 12:17:41 +0000
983+++ lib/lp/bugs/tests/bug.py 2010-08-06 18:42:48 +0000
984@@ -196,7 +196,8 @@
985 params = CreateBugParams(
986 owner=no_priv, title=title, comment='Something is broken.')
987 bug = target.createBug(params)
988- bug.duplicateof = duplicateof
989+ if duplicateof is not None:
990+ bug.markAsDuplicate(duplicateof)
991 sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
992 if with_message is True:
993 bug.newMessage(
994
995=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
996--- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-07-22 12:17:41 +0000
997+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-08-06 18:42:48 +0000
998@@ -926,32 +926,6 @@
999 duplicate itself.
1000 ...
1001
1002-If the specified bug has other bugs marked as a duplicate of it, an
1003-error message is sent telling you this.
1004-
1005- >>> bug_four = getUtility(IBugSet).get(4)
1006- >>> bug_four.duplicateof.id
1007- 2
1008-
1009- >>> from canonical.launchpad.ftests import syncUpdate
1010- >>> syncUpdate(bug_four)
1011- >>> submit_commands(bug_four.duplicateof, 'duplicate 1')
1012- >>> bug_four.duplicateof.id
1013- 2
1014-
1015- >>> print_latest_email()
1016- Subject: Submit Request Failure
1017- To: test@canonical.com
1018- <BLANKLINE>
1019- ...
1020- Failing command:
1021- duplicate 1
1022- ...
1023- There are other bugs already marked as duplicates of Bug 2.
1024- These bugs should be changed to be duplicates of another bug
1025- if you are certain you would like to perform this change.
1026- ...
1027-
1028
1029 === cve $cve ===
1030
1031@@ -1916,7 +1890,7 @@
1032 ... Subject: A bug with no affects
1033 ...
1034 ... private yes
1035- ... duplicate 1
1036+ ... unsubscribe
1037 ... cve 1999-8979
1038 ... """
1039
1040
1041=== modified file 'lib/lp/bugs/tests/bugtarget-bugcount.txt'
1042--- lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-07-22 12:17:41 +0000
1043+++ lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-08-06 18:42:48 +0000
1044@@ -129,7 +129,7 @@
1045 ... bugtarget, "Another Test Bug", status=BugTaskStatus.NEW)
1046 >>> Store.of(another_bug).flush()
1047 >>> old_counts = bugtarget.getBugCounts(None)
1048- >>> another_bug.duplicateof = bug
1049+ >>> another_bug.markAsDuplicate(bug)
1050 >>> syncUpdate(another_bug)
1051 >>> new_counts = bugtarget.getBugCounts(None)
1052 >>> print_count_difference(
1053
1054=== modified file 'lib/lp/bugs/tests/test_bugchanges.py'
1055--- lib/lp/bugs/tests/test_bugchanges.py 2010-07-22 12:17:41 +0000
1056+++ lib/lp/bugs/tests/test_bugchanges.py 2010-08-06 18:42:48 +0000
1057@@ -90,7 +90,10 @@
1058 :return: The value of `attribute` before modification.
1059 """
1060 obj_before_modification = Snapshot(obj, providing=providedBy(obj))
1061- setattr(obj, attribute, new_value)
1062+ if attribute == 'duplicateof':
1063+ obj.markAsDuplicate(new_value)
1064+ else:
1065+ setattr(obj, attribute, new_value)
1066 notify(ObjectModifiedEvent(
1067 obj, obj_before_modification, [attribute], self.user))
1068
1069@@ -1304,7 +1307,7 @@
1070 duplicate_bug = self.factory.makeBug()
1071 duplicate_bug_recipients = duplicate_bug.getBugNotificationRecipients(
1072 level=BugNotificationLevel.METADATA).getRecipients()
1073- duplicate_bug.duplicateof = self.bug
1074+ duplicate_bug.markAsDuplicate(self.bug)
1075 self.saveOldChanges(duplicate_bug)
1076 self.changeAttribute(duplicate_bug, 'duplicateof', None)
1077
1078@@ -1335,7 +1338,7 @@
1079 bug_two = self.factory.makeBug()
1080 bug_recipients = self.bug.getBugNotificationRecipients(
1081 level=BugNotificationLevel.METADATA).getRecipients()
1082- self.bug.duplicateof = bug_one
1083+ self.bug.markAsDuplicate(bug_one)
1084 self.saveOldChanges()
1085 self.changeAttribute(self.bug, 'duplicateof', bug_two)
1086
1087
1088=== modified file 'lib/lp/bugs/tests/test_bugnotification.py'
1089--- lib/lp/bugs/tests/test_bugnotification.py 2010-07-22 12:17:41 +0000
1090+++ lib/lp/bugs/tests/test_bugnotification.py 2010-08-06 18:42:48 +0000
1091@@ -155,7 +155,7 @@
1092 user='test@canonical.com')
1093 self.bug = self.factory.makeBug()
1094 self.dupe_bug = self.factory.makeBug()
1095- self.dupe_bug.duplicateof = self.bug
1096+ self.dupe_bug.markAsDuplicate(self.bug)
1097 self.dupe_subscribers = set(
1098 self.dupe_bug.getDirectSubscribers() +
1099 self.dupe_bug.getIndirectSubscribers())
1100
1101=== added file 'lib/lp/bugs/tests/test_duplicate_handling.py'
1102--- lib/lp/bugs/tests/test_duplicate_handling.py 1970-01-01 00:00:00 +0000
1103+++ lib/lp/bugs/tests/test_duplicate_handling.py 2010-08-06 18:42:48 +0000
1104@@ -0,0 +1,102 @@
1105+# Copyright 2010 Canonical Ltd. This software is licensed under the
1106+# GNU Affero General Public License version 3 (see the file LICENSE).
1107+
1108+"""Tests for bug duplicate validation."""
1109+
1110+from textwrap import dedent
1111+import unittest
1112+
1113+from zope.security.interfaces import ForbiddenAttribute
1114+
1115+from canonical.testing import DatabaseFunctionalLayer
1116+
1117+from lp.bugs.interfaces.bug import InvalidDuplicateValue
1118+from lp.testing import TestCaseWithFactory
1119+
1120+
1121+class TestDuplicateAttributes(TestCaseWithFactory):
1122+ """Test bug attributes related to duplicate handling."""
1123+
1124+ layer = DatabaseFunctionalLayer
1125+
1126+ def setUp(self):
1127+ super(TestDuplicateAttributes, self).setUp(user='test@canonical.com')
1128+
1129+ def setDuplicateofDirectly(self, bug, duplicateof):
1130+ """Helper method to set duplicateof directly."""
1131+ bug.duplicateof = duplicateof
1132+
1133+ def test_duplicateof_readonly(self):
1134+ # Test that no one can set duplicateof directly.
1135+ bug = self.factory.makeBug()
1136+ dupe_bug = self.factory.makeBug()
1137+ self.assertRaises(
1138+ ForbiddenAttribute, self.setDuplicateofDirectly, bug, dupe_bug)
1139+
1140+
1141+class TestMarkDuplicateValidation(TestCaseWithFactory):
1142+ """Test for validation around marking bug duplicates."""
1143+
1144+ layer = DatabaseFunctionalLayer
1145+
1146+ def setUp(self):
1147+ super(TestMarkDuplicateValidation, self).setUp(
1148+ user='test@canonical.com')
1149+ self.bug = self.factory.makeBug()
1150+ self.dupe_bug = self.factory.makeBug()
1151+ self.dupe_bug.markAsDuplicate(self.bug)
1152+ self.possible_dupe = self.factory.makeBug()
1153+
1154+ def assertDuplicateError(self, bug, duplicateof, msg):
1155+ try:
1156+ bug.markAsDuplicate(duplicateof)
1157+ except InvalidDuplicateValue, err:
1158+ self.assertEqual(str(err), msg)
1159+
1160+ def test_error_on_duplicate_to_duplicate(self):
1161+ # Test that a bug cannot be marked a duplicate of
1162+ # a bug that is already itself a duplicate.
1163+ msg = dedent(u"""
1164+ Bug %s is already a duplicate of bug %s. You
1165+ can only mark a bug report as duplicate of one that
1166+ isn't a duplicate itself.
1167+ """ % (
1168+ self.dupe_bug.id, self.dupe_bug.duplicateof.id))
1169+ self.assertDuplicateError(
1170+ self.possible_dupe, self.dupe_bug, msg)
1171+
1172+ def test_error_duplicate_to_itself(self):
1173+ # Test that a bug cannot be marked its own duplicate
1174+ msg = dedent(u"""
1175+ You can't mark a bug as a duplicate of itself.""")
1176+ self.assertDuplicateError(self.bug, self.bug, msg)
1177+
1178+
1179+class TestMoveDuplicates(TestCaseWithFactory):
1180+ """Test duplicates are moved when master bug is marked a duplicate."""
1181+
1182+ layer = DatabaseFunctionalLayer
1183+
1184+ def setUp(self):
1185+ super(TestMoveDuplicates, self).setUp(user='test@canonical.com')
1186+
1187+ def test_duplicates_are_moved(self):
1188+ # Confirm that a bug with two duplicates can be marked
1189+ # a duplicate of a new bug and that the duplicates will
1190+ # be re-marked as duplicates of the new bug, too.
1191+ bug = self.factory.makeBug()
1192+ dupe_one = self.factory.makeBug()
1193+ dupe_two = self.factory.makeBug()
1194+ dupe_one.markAsDuplicate(bug)
1195+ dupe_two.markAsDuplicate(bug)
1196+ self.assertEqual(dupe_one.duplicateof, bug)
1197+ self.assertEqual(dupe_two.duplicateof, bug)
1198+ new_bug = self.factory.makeBug()
1199+ bug.markAsDuplicate(new_bug)
1200+ self.assertEqual(bug.duplicateof, new_bug)
1201+ self.assertEqual(dupe_one.duplicateof, new_bug)
1202+ self.assertEqual(dupe_two.duplicateof, new_bug)
1203+
1204+
1205+def test_suite():
1206+ return unittest.TestLoader().loadTestsFromName(__name__)
1207
1208=== modified file 'lib/lp/bugs/windmill/tests/test_mark_duplicate.py'
1209--- lib/lp/bugs/windmill/tests/test_mark_duplicate.py 2010-07-26 16:00:01 +0000
1210+++ lib/lp/bugs/windmill/tests/test_mark_duplicate.py 2010-08-06 18:42:48 +0000
1211@@ -44,10 +44,13 @@
1212 client.waits.forElement(
1213 xpath=MAIN_FORM_ELEMENT, timeout=constants.FOR_ELEMENT)
1214
1215- # Initially the form overlay is hidden
1216+ # Initially the form overlay is hidden...
1217 client.asserts.assertElemJS(
1218 xpath=MAIN_FORM_ELEMENT, js=FORM_NOT_VISIBLE)
1219
1220+ # ...and there is an expandable form for editing bug status, etc.
1221+ client.asserts.assertNode(classname='bug-status-expand')
1222+
1223 # Clicking on the mark duplicate link brings up the formoverlay.
1224 # Entering 1 as the duplicate ID changes the duplicate text.
1225 client.click(classname=u'menu-link-mark-dupe')
1226@@ -66,7 +69,7 @@
1227 id='warning-comment-on-duplicate', timeout=constants.FOR_ELEMENT)
1228
1229 # The duplicate can be cleared:
1230- client.click(classname=u'menu-link-mark-dupe')
1231+ client.click(id=u'mark-duplicate-text')
1232 client.type(text=u'', id=u'field.duplicateof')
1233 client.click(xpath=CHANGE_BUTTON)
1234 client.waits.forElement(
1235@@ -77,7 +80,7 @@
1236 client.asserts.assertNotNode(id='warning-comment-on-duplicate')
1237
1238 # Entering a false bug number results in input validation errors
1239- client.click(classname=u'menu-link-mark-dupe')
1240+ client.click(id=u'mark-duplicate-text')
1241 client.type(text=u'123', id=u'field.duplicateof')
1242 client.click(xpath=CHANGE_BUTTON)
1243 error_xpath = (
1244@@ -105,6 +108,14 @@
1245 xpath=u"//h1[@id='bug-title']/span[1]",
1246 validator=u'Firefox does not support SVG')
1247
1248+ # If someone wants to set the master to dupe another bug, there
1249+ # is a warning in the dupe widget about this bug having its own
1250+ # duplicates.
1251+ client.click(classname='menu-link-mark-dupe')
1252+ client.asserts.assertTextIn(
1253+ classname='large-warning', validator=u'This bug has duplicates',
1254+ timeout=constants.FOR_ELEMENT)
1255+
1256 # When we go back to the page for the duplicate bug...
1257 client.open(url=u'http://bugs.launchpad.dev:8085/bugs/15')
1258 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
1259@@ -115,6 +126,9 @@
1260 # as the one we saw before.
1261 client.asserts.assertNode(id='warning-comment-on-duplicate')
1262
1263+ # Duplicate pages also do not have the expandable form on them.
1264+ client.asserts.assertNotNode(classname='bug-status-expand')
1265+
1266 # Once we remove the duplicate mark...
1267 client.click(id=u'change_duplicate_bug')
1268 client.type(text=u'', id=u'field.duplicateof')
1269
1270=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
1271--- lib/lp/registry/browser/tests/person-views.txt 2010-07-22 12:17:41 +0000
1272+++ lib/lp/registry/browser/tests/person-views.txt 2010-08-06 18:42:48 +0000
1273@@ -546,7 +546,7 @@
1274
1275 But duplicate bugs are never displayed.
1276
1277- >>> another_bug.duplicateof = bug
1278+ >>> another_bug.markAsDuplicate(bug)
1279
1280 # Create a new view because we're testing some cached properties.
1281
1282
1283=== modified file 'versions.cfg'
1284--- versions.cfg 2010-08-05 09:57:43 +0000
1285+++ versions.cfg 2010-08-06 18:42:48 +0000
1286@@ -64,9 +64,7 @@
1287 simplesettings = 0.4
1288 SimpleTal = 4.1
1289 sourcecodegen = 0.6.9
1290-# This is Storm 0.15 with r342 cherry-picked which fixes a memory leak
1291-# important for message sharing migration script.
1292-storm = 0.15danilo-storm-launchpad-r342
1293+storm = 0.17
1294 # Has the LessThan matcher.
1295 testtools = 0.9.6dev91
1296 transaction = 1.0.0