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