Merge lp:~bac/launchpad/bug-720147 into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 12967
Proposed branch: lp:~bac/launchpad/bug-720147
Merge into: lp:launchpad
Diff against target: 432 lines (+220/-32)
5 files modified
lib/lp/bugs/browser/bugtarget.py (+10/-10)
lib/lp/bugs/interfaces/bug.py (+5/-1)
lib/lp/bugs/model/bug.py (+14/-4)
lib/lp/bugs/scripts/tests/test_bugnotification.py (+65/-13)
lib/lp/bugs/tests/test_bug.py (+126/-4)
To merge this branch: bzr merge lp:~bac/launchpad/bug-720147
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+59677@code.launchpad.net

Commit message

[r=benji][bug=720147] Correctly handle filtering for bugs created without the default status.

Description of the change

= Summary =

Bugs created with a status other than NEW were sending notifications for
subscriptions with other filters.

== Proposed fix ==

The bug notification was kicked off before the status change was
effected resulting in improper notices being sent.

== Pre-implementation notes ==

This work was done by Graham and Gary. I'm merely shepherding it
through review and landing in their absence.

I did, however, clean up some lint.

== Implementation details ==

As above.

== Tests ==

bin/test -vvm lp.bugs -t test_bugnotification

== Demo and Q/A ==

Create a structural subscription with filter with NEW status. Create a
bug with initial status of TRIAGE. Ensure notification is not sent.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/browser/bugtarget.py
  lib/lp/bugs/scripts/tests/test_bugnotification.py
  lib/lp/bugs/model/bug.py
  lib/lp/bugs/tests/test_bug.py
  lib/lp/bugs/interfaces/bug.py

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good.

