Merge lp:~deryck/launchpad/update-storm-17-add-back-dupe-changes into lp:launchpad
- update-storm-17-add-back-dupe-changes
- Merge into devel
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 |
Related bugs: |
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.markAsDupli
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:/
https:/
And I have approval from Robert to change the version of Storm here:
https:/
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
Brad Crittenden (bac) : | # |
Preview Diff
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 |