Merge lp:~sinzui/launchpad/bugtask-create-question-1 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 12914
Proposed branch: lp:~sinzui/launchpad/bugtask-create-question-1
Merge into: lp:launchpad
Diff against target: 291 lines (+115/-27)
6 files modified
lib/lp/answers/doc/notifications.txt (+8/-4)
lib/lp/answers/model/question.py (+12/-7)
lib/lp/answers/notification.py (+17/-10)
lib/lp/answers/tests/test_question_notifications.py (+32/-1)
lib/lp/answers/tests/test_questiontarget.py (+42/-1)
lib/lp/bugs/tests/test_bugtarget.py (+4/-4)
To merge this branch: bzr merge lp:~sinzui/launchpad/bugtask-create-question-1
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+58826@code.launchpad.net

Description of the change

Send the first and last messages only when converting a bug to a question.

    Launchpad bugs:
        https://bugs.launchpad.net/bugs/438116
        https://bugs.launchpad.net/bugs/247685
        https://bugs.launchpad.net/bugs/304960
    Pre-implementation: jcsackett, lifeless

BugTask:+create-question times out:
There are two principle issues described in this bug: sending multiple emails
to users, and sending them in proc. The later issue is also the cause of bug
608037
, bug 618390, and bug 618385. Fixing this bug will only be about
addressing the unpredictable number of messages.

While reading the CreateQuestionFromBug() I saw two places that are creating
more emails than we want. I also saw that there are two other bugs that
can be fixed while tuning this method.

--------------------------------------------------------------------

RULES

bugs #438116 "Timeout when converting bug into question "
    * Convert only the last bug message to a question message.
    * Do not send emails about the implicit bug link (create it directly?)

bug #247685 "Email for a question converted from a bug has the wrong sender"
    * Ensure the sender of the first message is the user who actually sent
      the message. The sender comes from the user in the event...this
      needs to be corrected.

bug #304960 "Bug converted to Question lost subscribers"
    * Ensure direct subscribers to the bug are added as direct subscribers
      to the question.

QA

    * Convert a bug with more than two comments and at least two subscribers
      to a question.
    * Verify each contact gets two messages, the first from the user
      who reported the bug, and a second from the user who converted the
      bug to a question.
    * Verify the question has the same direct subscribers as the bug.

LINT

    lib/lp/answers/notification.py
    lib/lp/answers/model/question.py
    lib/lp/answers/tests/test_question_notifications.py
    lib/lp/answers/tests/test_questiontarget.py
    lib/lp/bugs/tests/test_bugtarget.py

TEST

    ./bin/test -vv -t TestQuestionTargetCreateQuestionFromBug \
        -t QuestionCreatedTestCase -t bugtarget-questiontarget

IMPLEMENTATION

The bug link is directly created instead of using the helper method. The
method sends an duplicate email about the bug to the answer contacts, and
it is subscribing the question owner to a bug hie is already subscribed too.
The message loop is replaced with a single message pulled from the end of
the bug messages. The direct bug subscribers are subscribed to the question
so that they can learn the answer.
    lib/lp/answers/model/question.py
    lib/lp/answers/tests/test_questiontarget.py