The only thought I had was that the part starting on line 13 of the diff
is a bit repetitive and I had to look very closely to see if it was
exactly the same pattern for each line or if one or more were subtly
different. However, I'm not sure my version is any better:
http://pastebin.ubuntu.com/602393/ (untested).

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugtarget.py'
2--- lib/lp/bugs/browser/bugtarget.py 2011-04-15 02:56:03 +0000
3+++ lib/lp/bugs/browser/bugtarget.py 2011-05-02 17:11:01 +0000
4@@ -556,20 +556,20 @@
5
6 if extra_data.private:
7 params.private = extra_data.private
8-
9- self.added_bug = bug = context.createBug(params)
10-
11- # Apply any extra options given by a bug supervisor.
12- bugtask = self.added_bug.default_bugtask
13+ if 'status' in data:
14+ params.status = data['status']
15 if 'assignee' in data:
16- bugtask.transitionToAssignee(data['assignee'])
17+ params.assignee = data['assignee']
18 if 'status' in data:
19- bugtask.transitionToStatus(data['status'], self.user)
20+ params.status = data['status']
21 if 'importance' in data:
22- bugtask.transitionToImportance(data['importance'], self.user)
23+ params.importance = data['importance']
24 if 'milestone' in data:
25- bugtask.milestone = data['milestone']
26-
27+ params.milestone = data['milestone']
28+
29+ self.added_bug = bug = context.createBug(params)
30+
31+ # Apply any extra options given by a bug supervisor.
32 for comment in extra_data.comments:
33 bug.newMessage(self.user, bug.followup_subject(), comment)
34 notifications.append(
35
36=== modified file 'lib/lp/bugs/interfaces/bug.py'
37--- lib/lp/bugs/interfaces/bug.py 2011-04-29 11:20:01 +0000
38+++ lib/lp/bugs/interfaces/bug.py 2011-05-02 17:11:01 +0000
39@@ -97,7 +97,8 @@
40 def __init__(self, owner, title, comment=None, description=None, msg=None,
41 status=None, datecreated=None, security_related=False,
42 private=False, subscribers=(), binarypackagename=None,
43- tags=None, subscribe_owner=True, filed_by=None):
44+ tags=None, subscribe_owner=True, filed_by=None,
45+ importance=None, milestone=None, assignee=None):
46 self.owner = owner
47 self.title = title
48 self.comment = comment
49@@ -115,6 +116,9 @@
50 self.tags = tags
51 self.subscribe_owner = subscribe_owner
52 self.filed_by = filed_by
53+ self.importance = importance
54+ self.milestone = milestone
55+ self.assignee = assignee
56
57 def setBugTarget(self, product=None, distribution=None,
58 sourcepackagename=None):
59
60=== modified file 'lib/lp/bugs/model/bug.py'
61--- lib/lp/bugs/model/bug.py 2011-05-02 01:27:43 +0000
62+++ lib/lp/bugs/model/bug.py 2011-05-02 17:11:01 +0000
63@@ -222,7 +222,8 @@
64 "datecreated", "security_related", "private",
65 "distribution", "sourcepackagename", "binarypackagename",
66 "product", "status", "subscribers", "tags",
67- "subscribe_owner", "filed_by"])
68+ "subscribe_owner", "filed_by", "importance",
69+ "milestone", "assignee"])
70
71
72 class BugTag(SQLBase):
73@@ -2442,15 +2443,24 @@
74 # Create the task on a product if one was passed.
75 if params.product:
76 getUtility(IBugTaskSet).createTask(
77- bug=bug, product=params.product, owner=params.owner,
78- status=params.status)
79+ bug=bug, product=params.product, owner=params.owner)
80
81 # Create the task on a source package name if one was passed.
82 if params.distribution:
83 getUtility(IBugTaskSet).createTask(
84 bug=bug, distribution=params.distribution,
85 sourcepackagename=params.sourcepackagename,
86- owner=params.owner, status=params.status)
87+ owner=params.owner)
88+
89+ bug_task = bug.default_bugtask
90+ if params.assignee:
91+ bug_task.transitionToAssignee(params.assignee)
92+ if params.status:
93+ bug_task.transitionToStatus(params.status, params.owner)
94+ if params.importance:
95+ bug_task.transitionToImportance(params.importance, params.owner)
96+ if params.milestone:
97+ bug_task.transitionToMilestone(params.milestone, params.owner)
98
99 # Tell everyone.
100 notify(event)
101
102=== modified file 'lib/lp/bugs/scripts/tests/test_bugnotification.py'
103--- lib/lp/bugs/scripts/tests/test_bugnotification.py 2011-04-05 22:34:35 +0000
104+++ lib/lp/bugs/scripts/tests/test_bugnotification.py 2011-05-02 17:11:01 +0000
105@@ -21,6 +21,7 @@
106 )
107 from canonical.launchpad.ftests import login
108 from canonical.launchpad.helpers import get_contact_email_addresses
109+from canonical.launchpad.interfaces.lpstorm import IStore
110 from canonical.launchpad.interfaces.message import IMessageSet
111 from canonical.testing.layers import LaunchpadZopelessLayer
112 from lp.bugs.adapters.bugchange import (
113@@ -38,16 +39,20 @@
114 CveUnlinkedFromBug,
115 )
116 from lp.bugs.interfaces.bug import (
117+ CreateBugParams,
118 IBug,
119 IBugSet,
120 )
121 from lp.bugs.interfaces.bugnotification import IBugNotificationSet
122-from lp.bugs.interfaces.bugtask import BugTaskStatus
123+from lp.bugs.interfaces.bugtask import (
124+ BugTaskImportance,
125+ BugTaskStatus,
126+ )
127 from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
128 from lp.bugs.model.bugnotification import (
129 BugNotification,
130 BugNotificationFilter,
131-# BugNotificationRecipient,
132+ BugNotificationRecipient,
133 )
134 from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilterMute
135 from lp.bugs.model.bugtask import BugTask
136@@ -1021,7 +1026,7 @@
137 def test_header_single(self):
138 # A single filter with a description makes all emails
139 # include that particular filter description in a header.
140- bug_filter = self.addFilter(u"Test filter")
141+ self.addFilter(u"Test filter")
142
143 self.assertContentEqual([u"Test filter"],
144 self.getSubscriptionEmailHeaders())
145@@ -1029,8 +1034,8 @@
146 def test_header_multiple(self):
147 # Multiple filters with a description make all emails
148 # include all filter descriptions in the header.
149- bug_filter = self.addFilter(u"First filter")
150- bug_filter = self.addFilter(u"Second filter")
151+ self.addFilter(u"First filter")
152+ self.addFilter(u"Second filter")
153
154 self.assertContentEqual([u"First filter", u"Second filter"],
155 self.getSubscriptionEmailHeaders())
156@@ -1042,10 +1047,10 @@
157 other_person = self.factory.makePerson()
158 other_subscription = self.bug.default_bugtask.target.addSubscription(
159 other_person, other_person)
160- bug_filter = self.addFilter(u"Someone's filter", other_subscription)
161+ self.addFilter(u"Someone's filter", other_subscription)
162 self.addNotificationRecipient(self.notification, other_person)
163
164- this_filter = self.addFilter(u"Test filter")
165+ self.addFilter(u"Test filter")
166
167 the_subscriber = self.subscription.subscriber
168 self.assertEquals(
169@@ -1062,7 +1067,7 @@
170 def test_body_single(self):
171 # A single filter with a description makes all emails
172 # include that particular filter description in the body.
173- bug_filter = self.addFilter(u"Test filter")
174+ self.addFilter(u"Test filter")
175
176 self.assertContentEqual([u"Matching subscriptions: Test filter"],
177 self.getSubscriptionEmailBody())
178@@ -1070,15 +1075,15 @@
179 def test_body_multiple(self):
180 # Multiple filters with description make all emails
181 # include them in the email body.
182- bug_filter = self.addFilter(u"First filter")
183- bug_filter = self.addFilter(u"Second filter")
184+ self.addFilter(u"First filter")
185+ self.addFilter(u"Second filter")
186
187 self.assertContentEqual(
188 [u"Matching subscriptions: First filter, Second filter"],
189 self.getSubscriptionEmailBody())
190
191 def test_muted(self):
192- bug_filter = self.addFilter(u"Test filter")
193+ self.addFilter(u"Test filter")
194 BugSubscriptionFilterMute(
195 person=self.subscription.subscriber,
196 filter=self.notification.bug_filters.one())
197@@ -1089,11 +1094,58 @@
198 def test_header_multiple_one_muted(self):
199 # Multiple filters with a description make all emails
200 # include all filter descriptions in the header.
201- bug_filter = self.addFilter(u"First filter")
202- bug_filter = self.addFilter(u"Second filter")
203+ self.addFilter(u"First filter")
204+ self.addFilter(u"Second filter")
205 BugSubscriptionFilterMute(
206 person=self.subscription.subscriber,
207 filter=self.notification.bug_filters[0])
208
209 self.assertContentEqual([u"Second filter"],
210 self.getSubscriptionEmailHeaders())
211+
212+
213+class TestEmailNotificationsWithFiltersWhenBugCreated(TestCaseWithFactory):
214+ # See bug 720147.
215+
216+ layer = LaunchpadZopelessLayer
217+
218+ def setUp(self):
219+ super(TestEmailNotificationsWithFiltersWhenBugCreated, self).setUp()
220+ self.subscriber = self.factory.makePerson()
221+ self.submitter = self.factory.makePerson()
222+ self.product = self.factory.makeProduct(
223+ bug_supervisor=self.submitter)
224+ self.subscription = self.product.addSubscription(
225+ self.subscriber, self.subscriber)
226+ self.filter = self.subscription.bug_filters[0]
227+ self.filter.description = u'Needs triage'
228+ self.filter.statuses = [BugTaskStatus.NEW, BugTaskStatus.INCOMPLETE]
229+
230+ def test_filters_match_when_bug_is_created(self):
231+ message = u"this is an unfiltered comment"
232+ params = CreateBugParams(
233+ title=u"crashes all the time",
234+ comment=message, owner=self.submitter,
235+ status=BugTaskStatus.NEW)
236+ bug = self.product.createBug(params)
237+ notification = IStore(BugNotification).find(
238+ BugNotification,
239+ BugNotification.id==BugNotificationRecipient.bug_notificationID,
240+ BugNotificationRecipient.personID == self.subscriber.id,
241+ BugNotification.bug == bug).one()
242+ self.assertEqual(notification.message.text_contents, message)
243+
244+ def test_filters_do_not_match_when_bug_is_created(self):
245+ message = u"this is a filtered comment"
246+ params = CreateBugParams(
247+ title=u"crashes all the time",
248+ comment=message, owner=self.submitter,
249+ status=BugTaskStatus.TRIAGED,
250+ importance=BugTaskImportance.HIGH)
251+ bug = self.product.createBug(params)
252+ notifications = IStore(BugNotification).find(
253+ BugNotification,
254+ BugNotification.id==BugNotificationRecipient.bug_notificationID,
255+ BugNotificationRecipient.personID == self.subscriber.id,
256+ BugNotification.bug == bug)
257+ self.assertTrue(notifications.is_empty())
258
259=== modified file 'lib/lp/bugs/tests/test_bug.py'
260--- lib/lp/bugs/tests/test_bug.py 2011-04-13 15:23:54 +0000
261+++ lib/lp/bugs/tests/test_bug.py 2011-05-02 17:11:01 +0000
262@@ -1,4 +1,4 @@
263-# Copyright 2011 Canonical Ltd. This software is licensed under the
264+
265 # GNU Affero General Public License version 3 (see the file LICENSE).
266
267 """Tests for lp.bugs.model.Bug."""
268@@ -6,11 +6,23 @@
269 __metaclass__ = type
270
271 from lazr.lifecycle.snapshot import Snapshot
272+from zope.component import getUtility
273 from zope.interface import providedBy
274
275 from canonical.testing.layers import DatabaseFunctionalLayer
276
277 from lp.bugs.enum import BugNotificationLevel
278+from lp.bugs.interfaces.bug import(
279+ CreateBugParams,
280+ IBugSet,
281+ )
282+from lp.bugs.interfaces.bugtask import (
283+ BugTaskImportance,
284+ BugTaskStatus,
285+ UserCannotEditBugTaskAssignee,
286+ UserCannotEditBugTaskImportance,
287+ UserCannotEditBugTaskMilestone,
288+ )
289 from lp.testing import (
290 person_logged_in,
291 StormStatementRecorder,
292@@ -30,7 +42,7 @@
293 # Bug.isMuted() will return True if the passed to it has a
294 # BugSubscription with a BugNotificationLevel of NOTHING.
295 with person_logged_in(self.person):
296- subscription = self.bug.subscribe(
297+ self.bug.subscribe(
298 self.person, self.person, level=BugNotificationLevel.NOTHING)
299 self.assertEqual(True, self.bug.isMuted(self.person))
300
301@@ -38,7 +50,7 @@
302 # Bug.isMuted() will return False if the user has a subscription
303 # with BugNotificationLevel that's not NOTHING.
304 with person_logged_in(self.person):
305- subscription = self.bug.subscribe(
306+ self.bug.subscribe(
307 self.person, self.person, level=BugNotificationLevel.METADATA)
308 self.assertEqual(False, self.bug.isMuted(self.person))
309
310@@ -122,7 +134,7 @@
311 # optimizations that might trigger the problem again.
312 with person_logged_in(self.person):
313 with StormStatementRecorder() as recorder:
314- snapshot = Snapshot(self.bug, providing=providedBy(self.bug))
315+ Snapshot(self.bug, providing=providedBy(self.bug))
316 sql_statements = recorder.statements
317 # This uses "self" as a marker to show that the attribute does not
318 # exist. We do not use hasattr because it eats exceptions.
319@@ -139,3 +151,113 @@
320 [token for token in sql_tokens
321 if token.startswith('message')],
322 [])
323+
324+
325+class TestBugCreation(TestCaseWithFactory):
326+ """Tests for bug creation."""
327+
328+ layer = DatabaseFunctionalLayer
329+
330+ def test_CreateBugParams_accepts_importance(self):
331+ # The importance of the initial bug task can be set using
332+ # CreateBugParams
333+ owner = self.factory.makePerson()
334+ target = self.factory.makeProduct(owner=owner)
335+ with person_logged_in(owner):
336+ params = CreateBugParams(
337+ owner=owner, title="A bug", comment="Nothing important.",
338+ importance=BugTaskImportance.HIGH)
339+ params.setBugTarget(product=target)
340+ bug = getUtility(IBugSet).createBug(params)
341+ self.assertEqual(
342+ bug.default_bugtask.importance, params.importance)
343+
344+ def test_CreateBugParams_accepts_assignee(self):
345+ # The assignee of the initial bug task can be set using
346+ # CreateBugParams
347+ owner = self.factory.makePerson()
348+ target = self.factory.makeProduct(owner=owner)
349+ with person_logged_in(owner):
350+ params = CreateBugParams(
351+ owner=owner, title="A bug", comment="Nothing important.",
352+ assignee=owner)
353+ params.setBugTarget(product=target)
354+ bug = getUtility(IBugSet).createBug(params)
355+ self.assertEqual(
356+ bug.default_bugtask.assignee, params.assignee)
357+
358+ def test_CreateBugParams_accepts_milestone(self):
359+ # The milestone of the initial bug task can be set using
360+ # CreateBugParams
361+ owner = self.factory.makePerson()
362+ target = self.factory.makeProduct(owner=owner)
363+ with person_logged_in(owner):
364+ params = CreateBugParams(
365+ owner=owner, title="A bug", comment="Nothing important.",
366+ milestone=self.factory.makeMilestone(product=target))
367+ params.setBugTarget(product=target)
368+ bug = getUtility(IBugSet).createBug(params)
369+ self.assertEqual(
370+ bug.default_bugtask.milestone, params.milestone)
371+
372+ def test_CreateBugParams_accepts_status(self):
373+ # The status of the initial bug task can be set using
374+ # CreateBugParams
375+ owner = self.factory.makePerson()
376+ target = self.factory.makeProduct(owner=owner)
377+ with person_logged_in(owner):
378+ params = CreateBugParams(
379+ owner=owner, title="A bug", comment="Nothing important.",
380+ status=BugTaskStatus.TRIAGED)
381+ params.setBugTarget(product=target)
382+ bug = getUtility(IBugSet).createBug(params)
383+ self.assertEqual(
384+ bug.default_bugtask.status, params.status)
385+
386+ def test_CreateBugParams_rejects_not_allowed_importance_changes(self):
387+ # createBug() will reject any importance value passed by users
388+ # who don't have the right to set the importance.
389+ person = self.factory.makePerson()
390+ target = self.factory.makeProduct()
391+ with person_logged_in(person):
392+ params = CreateBugParams(
393+ owner=person, title="A bug", comment="Nothing important.",
394+ importance=BugTaskImportance.HIGH)
395+ params.setBugTarget(product=target)
396+ self.assertRaises(
397+ UserCannotEditBugTaskImportance,
398+ getUtility(IBugSet).createBug, params)
399+
400+ def test_CreateBugParams_rejects_not_allowed_assignee_changes(self):
401+ # createBug() will reject any importance value passed by users
402+ # who don't have the right to set the assignee.
403+ person = self.factory.makePerson()
404+ person_2 = self.factory.makePerson()
405+ target = self.factory.makeProduct()
406+ # Setting the target's bug supervisor means that
407+ # canTransitionToAssignee() will return False for `person` if
408+ # another Person is passed as `assignee`.
409+ with person_logged_in(target.owner):
410+ target.setBugSupervisor(target.owner, target.owner)
411+ with person_logged_in(person):
412+ params = CreateBugParams(
413+ owner=person, title="A bug", comment="Nothing important.",
414+ assignee=person_2)
415+ params.setBugTarget(product=target)
416+ self.assertRaises(
417+ UserCannotEditBugTaskAssignee,
418+ getUtility(IBugSet).createBug, params)
419+
420+ def test_CreateBugParams_rejects_not_allowed_milestone_changes(self):
421+ # createBug() will reject any importance value passed by users
422+ # who don't have the right to set the milestone.
423+ person = self.factory.makePerson()
424+ target = self.factory.makeProduct()
425+ with person_logged_in(person):
426+ params = CreateBugParams(
427+ owner=person, title="A bug", comment="Nothing important.",
428+ milestone=self.factory.makeMilestone(product=target))
429+ params.setBugTarget(product=target)
430+ self.assertRaises(
431+ UserCannotEditBugTaskMilestone,
432+ getUtility(IBugSet).createBug, params)