Merge lp:~stevenk/launchpad/destroy-cause-unknownrecipienterror into lp:launchpad

Proposed by Steve Kowalik
Status: Merged
Approved by: Steve Kowalik
Approved revision: no longer in the source branch.
Merged at revision: 15989
Proposed branch: lp:~stevenk/launchpad/destroy-cause-unknownrecipienterror
Merge into: lp:launchpad
Diff against target: 331 lines (+65/-67)
2 files modified
lib/lp/bugs/browser/tests/test_bugtask.py (+57/-67)
lib/lp/bugs/subscribers/bug.py (+8/-0)
To merge this branch: bzr merge lp:~stevenk/launchpad/destroy-cause-unknownrecipienterror
Reviewer Review Type Date Requested Status
Ian Booth (community) Approve
Review via email: mp+125397@code.launchpad.net

Commit message

Ignore new subscribers that are not in the recipient list when filtering for the assignee in the depths of the bug zope subscribers.

Description of the change

When changing assignee, we want to filter it out of the "normal" notification mail that other subscribers recieve. To this end in the zope subscriber function receives a list of subscribers that will be notified by set difference using the two objects on the event. We then trawl through this list looking for the assignee so we can remove them.

So, this is fine. In the case that we only change the assignee, *or* all new subscribers are in the recipients list we build. Where this house of cards completely falls down is if we have a structural subscriber on a milestone with LIFECYCLE set. (LIFECYCLE subscribers only get notified on a new bug or a bug being closed.) When the assignee is changed and the milestone is set in *one action* using BugTask:+editstatus, we go through the list of new subscribers, and then raise an UnknownRecipientError because the structural subscriber isn't in the recipient list, and then OOPS.

Yay for layering violations. I've worked around this by checking if the person is in the recipients list first, and added a verbose comment to that effect.

I have also gone through test_bugtask.py and removed a bunch of excess whitespace to cause this branch to be a net LoC loss. Also, correct a thinko that I noticed.

