← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Brad Crittenden has proposed merging lp:~bac/launchpad/bug-720147 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #720147 in Launchpad itself: "Subscription to New or Incomplete bugs is allowing just-filed bugs of all statuses through"
  https://bugs.launchpad.net/launchpad/+bug/720147

For more details, see:
https://code.launchpad.net/~bac/launchpad/bug-720147/+merge/59677

= 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
-- 
https://code.launchpad.net/~bac/launchpad/bug-720147/+merge/59677
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/launchpad/bug-720147 into lp:launchpad.
=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py	2011-04-15 02:56:03 +0000
+++ lib/lp/bugs/browser/bugtarget.py	2011-05-02 17:11:01 +0000
@@ -556,20 +556,20 @@
 
         if extra_data.private:
             params.private = extra_data.private
-
-        self.added_bug = bug = context.createBug(params)
-
-        # Apply any extra options given by a bug supervisor.
-        bugtask = self.added_bug.default_bugtask
+        if 'status' in data:
+            params.status = data['status']
         if 'assignee' in data:
-            bugtask.transitionToAssignee(data['assignee'])
+            params.assignee = data['assignee']
         if 'status' in data:
-            bugtask.transitionToStatus(data['status'], self.user)
+            params.status = data['status']
         if 'importance' in data:
-            bugtask.transitionToImportance(data['importance'], self.user)
+            params.importance = data['importance']
         if 'milestone' in data:
-            bugtask.milestone = data['milestone']
-
+            params.milestone = data['milestone']
+
+        self.added_bug = bug = context.createBug(params)
+
+        # Apply any extra options given by a bug supervisor.
         for comment in extra_data.comments:
             bug.newMessage(self.user, bug.followup_subject(), comment)
             notifications.append(

=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py	2011-04-29 11:20:01 +0000
+++ lib/lp/bugs/interfaces/bug.py	2011-05-02 17:11:01 +0000
@@ -97,7 +97,8 @@
     def __init__(self, owner, title, comment=None, description=None, msg=None,
                  status=None, datecreated=None, security_related=False,
                  private=False, subscribers=(), binarypackagename=None,
-                 tags=None, subscribe_owner=True, filed_by=None):
+                 tags=None, subscribe_owner=True, filed_by=None,
+                 importance=None, milestone=None, assignee=None):
         self.owner = owner
         self.title = title
         self.comment = comment
@@ -115,6 +116,9 @@
         self.tags = tags
         self.subscribe_owner = subscribe_owner
         self.filed_by = filed_by
+        self.importance = importance
+        self.milestone = milestone
+        self.assignee = assignee
 
     def setBugTarget(self, product=None, distribution=None,
                      sourcepackagename=None):

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2011-05-02 01:27:43 +0000
+++ lib/lp/bugs/model/bug.py	2011-05-02 17:11:01 +0000
@@ -222,7 +222,8 @@
             "datecreated", "security_related", "private",
             "distribution", "sourcepackagename", "binarypackagename",
             "product", "status", "subscribers", "tags",
-            "subscribe_owner", "filed_by"])
+            "subscribe_owner", "filed_by", "importance",
+            "milestone", "assignee"])
 
 
 class BugTag(SQLBase):
@@ -2442,15 +2443,24 @@
         # Create the task on a product if one was passed.
         if params.product:
             getUtility(IBugTaskSet).createTask(
-                bug=bug, product=params.product, owner=params.owner,
-                status=params.status)
+                bug=bug, product=params.product, owner=params.owner)
 
         # Create the task on a source package name if one was passed.
         if params.distribution:
             getUtility(IBugTaskSet).createTask(
                 bug=bug, distribution=params.distribution,
                 sourcepackagename=params.sourcepackagename,
-                owner=params.owner, status=params.status)
+                owner=params.owner)
+
+        bug_task = bug.default_bugtask
+        if params.assignee:
+            bug_task.transitionToAssignee(params.assignee)
+        if params.status:
+            bug_task.transitionToStatus(params.status, params.owner)
+        if params.importance:
+            bug_task.transitionToImportance(params.importance, params.owner)
+        if params.milestone:
+            bug_task.transitionToMilestone(params.milestone, params.owner)
 
         # Tell everyone.
         notify(event)

=== modified file 'lib/lp/bugs/scripts/tests/test_bugnotification.py'
--- lib/lp/bugs/scripts/tests/test_bugnotification.py	2011-04-05 22:34:35 +0000
+++ lib/lp/bugs/scripts/tests/test_bugnotification.py	2011-05-02 17:11:01 +0000
@@ -21,6 +21,7 @@
     )
 from canonical.launchpad.ftests import login
 from canonical.launchpad.helpers import get_contact_email_addresses
