8687.15.11
by Karl Fogel
Add the copyright header block to files under lib/lp/answers/. |
1 |
# Copyright 2009 Canonical Ltd. This software is licensed under the
|
2 |
# GNU Affero General Public License version 3 (see the file LICENSE).
|
|
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
3 |
|
4 |
"""Notifications for the Answers system."""
|
|
5 |
||
6 |
__metaclass__ = type |
|
7 |
__all__ = [ |
|
8 |
'QuestionNotification', |
|
9 |
]
|
|
10 |
||
11 |
import os |
|
12 |
||
12952.5.1
by Curtis Hovey
Added recipient_set property to QuestionAddedNotification. |
13 |
from zope.component import getUtility |
14 |
||
14605.1.1
by Curtis Hovey
Moved canonical.config to lp.services. |
15 |
from lp.services.config import config |
14600.2.2
by Curtis Hovey
Moved webapp to lp.services. |
16 |
from lp.services.webapp.publisher import canonical_url |
12952.5.1
by Curtis Hovey
Added recipient_set property to QuestionAddedNotification. |
17 |
from lp.answers.enums import ( |
18 |
QuestionAction, |
|
19 |
QuestionRecipientSet, |
|
20 |
)
|
|
21 |
from lp.answers.interfaces.questionjob import IQuestionEmailJobSource |
|
11403.1.4
by Henning Eggers
Reformatted imports using format-imports script r32. |
22 |
from lp.registry.interfaces.person import IPerson |
23 |
from lp.services.mail.mailwrapper import MailWrapper |
|
11382.6.34
by Gavin Panella
Reformat imports in all files touched so far. |
24 |
from lp.services.propertycache import cachedproperty |
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
25 |
|
26 |
||
27 |
def get_email_template(filename): |
|
28 |
"""Returns the email template with the given file name.
|
|
29 |
||
30 |
The templates are located in 'emailtemplates'.
|
|
31 |
"""
|
|
32 |
base = os.path.dirname(__file__) |
|
33 |
fullpath = os.path.join(base, 'emailtemplates', filename) |
|
34 |
return open(fullpath).read() |
|
35 |
||
36 |
||
37 |
class QuestionNotification: |
|
38 |
"""Base class for a notification related to a question.
|
|
39 |
||
40 |
Creating an instance of that class will build the notification and
|
|
41 |
send it to the appropriate recipients. That way, subclasses of
|
|
42 |
QuestionNotification can be registered as event subscribers.
|
|
43 |
"""
|
|
44 |
||
12952.5.9
by Curtis Hovey
Moved recipient_set to the base class. |
45 |
recipient_set = QuestionRecipientSet.ASKER_SUBSCRIBER |
12952.5.1
by Curtis Hovey
Added recipient_set property to QuestionAddedNotification. |
46 |
|
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
47 |
def __init__(self, question, event): |
48 |
"""Base constructor.
|
|
49 |
||
50 |
It saves the question and event in attributes and then call
|
|
51 |
the initialize() and send() method.
|
|
52 |
"""
|
|
53 |
self.question = question |
|
54 |
self.event = event |
|
12906.1.2
by Curtis Hovey
Always send notfications about added questions as though they come from the owner. |
55 |
self._user = IPerson(self.event.user) |
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
56 |
self.initialize() |
12952.5.5
by Curtis Hovey
Save point for incomplete test. Need base branch merged to complete. |
57 |
self.job = None |
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
58 |
if self.shouldNotify(): |
12952.5.5
by Curtis Hovey
Save point for incomplete test. Need base branch merged to complete. |
59 |
self.job = self.enqueue() |
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
60 |
|
12906.1.2
by Curtis Hovey
Always send notfications about added questions as though they come from the owner. |
61 |
@property
|
62 |
def user(self): |
|
63 |
"""Return the user from the event. """
|
|
64 |
return self._user |
|
65 |
||
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
66 |
def getSubject(self): |
67 |
"""Return the subject of the notification.
|
|
68 |
||
69 |
Default to [Question #dd]: Title
|
|
70 |
"""
|
|
71 |
return '[Question #%s]: %s' % (self.question.id, self.question.title) |
|
72 |
||
73 |
def getBody(self): |
|
74 |
"""Return the content of the notification message.
|
|
75 |
||
76 |
This method must be implemented by a subclass.
|
|
77 |
"""
|
|
78 |
raise NotImplementedError |
|
79 |
||
80 |
def getHeaders(self): |
|
81 |
"""Return additional headers to add to the email.
|
|
82 |
||
83 |
Default implementation adds a X-Launchpad-Question header.
|
|
84 |
"""
|
|
85 |
question = self.question |
|
86 |
headers = dict() |
|
87 |
if self.question.distribution: |
|
88 |
if question.sourcepackagename: |
|
89 |
sourcepackage = question.sourcepackagename.name |
|
90 |
else: |
|
91 |
sourcepackage = 'None' |
|
92 |
target = 'distribution=%s; sourcepackage=%s;' % ( |
|
93 |
question.distribution.name, sourcepackage) |
|
94 |
else: |
|
95 |
target = 'product=%s;' % question.product.name |
|
96 |
if question.assignee: |
|
97 |
assignee = question.assignee.name |
|
98 |
else: |
|
99 |
assignee = 'None' |
|
100 |
||
101 |
headers['X-Launchpad-Question'] = ( |
|
102 |
'%s status=%s; assignee=%s; priority=%s; language=%s' % ( |
|
103 |
target, question.status.title, assignee, |
|
104 |
question.priority.title, question.language.code)) |
|
105 |
headers['Reply-To'] = 'question%s@%s' % ( |
|
106 |
self.question.id, config.answertracker.email_domain) |
|
107 |
||
108 |
return headers |
|
109 |
||
110 |
def initialize(self): |
|
111 |
"""Initialization hook for subclasses.
|
|
112 |
||
113 |
This method is called before send() and can be use for any
|
|
114 |
setup purpose.
|
|
115 |
||
116 |
Default does nothing.
|
|
117 |
"""
|
|
118 |
pass
|
|
119 |
||
120 |
def shouldNotify(self): |
|
121 |
"""Return if there is something to notify about.
|
|
122 |
||
123 |
When this method returns False, no notification will be sent.
|
|
124 |
By default, all event trigger a notification.
|
|
125 |
"""
|
|
126 |
return True |
|
127 |
||
12952.5.1
by Curtis Hovey
Added recipient_set property to QuestionAddedNotification. |
128 |
def enqueue(self): |
129 |
"""Create a job to send email about the event."""
|
|
12952.5.5
by Curtis Hovey
Save point for incomplete test. Need base branch merged to complete. |
130 |
subject = self.getSubject() |
131 |
body = self.getBody() |
|
132 |
headers = self.getHeaders() |
|
133 |
job_source = getUtility(IQuestionEmailJobSource) |
|
134 |
job = job_source.create( |
|
135 |
self.question, self.user, self.recipient_set, |
|
136 |
subject, body, headers) |
|
137 |
return job |
|
12952.5.1
by Curtis Hovey
Added recipient_set property to QuestionAddedNotification. |
138 |
|
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
139 |
@property
|
140 |
def unsupported_language(self): |
|
141 |
"""Whether the question language is unsupported or not."""
|
|
142 |
supported_languages = self.question.target.getSupportedLanguages() |
|
143 |
return self.question.language not in supported_languages |
|
144 |
||
145 |
@property
|
|
146 |
def unsupported_language_warning(self): |
|
147 |
"""Warning about the fact that the question is written in an
|
|
148 |
unsupported language."""
|
|
149 |
return get_email_template( |
|
150 |
'question-unsupported-language-warning.txt') % { |
|
151 |
'question_language': self.question.language.englishname, |
|
152 |
'target_name': self.question.target.displayname} |
|
153 |
||
154 |
||
155 |
class QuestionAddedNotification(QuestionNotification): |
|
156 |
"""Notification sent when a question is added."""
|
|
157 |
||
12906.1.2
by Curtis Hovey
Always send notfications about added questions as though they come from the owner. |
158 |
@property
|
159 |
def user(self): |
|
160 |
"""Return the question owner.
|
|
161 |
||
162 |
Questions can be created by other users for the owner; the
|
|
163 |
question is from the owner.
|
|
164 |
"""
|
|
165 |
return self.question.owner |
|
166 |
||
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
167 |
def getBody(self): |
168 |
"""See QuestionNotification."""
|
|
169 |
question = self.question |
|
170 |
body = get_email_template('question-added-notification.txt') % { |
|
171 |
'target_name': question.target.displayname, |
|
172 |
'question_id': question.id, |
|
173 |
'question_url': canonical_url(question), |
|
174 |
'comment': question.description} |
|
175 |
if self.unsupported_language: |
|
176 |
body += self.unsupported_language_warning |
|
177 |
return body |
|
178 |
||
179 |
||
180 |
class QuestionModifiedDefaultNotification(QuestionNotification): |
|
181 |
"""Base implementation of a notification when a question is modified."""
|
|
182 |
||
12952.5.2
by Curtis Hovey
Added recipient_set to QuestionModifiedDefaultNotification. |
183 |
recipient_set = QuestionRecipientSet.SUBSCRIBER |
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
184 |
# Email template used to render the body.
|
185 |
body_template = "question-modified-notification.txt" |
|
186 |
||
187 |
def initialize(self): |
|
188 |
"""Save the old question for comparison. It also set the new_message
|
|
189 |
attribute if a new message was added.
|
|
190 |
"""
|
|
191 |
self.old_question = self.event.object_before_modification |
|
192 |
||
193 |
new_messages = set( |
|
194 |
self.question.messages).difference(self.old_question.messages) |
|
195 |
assert len(new_messages) <= 1, ( |
|
196 |
"There shouldn't be more than one message for a "
|
|
197 |
"notification.") |
|
198 |
if new_messages: |
|
199 |
self.new_message = new_messages.pop() |
|
200 |
else: |
|
201 |
self.new_message = None |
|
202 |
||
203 |
self.wrapper = MailWrapper() |
|
204 |
||
205 |
@cachedproperty
|
|
206 |
def metadata_changes_text(self): |
|
207 |
"""Textual representation of the changes to the question metadata."""
|
|
208 |
question = self.question |
|
209 |
old_question = self.old_question |
|
12906.1.2
by Curtis Hovey
Always send notfications about added questions as though they come from the owner. |
210 |
indent = 4 * ' ' |
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
211 |
info_fields = [] |
212 |
if question.status != old_question.status: |
|
213 |
info_fields.append(indent + 'Status: %s => %s' % ( |
|
214 |
old_question.status.title, question.status.title)) |
|
215 |
if question.target != old_question.target: |
|
216 |
info_fields.append( |
|
217 |
indent + 'Project: %s => %s' % ( |
|
218 |
old_question.target.displayname, question.target.displayname)) |
|
219 |
||
8711.2.2
by Curtis Hovey
Assigning a question send a notifcation message. |
220 |
if question.assignee != old_question.assignee: |
221 |
if old_question.assignee is None: |
|
222 |
old_assignee = None |
|
223 |
else: |
|
224 |
old_assignee = old_question.assignee.displayname |
|
225 |
if question.assignee is None: |
|
226 |
assignee = None |
|
227 |
else: |
|
228 |
assignee = question.assignee.displayname |
|
229 |
info_fields.append(indent + 'Assignee: %s => %s' % ( |
|
230 |
old_assignee, assignee)) |
|
231 |
||
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
232 |
old_bugs = set(old_question.bugs) |
233 |
bugs = set(question.bugs) |
|
234 |
for linked_bug in bugs.difference(old_bugs): |
|
235 |
info_fields.append( |
|
236 |
indent + 'Linked to bug: #%s\n' % linked_bug.id + |
|
237 |
indent + '%s\n' % canonical_url(linked_bug) + |
|
238 |
indent + '"%s"' % linked_bug.title) |
|
239 |
for unlinked_bug in old_bugs.difference(bugs): |
|
240 |
info_fields.append( |
|
241 |
indent + 'Removed link to bug: #%s\n' % unlinked_bug.id + |
|
242 |
indent + '%s\n' % canonical_url(unlinked_bug) + |
|
243 |
indent + '"%s"' % unlinked_bug.title) |
|
244 |
||
245 |
if question.faq != old_question.faq: |
|
246 |
if question.faq is None: |
|
247 |
info_fields.append( |
|
248 |
indent + 'Related FAQ was removed:\n' + |
|
249 |
indent + old_question.faq.title + '\n' + |
|
250 |
indent + canonical_url(old_question.faq)) |
|
251 |
else: |
|
252 |
info_fields.append( |
|
253 |
indent + 'Related FAQ set to:\n' + |
|
254 |
indent + question.faq.title + '\n' + |
|
255 |
indent + canonical_url(question.faq)) |
|
256 |
||
257 |
if question.title != old_question.title: |
|
258 |
info_fields.append('Summary changed to:\n%s' % question.title) |
|
259 |
if question.description != old_question.description: |
|
260 |
info_fields.append( |
|
261 |
'Description changed to:\n%s' % ( |
|
262 |
self.wrapper.format(question.description))) |
|
263 |
||
264 |
question_changes = '\n\n'.join(info_fields) |
|
265 |
return question_changes |
|
266 |
||
267 |
def getSubject(self): |
|
12906.2.4
by Curtis Hovey
Removed the rules to preserve or rewite a question subject line from email. |
268 |
"""The reply subject line."""
|
269 |
line = super(QuestionModifiedDefaultNotification, self).getSubject() |
|
270 |
return 'Re: %s' % line |
|
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
271 |
|
272 |
def getHeaders(self): |
|
273 |
"""Add a References header."""
|
|
274 |
headers = QuestionNotification.getHeaders(self) |
|
275 |
if self.new_message: |
|
276 |
# XXX flacoste 2007-02-02 bug=83846:
|
|
277 |
# The first message cannot contain a References
|
|
278 |
# because we don't create a Message instance for the
|
|
279 |
# question description, so we don't have a Message-ID.
|
|
280 |
messages = list(self.question.messages) |
|
281 |
assert self.new_message in messages, ( |
|
282 |
"Question %s: message id %s not in %s." % ( |
|
283 |
self.question.id, self.new_message.id, |
|
284 |
[m.id for m in messages])) |
|
285 |
index = messages.index(self.new_message) |
|
286 |
if index > 0: |
|
287 |
headers['References'] = ( |
|
12906.1.2
by Curtis Hovey
Always send notfications about added questions as though they come from the owner. |
288 |
self.question.messages[index - 1].rfc822msgid) |
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
289 |
return headers |
290 |
||
291 |
def shouldNotify(self): |
|
292 |
"""Only send a notification when a message was added or some
|
|
293 |
metadata was changed.
|
|
294 |
"""
|
|
295 |
return self.new_message or self.metadata_changes_text |
|
296 |
||
297 |
def getBody(self): |
|
298 |
"""See QuestionNotification."""
|
|
299 |
body = self.metadata_changes_text |
|
300 |
replacements = dict( |
|
301 |
question_id=self.question.id, |
|
302 |
target_name=self.question.target.displayname, |
|
303 |
question_url=canonical_url(self.question)) |
|
304 |
||
305 |
if self.new_message: |
|
306 |
if body: |
|
307 |
body += '\n\n' |
|
308 |
body += self.getNewMessageText() |
|
309 |
replacements['new_message_id'] = list( |
|
310 |
self.question.messages).index(self.new_message) |
|
311 |
||
312 |
replacements['body'] = body |
|
313 |
||
314 |
return get_email_template(self.body_template) % replacements |
|
315 |
||
316 |
# Header template used when a new message is added to the question.
|
|
317 |
action_header_template = { |
|
318 |
QuestionAction.REQUESTINFO: |
|
12906.2.1
by Curtis Hovey
Fixed grammar. |
319 |
'%(person)s requested more information:', |
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
320 |
QuestionAction.CONFIRM: |
321 |
'%(person)s confirmed that the question is solved:', |
|
322 |
QuestionAction.COMMENT: |
|
323 |
'%(person)s posted a new comment:', |
|
324 |
QuestionAction.GIVEINFO: |
|
325 |
'%(person)s gave more information on the question:', |
|
326 |
QuestionAction.REOPEN: |
|
327 |
'%(person)s is still having a problem:', |
|
328 |
QuestionAction.ANSWER: |
|
329 |
'%(person)s proposed the following answer:', |
|
330 |
QuestionAction.EXPIRE: |
|
331 |
'%(person)s expired the question:', |
|
332 |
QuestionAction.REJECT: |
|
333 |
'%(person)s rejected the question:', |
|
334 |
QuestionAction.SETSTATUS: |
|
335 |
'%(person)s changed the question status:', |
|
336 |
}
|
|
337 |
||
338 |
def getNewMessageText(self): |
|
339 |
"""Should return the notification text related to a new message."""
|
|
340 |
if not self.new_message: |
|
341 |
return '' |
|
342 |
||
343 |
header = self.action_header_template.get( |
|
344 |
self.new_message.action, '%(person)s posted a new message:') % { |
|
345 |
'person': self.new_message.owner.displayname} |
|
346 |
||
347 |
return '\n'.join([ |
|
348 |
header, self.wrapper.format(self.new_message.text_contents)]) |
|
349 |
||
350 |
||
351 |
class QuestionModifiedOwnerNotification(QuestionModifiedDefaultNotification): |
|
352 |
"""Notification sent to the owner when his question is modified."""
|
|
353 |
||
12952.5.3
by Curtis Hovey
Added recipient_set to QuestionModifiedOwnerNotification. |
354 |
recipient_set = QuestionRecipientSet.ASKER |
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
355 |
# These actions will be done by the owner, so use the second person.
|
356 |
action_header_template = dict( |
|
357 |
QuestionModifiedDefaultNotification.action_header_template) |
|
358 |
action_header_template.update({ |
|
359 |
QuestionAction.CONFIRM: |
|
360 |
'You confirmed that the question is solved:', |
|
361 |
QuestionAction.GIVEINFO: |
|
362 |
'You gave more information on the question:', |
|
363 |
QuestionAction.REOPEN: |
|
364 |
'You are still having a problem:', |
|
365 |
})
|
|
366 |
||
367 |
body_template = 'question-modified-owner-notification.txt' |
|
368 |
||
369 |
body_template_by_action = { |
|
370 |
QuestionAction.ANSWER: "question-answered-owner-notification.txt", |
|
371 |
QuestionAction.EXPIRE: "question-expired-owner-notification.txt", |
|
372 |
QuestionAction.REJECT: "question-rejected-owner-notification.txt", |
|
373 |
QuestionAction.REQUESTINFO: ( |
|
374 |
"question-info-requested-owner-notification.txt"), |
|
375 |
}
|
|
376 |
||
377 |
def initialize(self): |
|
378 |
"""Set the template based on the new comment action."""
|
|
379 |
QuestionModifiedDefaultNotification.initialize(self) |
|
380 |
if self.new_message: |
|
381 |
self.body_template = self.body_template_by_action.get( |
|
382 |
self.new_message.action, self.body_template) |
|
383 |
||
384 |
def getBody(self): |
|
385 |
"""See QuestionNotification."""
|
|
386 |
body = QuestionModifiedDefaultNotification.getBody(self) |
|
387 |
if self.unsupported_language: |
|
388 |
body += self.unsupported_language_warning |
|
389 |
return body |
|
390 |
||
391 |
||
392 |
class QuestionUnsupportedLanguageNotification(QuestionNotification): |
|
393 |
"""Notification sent to answer contacts for unsupported languages."""
|
|
394 |
||
12952.5.4
by Curtis Hovey
Added recipient_set to QuestionUnsupportedLanguageNotification. |
395 |
recipient_set = QuestionRecipientSet.CONTACT |
396 |
||
7944.3.11
by Francis J. Lacoste
Moved ZCML, templates and adapters. Split out karma and notifications. |
397 |
def getSubject(self): |
398 |
"""See QuestionNotification."""
|
|
399 |
return '[Question #%s]: (%s) %s' % ( |
|
400 |
self.question.id, self.question.language.englishname, |
|
401 |
self.question.title) |
|
402 |
||
403 |
def shouldNotify(self): |
|
404 |
"""Return True when the question is in an unsupported language."""
|
|
405 |
return self.unsupported_language |
|
406 |
||
407 |
def getBody(self): |
|
408 |
"""See QuestionNotification."""
|
|
409 |
question = self.question |
|
410 |
return get_email_template( |
|
411 |
'question-unsupported-languages-added.txt') % { |
|
412 |
'target_name': question.target.displayname, |
|
413 |
'question_id': question.id, |
|
414 |
'question_url': canonical_url(question), |
|
415 |
'question_language': question.language.englishname, |
|
416 |
'comment': question.description} |