To post a comment you must log in.
Revision history for this message
Ian Booth (wallyworld) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
2--- lib/lp/bugs/browser/tests/test_bugtask.py 2012-08-30 06:18:38 +0000
3+++ lib/lp/bugs/browser/tests/test_bugtask.py 2012-09-20 04:52:24 +0000
4@@ -41,6 +41,7 @@
5 BugTaskListingItem,
6 BugTasksAndNominationsView,
7 )
8+from lp.bugs.enums import BugNotificationLevel
9 from lp.bugs.feed.bug import (
10 BugTargetBugsFeed,
11 PersonBugsFeed,
12@@ -277,6 +278,30 @@
13 self.assertEqual(1, len(view.errors))
14 self.assertEqual(product, bug.default_bugtask.target)
15
16+ def test_changing_milestone_and_assignee_with_lifecycle(self):
17+ # Changing the milestone and assignee of a bugtask when the milestone
18+ # has a LIFECYCLE structsub is fine. Also see bug 1036882.
19+ subscriber = self.factory.makePerson()
20+ product = self.factory.makeProduct(official_malone=True)
21+ milestone = self.factory.makeMilestone(product=product)
22+ with person_logged_in(subscriber):
23+ structsub = milestone.addBugSubscription(subscriber, subscriber)
24+ structsub.bug_filters[0].bug_notification_level = (
25+ BugNotificationLevel.LIFECYCLE)
26+ bug = self.factory.makeBug(target=product)
27+ with person_logged_in(product.owner):
28+ form_data = {
29+ '%s.milestone' % product.name: milestone.id,
30+ '%s.assignee.option' % product.name:
31+ '%s.assignee.assign_to_me' % product.name,
32+ '%s.assignee' % product.name: product.owner.name,
33+ '%s.actions.save' % product.name: 'Save Changes',
34+ }
35+ create_initialized_view(
36+ bug.default_bugtask, name='+editstatus', form=form_data)
37+ self.assertEqual(product.owner, bug.default_bugtask.assignee)
38+ self.assertEqual(milestone, bug.default_bugtask.milestone)
39+
40 def test_bugtag_urls_are_encoded(self):
41 # The link to bug tags are encoded to protect against special chars.
42 product = self.factory.makeProduct(name='foobar')
43@@ -294,8 +319,7 @@
44 def test_information_type(self):
45 owner = self.factory.makePerson()
46 bug = self.factory.makeBug(
47- owner=owner,
48- information_type=InformationType.USERDATA)
49+ owner=owner, information_type=InformationType.USERDATA)
50 login_person(owner)
51 bugtask = self.factory.makeBugTask(bug=bug)
52 view = create_initialized_view(bugtask, name="+index")
53@@ -432,17 +456,13 @@
54 self.useFixture(FeatureFixture(
55 {'bugs.affected_count_includes_dupes.disabled': ''}))
56 self.makeDuplicate()
57- self.assertEqual(
58- 3, self.view.total_users_affected_count)
59+ self.assertEqual(3, self.view.total_users_affected_count)
60 self.assertEqual(
61 "This bug affects 3 people. Does this bug affect you?",
62 self.view.affected_statement)
63 self.assertEqual(
64- "This bug affects 3 people",
65- self.view.anon_affected_statement)
66- self.assertEqual(
67- self.view.other_users_affected_count,
68- 3)
69+ "This bug affects 3 people", self.view.anon_affected_statement)
70+ self.assertEqual(self.view.other_users_affected_count, 3)
71
72 def test_counts_affected_by_duplicate(self):
73 self.useFixture(FeatureFixture(
74@@ -455,11 +475,8 @@
75 "This bug affects 3 people. Does this bug affect you?",
76 self.view.affected_statement)
77 self.assertEqual(
78- "This bug affects 4 people",
79- self.view.anon_affected_statement)
80- self.assertEqual(
81- self.view.other_users_affected_count,
82- 3)
83+ "This bug affects 4 people", self.view.anon_affected_statement)
84+ self.assertEqual(self.view.other_users_affected_count, 3)
85
86 def test_counts_affected_by_master(self):
87 self.useFixture(FeatureFixture(
88@@ -472,11 +489,8 @@
89 "This bug affects you and 3 other people",
90 self.view.affected_statement)
91 self.assertEqual(
92- "This bug affects 4 people",
93- self.view.anon_affected_statement)
94- self.assertEqual(
95- self.view.other_users_affected_count,
96- 3)
97+ "This bug affects 4 people", self.view.anon_affected_statement)
98+ self.assertEqual(self.view.other_users_affected_count, 3)
99
100 def test_counts_affected_by_duplicate_not_by_master(self):
101 self.useFixture(FeatureFixture(
102@@ -493,11 +507,8 @@
103 # either 3 or 4 people. However at the moment the "No" answer on the
104 # master is more authoritative than the "Yes" on the dupe.
105 self.assertEqual(
106- "This bug affects 3 people",
107- self.view.anon_affected_statement)
108- self.assertEqual(
109- self.view.other_users_affected_count,
110- 3)
111+ "This bug affects 3 people", self.view.anon_affected_statement)
112+ self.assertEqual(self.view.other_users_affected_count, 3)
113
114 def test_total_users_affected_count_without_dupes(self):
115 self.useFixture(FeatureFixture(
116@@ -505,25 +516,20 @@
117 self.makeDuplicate()
118 self.refresh()
119 # Does not count the two users of bug2, so just 1.
120- self.assertEqual(
121- 1, self.view.total_users_affected_count)
122+ self.assertEqual(1, self.view.total_users_affected_count)
123 self.assertEqual(
124 "This bug affects 1 person. Does this bug affect you?",
125 self.view.affected_statement)
126 self.assertEqual(
127- "This bug affects 1 person",
128- self.view.anon_affected_statement)
129- self.assertEqual(
130- 1,
131- self.view.other_users_affected_count)
132+ "This bug affects 1 person", self.view.anon_affected_statement)
133+ self.assertEqual(1, self.view.other_users_affected_count)
134
135 def test_affected_statement_no_one_affected(self):
136 self.bug.markUserAffected(self.bug.owner, False)
137 self.failUnlessEqual(
138 0, self.view.other_users_affected_count)
139 self.failUnlessEqual(
140- "Does this bug affect you?",
141- self.view.affected_statement)
142+ "Does this bug affect you?", self.view.affected_statement)
143
144 def test_affected_statement_only_you(self):
145 self.view.context.markUserAffected(self.view.user, True)
146@@ -532,8 +538,7 @@
147 self.failUnlessEqual(
148 0, self.view.other_users_affected_count)
149 self.failUnlessEqual(
150- "This bug affects you",
151- self.view.affected_statement)
152+ "This bug affects you", self.view.affected_statement)
153
154 def test_affected_statement_only_not_you(self):
155 self.view.context.markUserAffected(self.view.user, False)
156@@ -542,8 +547,7 @@
157 self.failUnlessEqual(
158 0, self.view.other_users_affected_count)
159 self.failUnlessEqual(
160- "This bug doesn't affect you",
161- self.view.affected_statement)
162+ "This bug doesn't affect you", self.view.affected_statement)
163
164 def test_affected_statement_1_person_not_you(self):
165 self.assertIs(None, self.bug.isUserAffected(self.view.user))
166@@ -565,8 +569,7 @@
167 def test_affected_statement_1_person_and_not_you(self):
168 self.view.context.markUserAffected(self.view.user, False)
169 self.failIf(self.bug.isUserAffected(self.view.user))
170- self.failUnlessEqual(
171- 1, self.view.other_users_affected_count)
172+ self.failUnlessEqual(1, self.view.other_users_affected_count)
173 self.failUnlessEqual(
174 "This bug affects 1 person, but not you",
175 self.view.affected_statement)
176@@ -575,8 +578,7 @@
177 self.assertIs(None, self.bug.isUserAffected(self.view.user))
178 other_user = self.factory.makePerson()
179 self.view.context.markUserAffected(other_user, True)
180- self.failUnlessEqual(
181- 2, self.view.other_users_affected_count)
182+ self.failUnlessEqual(2, self.view.other_users_affected_count)
183 self.failUnlessEqual(
184 "This bug affects 2 people. Does this bug affect you?",
185 self.view.affected_statement)
186@@ -586,8 +588,7 @@
187 self.failUnless(self.bug.isUserAffected(self.view.user))
188 other_user = self.factory.makePerson()
189 self.view.context.markUserAffected(other_user, True)
190- self.failUnlessEqual(
191- 2, self.view.other_users_affected_count)
192+ self.failUnlessEqual(2, self.view.other_users_affected_count)
193 self.failUnlessEqual(
194 "This bug affects you and 2 other people",
195 self.view.affected_statement)
196@@ -597,8 +598,7 @@
197 self.failIf(self.bug.isUserAffected(self.view.user))
198 other_user = self.factory.makePerson()
199 self.view.context.markUserAffected(other_user, True)
200- self.failUnlessEqual(
201- 2, self.view.other_users_affected_count)
202+ self.failUnlessEqual(2, self.view.other_users_affected_count)
203 self.failUnlessEqual(
204 "This bug affects 2 people, but not you",
205 self.view.affected_statement)
206@@ -611,15 +611,13 @@
207 def test_anon_affected_statement_1_user_affected(self):
208 self.failUnlessEqual(1, self.bug.users_affected_count)
209 self.failUnlessEqual(
210- "This bug affects 1 person",
211- self.view.anon_affected_statement)
212+ "This bug affects 1 person", self.view.anon_affected_statement)
213
214 def test_anon_affected_statement_2_users_affected(self):
215 self.view.context.markUserAffected(self.view.user, True)
216 self.failUnlessEqual(2, self.bug.users_affected_count)
217 self.failUnlessEqual(
218- "This bug affects 2 people",
219- self.view.anon_affected_statement)
220+ "This bug affects 2 people", self.view.anon_affected_statement)
221
222 def test_getTargetLinkTitle_product(self):
223 # The target link title is always none for products.
224@@ -690,8 +688,7 @@
225 bug_task = self.factory.makeBugTask(
226 bug=self.bug, target=target, publish=False)
227 self.view.initialize()
228- self.assertTrue(
229- target in self.view.target_releases.keys())
230+ self.assertTrue(target in self.view.target_releases.keys())
231 self.assertEqual(
232 'Latest release: 2.0, uploaded to universe on '
233 '2008-07-18 10:20:30+00:00 by Tim (tim), maintained by Jim (jim)',
234@@ -714,8 +711,7 @@
235 bug_task = self.factory.makeBugTask(
236 bug=self.bug, target=target, publish=False)
237 self.view.initialize()
238- self.assertTrue(
239- target in self.view.target_releases.keys())
240+ self.assertTrue(target in self.view.target_releases.keys())
241 self.assertEqual(
242 'Latest release: 2.0, uploaded to universe on '
243 '2008-07-18 10:20:30+00:00 by Tim (tim), maintained by Jim (jim)',
244@@ -803,8 +799,7 @@
245 product_foo = self.factory.makeProduct(name="foo")
246 foo_bug = self.factory.makeBug(target=product_foo)
247 assignee = self.factory.makeTeam(
248- name="assignee",
249- visibility=PersonVisibility.PRIVATE)
250+ name="assignee", visibility=PersonVisibility.PRIVATE)
251 foo_bug.default_bugtask.transitionToAssignee(assignee)
252
253 # Render the view.
254@@ -1130,7 +1125,7 @@
255 target=distro, owner=owner,
256 information_type=InformationType.PRIVATESECURITY)
257 # XXX wgrant 2012-08-30 bug=1041002: Distributions don't have
258- # sharing policies yet, so it isn't possible legitimately create
259+ # sharing policies yet, so it isn't possible to legitimately create
260 # a Proprietary distro bug.
261 removeSecurityProxy(bug).information_type = (
262 InformationType.PROPRIETARY)
263@@ -1152,16 +1147,13 @@
264
265 def setUp(self):
266 super(TestBugTaskEditViewStatusField, self).setUp()
267- product_owner = self.factory.makePerson(name='product-owner')
268 bug_supervisor = self.factory.makePerson(name='bug-supervisor')
269- product = self.factory.makeProduct(
270- owner=product_owner, bug_supervisor=bug_supervisor)
271+ product = self.factory.makeProduct(bug_supervisor=bug_supervisor)
272 self.bug = self.factory.makeBug(target=product)
273
274 def getWidgetOptionTitles(self, widget):
275 """Return the titles of options of the given choice widget."""
276- return [
277- item.value.title for item in widget.field.vocabulary]
278+ return [item.value.title for item in widget.field.vocabulary]
279
280 def test_status_field_items_for_anonymous(self):
281 # Anonymous users see only the current value.
282@@ -1622,8 +1614,8 @@
283 def setUp(self):
284 super(TestProjectGroupBugs, self).setUp()
285 self.owner = self.factory.makePerson(name='bob')
286- self.target = self.factory.makeProject(name='container',
287- owner=self.owner)
288+ self.target = self.factory.makeProject(
289+ name='container', owner=self.owner)
290
291 def makeSubordinateProduct(self, tracks_bugs_in_lp):
292 """Create a new product and add it to the project group."""
293@@ -1844,8 +1836,7 @@
294
295 def _assertThatUnbatchedAndBatchedActivityMatch(self, unbatched_activity,
296 batched_activity):
297- zipped_activity = zip(
298- unbatched_activity, batched_activity)
299+ zipped_activity = zip(unbatched_activity, batched_activity)
300 for index, items in enumerate(zipped_activity):
301 unbatched_item, batched_item = items
302 self.assertEqual(
303@@ -1877,8 +1868,7 @@
304 bug_task = self.factory.makeBugTask()
305 view = create_initialized_view(bug_task, '+batched-comments')
306 self.assertEqual(
307- config.malone.comments_list_default_batch_size,
308- view.batch_size)
309+ config.malone.comments_list_default_batch_size, view.batch_size)
310 view = create_initialized_view(
311 bug_task, '+batched-comments', form={'batch_size': 20})
312 self.assertEqual(20, view.batch_size)
313
314=== modified file 'lib/lp/bugs/subscribers/bug.py'
315--- lib/lp/bugs/subscribers/bug.py 2012-09-07 20:15:30 +0000
316+++ lib/lp/bugs/subscribers/bug.py 2012-09-20 04:52:24 +0000
317@@ -160,6 +160,14 @@
318 elif (isinstance(change, BugTaskAssigneeChange) and
319 new_subscribers is not None):
320 for person in new_subscribers:
321+ # If this change involves multiple changes, other structural
322+ # subscribers will leak into new_subscribers, and they may
323+ # not be in the recipients list, due to having a LIFECYCLE
324+ # structural subscription.
325+ if person not in recipients:
326+ continue
327+ # We are only interested in dropping the assignee out, since
328+ # we send assignment notifications separately.
329 reason, rationale = recipients.getReason(person)
330 if 'Assignee' in rationale:
331 recipients.remove(person)