Added a property to select the correct user when sending emails. Since
Questions can be created for users, the question owner is the real sender
of the email (bug #247685). The test is somewhat arcane. The use of fakes
allows the tests to run on the lowest layer and does not send emails.
    lib/lp/answers/notification.py
    lib/lp/answers/tests/test_question_notifications.py

Run the bugtarget doctests on the correct layer.
    lib/lp/bugs/tests/test_bugtarget.py

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Curtis,

Thanks for tackling this really old problem. Our users will be glad to get this fixed.

* s/recieve/receive

The tests look good and the code makes sense.

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/answers/doc/notifications.txt'
2--- lib/lp/answers/doc/notifications.txt 2011-03-23 16:28:51 +0000
3+++ lib/lp/answers/doc/notifications.txt 2011-04-22 21:17:28 +0000
4@@ -852,15 +852,19 @@
5 >>> bug_question = ubuntu.createQuestionFromBug(bug)
6 >>> notifications = pop_notifications()
7 >>> len(notifications)
8- 2
9+ 4
10
11 >>> [email_msg['To'] for email_msg in notifications]
12- ['no-priv@canonical.com', 'support@ubuntu.com']
13+ ['no-priv@canonical.com', 'no-priv@canonical.com',
14+ 'support@ubuntu.com', 'support@ubuntu.com']
15+
16+ >>> notifications[0]['Subject']
17+ '[Question #...]: Installer fails on a Mac PPC'
18
19 >>> notifications[1]['Subject']
20- '[Question #...]: Installer fails on a Mac PPC'
21+ 'Re: [Question #...]: Installer fails on a Mac PPC'
22
23- >>> notification_body = notifications[1].get_payload(decode=True)
24+ >>> notification_body = notifications[0].get_payload(decode=True)
25 >>> print notification_body
26 New question #... on Ubuntu:
27 http://answers.launchpad.dev/ubuntu/+question/...
28
29=== modified file 'lib/lp/answers/model/question.py'
30--- lib/lp/answers/model/question.py 2011-04-21 21:14:50 +0000
31+++ lib/lp/answers/model/question.py 2011-04-22 21:17:28 +0000
32@@ -1190,13 +1190,18 @@
33 # Give the datelastresponse a current datetime, otherwise the
34 # Launchpad Janitor would quickly expire questions made from old bugs.
35 question.datelastresponse = datetime.now(pytz.timezone('UTC'))
36- question.linkBug(bug)
37- for message in bug.messages[1:]:
38- # Bug.message[0] is the original message, and probably a duplicate
39- # of Bug.description.
40- question.addComment(
41- message.owner, message.text_contents,
42- datecreated=message.datecreated)
43+ # Directly create the BugLink so that users do not receive duplicate
44+ # messages about the bug.
45+ question.createBugLink(bug)
46+ # Copy the last message that explains why the bug is a question.
47+ message = bug.messages[-1]
48+ question.addComment(
49+ message.owner, message.text_contents,
50+ datecreated=message.datecreated)
51+ # Direct subscribers to the bug want to know the question answer.
52+ for subscriber in bug.getDirectSubscribers():
53+ if subscriber != question.owner:
54+ question.subscribe(subscriber)
55 return question
56
57 def getQuestion(self, question_id):
58
59=== modified file 'lib/lp/answers/notification.py'
60--- lib/lp/answers/notification.py 2010-08-24 10:45:57 +0000
61+++ lib/lp/answers/notification.py 2011-04-22 21:17:28 +0000
62@@ -49,11 +49,16 @@
63 """
64 self.question = question
65 self.event = event
66- self.user = IPerson(self.event.user)
67+ self._user = IPerson(self.event.user)
68 self.initialize()
69 if self.shouldNotify():
70 self.send()
71
72+ @property
73+ def user(self):
74+ """Return the user from the event. """
75+ return self._user
76+
77 def getFromAddress(self):
78 """Return a formatted email address suitable for user in the From
79 header of the question notification.
80@@ -180,6 +185,15 @@
81 class QuestionAddedNotification(QuestionNotification):
82 """Notification sent when a question is added."""
83
84+ @property
85+ def user(self):
86+ """Return the question owner.
87+
88+ Questions can be created by other users for the owner; the
89+ question is from the owner.
90+ """
91+ return self.question.owner
92+
93 def getBody(self):
94 """See QuestionNotification."""
95 question = self.question
96@@ -222,7 +236,7 @@
97 """Textual representation of the changes to the question metadata."""
98 question = self.question
99 old_question = self.old_question
100- indent = 4*' '
101+ indent = 4 * ' '
102 info_fields = []
103 if question.status != old_question.status:
104 info_fields.append(indent + 'Status: %s => %s' % (
105@@ -306,10 +320,6 @@
106 # The first message cannot contain a References
107 # because we don't create a Message instance for the
108 # question description, so we don't have a Message-ID.
109-
110- # XXX sinzui 2007-02-01 bug=164435:
111- # Added an assert to gather better Opps information about
112- # the state of the messages.
113 messages = list(self.question.messages)
114 assert self.new_message in messages, (
115 "Question %s: message id %s not in %s." % (
116@@ -318,7 +328,7 @@
117 index = messages.index(self.new_message)
118 if index > 0:
119 headers['References'] = (
120- self.question.messages[index-1].rfc822msgid)
121+ self.question.messages[index - 1].rfc822msgid)
122 return headers
123
124 def shouldNotify(self):
125@@ -352,7 +362,6 @@
126 """
127 original_recipients = QuestionNotification.getRecipients(self)
128 recipients = NotificationRecipientSet()
129- owner = self.question.owner
130 for person in original_recipients:
131 if person != self.question.owner:
132 rationale, header = original_recipients.getReason(person)
133@@ -471,5 +480,3 @@
134 'question_url': canonical_url(question),
135 'question_language': question.language.englishname,
136 'comment': question.description}
137-
138-
139
140=== modified file 'lib/lp/answers/tests/test_question_notifications.py'
141--- lib/lp/answers/tests/test_question_notifications.py 2010-07-18 00:21:11 +0000
142+++ lib/lp/answers/tests/test_question_notifications.py 2011-04-22 21:17:28 +0000
143@@ -9,7 +9,10 @@
144
145 from zope.interface import implements
146
147-from lp.answers.notification import QuestionModifiedDefaultNotification
148+from lp.answers.notification import (
149+ QuestionAddedNotification,
150+ QuestionModifiedDefaultNotification,
151+ )
152 from lp.registry.interfaces.person import IPerson
153
154
155@@ -35,6 +38,7 @@
156 def __init__(self, id=1, title="Question title"):
157 self.id = id
158 self.title = title
159+ self.owner = FakeUser()
160
161
162 class StubQuestionMessage:
163@@ -109,3 +113,30 @@
164 self.assertEquals(
165 'Re: [Question #1]: Message subject',
166 self.notification.getSubject())
167+
168+ def test_user_is_event_user(self):
169+ """The notification user is always the event user."""
170+ question = StubQuestion()
171+ event = FakeEvent()
172+ notification = TestQuestionModifiedNotification(question, event)
173+ self.assertEqual(event.user, notification.user)
174+ self.assertNotEqual(question.owner, notification.user)
175+
176+
177+class TestQuestionAddedNotification(QuestionAddedNotification):
178+ """A subclass that does not send emails."""
179+
180+ def shouldNotify(self):
181+ return False
182+
183+
184+class QuestionCreatedTestCase(TestCase):
185+ """Test cases for mail notifications about created questions."""
186+
187+ def test_user_is_question_owner(self):
188+ """The notification user is always the question owner."""
189+ question = StubQuestion()
190+ event = FakeEvent()
191+ notification = TestQuestionAddedNotification(question, event)
192+ self.assertEqual(question.owner, notification.user)
193+ self.assertNotEqual(event.user, notification.user)
194
195=== modified file 'lib/lp/answers/tests/test_questiontarget.py'
196--- lib/lp/answers/tests/test_questiontarget.py 2011-04-21 21:22:16 +0000
197+++ lib/lp/answers/tests/test_questiontarget.py 2011-04-22 21:17:28 +0000
198@@ -14,7 +14,10 @@
199 from canonical.testing.layers import DatabaseFunctionalLayer
200 from lp.registry.interfaces.distribution import IDistributionSet
201 from lp.services.worlddata.interfaces.language import ILanguageSet
202-from lp.testing import TestCaseWithFactory
203+from lp.testing import (
204+ person_logged_in,
205+ TestCaseWithFactory,
206+ )
207
208
209 class TestQuestionTarget_answer_contacts_with_languages(TestCaseWithFactory):
210@@ -74,3 +77,41 @@
211 lang.englishname for lang in answer_contact.getLanguagesCache()]
212 # The languages cache has been filled in the correct order.
213 self.failUnlessEqual(langs, [u'English', u'Portuguese (Brazil)'])
214+
215+
216+class TestQuestionTargetCreateQuestionFromBug(TestCaseWithFactory):
217+ """Test the createQuestionFromBug from bug behavior."""
218+
219+ layer = DatabaseFunctionalLayer
220+
221+ def setUp(self):
222+ super(TestQuestionTargetCreateQuestionFromBug, self).setUp()
223+ self.bug = self.factory.makeBug(description="first comment")
224+ self.target = self.bug.bugtasks[0].target
225+ self.contributor = self.target.owner
226+ self.reporter = self.bug.owner
227+
228+ def test_first_and_last_messages_copied_to_question(self):
229+ # The question is created with the bug's description and the last
230+ # message which presumably is about why the bug was converted.
231+ with person_logged_in(self.reporter):
232+ self.bug.newMessage(owner=self.reporter, content='second comment')
233+ with person_logged_in(self.contributor):
234+ last_message = self.bug.newMessage(
235+ owner=self.contributor, content='third comment')
236+ question = self.target.createQuestionFromBug(self.bug)
237+ question_messages = list(question.messages)
238+ self.assertEqual(1, len(question_messages))
239+ self.assertEqual(last_message.content, question_messages[0].content)
240+ self.assertEqual(self.bug.description, question.description)
241+
242+ def test_bug_subscribers_copied_to_question(self):
243+ # Users who subscribe to the bug are also interested in the answer.
244+ subscriber = self.factory.makePerson()
245+ with person_logged_in(subscriber):
246+ self.bug.subscribe(subscriber, subscriber)
247+ with person_logged_in(self.contributor):
248+ self.bug.newMessage(owner=self.contributor, content='comment')
249+ question = self.target.createQuestionFromBug(self.bug)
250+ self.assertTrue(question.isSubscribed(subscriber))
251+ self.assertTrue(question.isSubscribed(question.owner))
252
253=== modified file 'lib/lp/bugs/tests/test_bugtarget.py'
254--- lib/lp/bugs/tests/test_bugtarget.py 2010-11-18 14:00:49 +0000
255+++ lib/lp/bugs/tests/test_bugtarget.py 2011-04-22 21:17:28 +0000
256@@ -28,7 +28,7 @@
257 tearDown,
258 )
259 from canonical.launchpad.webapp.interfaces import ILaunchBag
260-from canonical.testing.layers import LaunchpadFunctionalLayer
261+from canonical.testing.layers import DatabaseFunctionalLayer
262 from lp.bugs.interfaces.bug import CreateBugParams
263 from lp.bugs.interfaces.bugtask import (
264 BugTaskSearchParams,
265@@ -191,7 +191,7 @@
266 class TestBugTargetSearchTasks(TestCaseWithFactory):
267 """Tests of IHasBugs.searchTasks()."""
268
269- layer = LaunchpadFunctionalLayer
270+ layer = DatabaseFunctionalLayer
271
272 def setUp(self):
273 super(TestBugTargetSearchTasks, self).setUp()
274@@ -299,7 +299,7 @@
275 for setUpMethod in setUpMethods:
276 test = LayeredDocFileSuite('bugtarget-questiontarget.txt',
277 setUp=setUpMethod, tearDown=tearDown,
278- layer=LaunchpadFunctionalLayer)
279+ layer=DatabaseFunctionalLayer)
280 suite.addTest(test)
281
282 setUpMethods.remove(sourcePackageForQuestionSetUp)
283@@ -309,7 +309,7 @@
284 for setUpMethod in setUpMethods:
285 test = LayeredDocFileSuite('bugtarget-bugcount.txt',
286 setUp=setUpMethod, tearDown=tearDown,
287- layer=LaunchpadFunctionalLayer)
288+ layer=DatabaseFunctionalLayer)
289 suite.addTest(test)
290
291 suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))