+from canonical.launchpad.interfaces.lpstorm import IStore
 from canonical.launchpad.interfaces.message import IMessageSet
 from canonical.testing.layers import LaunchpadZopelessLayer
 from lp.bugs.adapters.bugchange import (
@@ -38,16 +39,20 @@
     CveUnlinkedFromBug,
     )
 from lp.bugs.interfaces.bug import (
+    CreateBugParams,
     IBug,
     IBugSet,
     )
 from lp.bugs.interfaces.bugnotification import IBugNotificationSet
-from lp.bugs.interfaces.bugtask import BugTaskStatus
+from lp.bugs.interfaces.bugtask import (
+    BugTaskImportance,
+    BugTaskStatus,
+    )
 from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
 from lp.bugs.model.bugnotification import (
     BugNotification,
     BugNotificationFilter,
-#    BugNotificationRecipient,
+    BugNotificationRecipient,
     )
 from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilterMute
 from lp.bugs.model.bugtask import BugTask
@@ -1021,7 +1026,7 @@
     def test_header_single(self):
         # A single filter with a description makes all emails
         # include that particular filter description in a header.
-        bug_filter = self.addFilter(u"Test filter")
+        self.addFilter(u"Test filter")
 
         self.assertContentEqual([u"Test filter"],
                                 self.getSubscriptionEmailHeaders())
@@ -1029,8 +1034,8 @@
     def test_header_multiple(self):
         # Multiple filters with a description make all emails
         # include all filter descriptions in the header.
-        bug_filter = self.addFilter(u"First filter")
-        bug_filter = self.addFilter(u"Second filter")
+        self.addFilter(u"First filter")
+        self.addFilter(u"Second filter")
 
         self.assertContentEqual([u"First filter", u"Second filter"],
                                 self.getSubscriptionEmailHeaders())
@@ -1042,10 +1047,10 @@
         other_person = self.factory.makePerson()
         other_subscription = self.bug.default_bugtask.target.addSubscription(
             other_person, other_person)
-        bug_filter = self.addFilter(u"Someone's filter", other_subscription)
+        self.addFilter(u"Someone's filter", other_subscription)
         self.addNotificationRecipient(self.notification, other_person)
 
-        this_filter = self.addFilter(u"Test filter")
+        self.addFilter(u"Test filter")
 
         the_subscriber = self.subscription.subscriber
         self.assertEquals(
@@ -1062,7 +1067,7 @@
     def test_body_single(self):
         # A single filter with a description makes all emails
         # include that particular filter description in the body.
-        bug_filter = self.addFilter(u"Test filter")
+        self.addFilter(u"Test filter")
 
         self.assertContentEqual([u"Matching subscriptions: Test filter"],
                                 self.getSubscriptionEmailBody())
@@ -1070,15 +1075,15 @@
     def test_body_multiple(self):
         # Multiple filters with description make all emails
         # include them in the email body.
-        bug_filter = self.addFilter(u"First filter")
-        bug_filter = self.addFilter(u"Second filter")
+        self.addFilter(u"First filter")
+        self.addFilter(u"Second filter")
 
         self.assertContentEqual(
             [u"Matching subscriptions: First filter, Second filter"],
             self.getSubscriptionEmailBody())
 
     def test_muted(self):
-        bug_filter = self.addFilter(u"Test filter")
+        self.addFilter(u"Test filter")
         BugSubscriptionFilterMute(
             person=self.subscription.subscriber,
             filter=self.notification.bug_filters.one())
@@ -1089,11 +1094,58 @@
     def test_header_multiple_one_muted(self):
         # Multiple filters with a description make all emails
         # include all filter descriptions in the header.
-        bug_filter = self.addFilter(u"First filter")
-        bug_filter = self.addFilter(u"Second filter")
+        self.addFilter(u"First filter")
+        self.addFilter(u"Second filter")
         BugSubscriptionFilterMute(
             person=self.subscription.subscriber,
             filter=self.notification.bug_filters[0])
 
         self.assertContentEqual([u"Second filter"],
                                 self.getSubscriptionEmailHeaders())
+
+
+class TestEmailNotificationsWithFiltersWhenBugCreated(TestCaseWithFactory):
+    # See bug 720147.
+
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        super(TestEmailNotificationsWithFiltersWhenBugCreated, self).setUp()
+        self.subscriber = self.factory.makePerson()
+        self.submitter = self.factory.makePerson()
+        self.product = self.factory.makeProduct(
+            bug_supervisor=self.submitter)
+        self.subscription = self.product.addSubscription(
+            self.subscriber, self.subscriber)
+        self.filter = self.subscription.bug_filters[0]
+        self.filter.description = u'Needs triage'
+        self.filter.statuses = [BugTaskStatus.NEW, BugTaskStatus.INCOMPLETE]
+
+    def test_filters_match_when_bug_is_created(self):
+        message = u"this is an unfiltered comment"
+        params = CreateBugParams(
+            title=u"crashes all the time",
+            comment=message, owner=self.submitter,
+            status=BugTaskStatus.NEW)
+        bug = self.product.createBug(params)
+        notification = IStore(BugNotification).find(
+            BugNotification,
+            BugNotification.id==BugNotificationRecipient.bug_notificationID,
+            BugNotificationRecipient.personID == self.subscriber.id,
+            BugNotification.bug == bug).one()
+        self.assertEqual(notification.message.text_contents, message)
+
+    def test_filters_do_not_match_when_bug_is_created(self):
+        message = u"this is a filtered comment"
+        params = CreateBugParams(
+            title=u"crashes all the time",
+            comment=message, owner=self.submitter,
+            status=BugTaskStatus.TRIAGED,
+            importance=BugTaskImportance.HIGH)
+        bug = self.product.createBug(params)
+        notifications = IStore(BugNotification).find(
+            BugNotification,
+            BugNotification.id==BugNotificationRecipient.bug_notificationID,
+            BugNotificationRecipient.personID == self.subscriber.id,
+            BugNotification.bug == bug)
+        self.assertTrue(notifications.is_empty())

=== modified file 'lib/lp/bugs/tests/test_bug.py'
--- lib/lp/bugs/tests/test_bug.py	2011-04-13 15:23:54 +0000
+++ lib/lp/bugs/tests/test_bug.py	2011-05-02 17:11:01 +0000
@@ -1,4 +1,4 @@
-# Copyright 2011 Canonical Ltd.  This software is licensed under the
+
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for lp.bugs.model.Bug."""
@@ -6,11 +6,23 @@
 __metaclass__ = type
 
 from lazr.lifecycle.snapshot import Snapshot
+from zope.component import getUtility
 from zope.interface import providedBy
 
 from canonical.testing.layers import DatabaseFunctionalLayer
 
 from lp.bugs.enum import BugNotificationLevel
+from lp.bugs.interfaces.bug import(
+    CreateBugParams,
+    IBugSet,
+    )
+from lp.bugs.interfaces.bugtask import (
+    BugTaskImportance,
+    BugTaskStatus,
+    UserCannotEditBugTaskAssignee,
+    UserCannotEditBugTaskImportance,
+    UserCannotEditBugTaskMilestone,
+    )
 from lp.testing import (
     person_logged_in,
     StormStatementRecorder,
@@ -30,7 +42,7 @@
         # Bug.isMuted() will return True if the passed to it has a
         # BugSubscription with a BugNotificationLevel of NOTHING.
         with person_logged_in(self.person):
-            subscription = self.bug.subscribe(
+            self.bug.subscribe(
                 self.person, self.person, level=BugNotificationLevel.NOTHING)
             self.assertEqual(True, self.bug.isMuted(self.person))
 
@@ -38,7 +50,7 @@
         # Bug.isMuted() will return False if the user has a subscription
         # with BugNotificationLevel that's not NOTHING.
         with person_logged_in(self.person):
-            subscription = self.bug.subscribe(
+            self.bug.subscribe(
                 self.person, self.person, level=BugNotificationLevel.METADATA)
             self.assertEqual(False, self.bug.isMuted(self.person))
 
@@ -122,7 +134,7 @@
         # optimizations that might trigger the problem again.
         with person_logged_in(self.person):
             with StormStatementRecorder() as recorder:
-                snapshot = Snapshot(self.bug, providing=providedBy(self.bug))
+                Snapshot(self.bug, providing=providedBy(self.bug))
             sql_statements = recorder.statements
         # This uses "self" as a marker to show that the attribute does not
         # exist.  We do not use hasattr because it eats exceptions.
@@ -139,3 +151,113 @@
                 [token for token in sql_tokens
                  if token.startswith('message')],
                 [])
+
+
+class TestBugCreation(TestCaseWithFactory):
+    """Tests for bug creation."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_CreateBugParams_accepts_importance(self):
+        # The importance of the initial bug task can be set using
+        # CreateBugParams
+        owner = self.factory.makePerson()
+        target = self.factory.makeProduct(owner=owner)
+        with person_logged_in(owner):
+            params = CreateBugParams(
+                owner=owner, title="A bug", comment="Nothing important.",
+                importance=BugTaskImportance.HIGH)
+            params.setBugTarget(product=target)
+            bug = getUtility(IBugSet).createBug(params)
+            self.assertEqual(
+                bug.default_bugtask.importance, params.importance)
+
+    def test_CreateBugParams_accepts_assignee(self):
+        # The assignee of the initial bug task can be set using
+        # CreateBugParams
+        owner = self.factory.makePerson()
+        target = self.factory.makeProduct(owner=owner)
+        with person_logged_in(owner):
+            params = CreateBugParams(
+                owner=owner, title="A bug", comment="Nothing important.",
+                assignee=owner)
+            params.setBugTarget(product=target)
+            bug = getUtility(IBugSet).createBug(params)
+            self.assertEqual(
+                bug.default_bugtask.assignee, params.assignee)
+
+    def test_CreateBugParams_accepts_milestone(self):
+        # The milestone of the initial bug task can be set using
+        # CreateBugParams
+        owner = self.factory.makePerson()
+        target = self.factory.makeProduct(owner=owner)
+        with person_logged_in(owner):
+            params = CreateBugParams(
+                owner=owner, title="A bug", comment="Nothing important.",
+                milestone=self.factory.makeMilestone(product=target))
+            params.setBugTarget(product=target)
+            bug = getUtility(IBugSet).createBug(params)
+            self.assertEqual(
+                bug.default_bugtask.milestone, params.milestone)
+
+    def test_CreateBugParams_accepts_status(self):
+        # The status of the initial bug task can be set using
+        # CreateBugParams
+        owner = self.factory.makePerson()
+        target = self.factory.makeProduct(owner=owner)
+        with person_logged_in(owner):
+            params = CreateBugParams(
+                owner=owner, title="A bug", comment="Nothing important.",
+                status=BugTaskStatus.TRIAGED)
+            params.setBugTarget(product=target)
+            bug = getUtility(IBugSet).createBug(params)
+            self.assertEqual(
+                bug.default_bugtask.status, params.status)
+
+    def test_CreateBugParams_rejects_not_allowed_importance_changes(self):
+        # createBug() will reject any importance value passed by users
+        # who don't have the right to set the importance.
+        person = self.factory.makePerson()
+        target = self.factory.makeProduct()
+        with person_logged_in(person):
+            params = CreateBugParams(
+                owner=person, title="A bug", comment="Nothing important.",
+                importance=BugTaskImportance.HIGH)
+            params.setBugTarget(product=target)
+            self.assertRaises(
+                UserCannotEditBugTaskImportance,
+                getUtility(IBugSet).createBug, params)
+
+    def test_CreateBugParams_rejects_not_allowed_assignee_changes(self):
+        # createBug() will reject any importance value passed by users
+        # who don't have the right to set the assignee.
+        person = self.factory.makePerson()
+        person_2 = self.factory.makePerson()
+        target = self.factory.makeProduct()
+        # Setting the target's bug supervisor means that
+        # canTransitionToAssignee() will return False for `person` if
+        # another Person is passed as `assignee`.
+        with person_logged_in(target.owner):
+            target.setBugSupervisor(target.owner, target.owner)
+        with person_logged_in(person):
+            params = CreateBugParams(
+                owner=person, title="A bug", comment="Nothing important.",
+                assignee=person_2)
+            params.setBugTarget(product=target)
+            self.assertRaises(
+                UserCannotEditBugTaskAssignee,
+                getUtility(IBugSet).createBug, params)
+
+    def test_CreateBugParams_rejects_not_allowed_milestone_changes(self):
+        # createBug() will reject any importance value passed by users
+        # who don't have the right to set the milestone.
+        person = self.factory.makePerson()
+        target = self.factory.makeProduct()
+        with person_logged_in(person):
+            params = CreateBugParams(
+                owner=person, title="A bug", comment="Nothing important.",
+                milestone=self.factory.makeMilestone(product=target))
+            params.setBugTarget(product=target)
+            self.assertRaises(
+                UserCannotEditBugTaskMilestone,
+                getUtility(IBugSet).createBug, params)