Skip to content

Commit ee51e04

Browse files
committed
Send notifications when comments are created
Added a new interface - ICodeCommentChangeListener - to use for notifications. Added a new component - CodeCommentSystem - to act as the extension point for the above interface. (This could have been added to one of the other components, but I have plans for other refactorings and wanted a clean base for them.) Extended Comments.create() to trigger comment_created events. Added a new component - CodeCommentChangeListener - to respond to comment_created events. Sub-classed trac.notification.NotifyEmail to generate notification emails. Notifications are sent to the author of the resouce being commented on, and anyone else who has commented on the same resource. There is additional logic to determine who the notification should be *sent* and *copied* to. Added an option to control whether notifications are sent to the person making the comment (off by default). Removed unused variable.
1 parent 7e475ca commit ee51e04

File tree

5 files changed

+207
-2
lines changed

5 files changed

+207
-2
lines changed

code_comments/api.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from trac.core import Component, ExtensionPoint, Interface
2+
3+
4+
class ICodeCommentChangeListener(Interface):
5+
"""An interface for receiving comment change events."""
6+
7+
def comment_created(comment):
8+
"""New comment created."""
9+
10+
11+
class CodeCommentSystem(Component):
12+
change_listeners = ExtensionPoint(ICodeCommentChangeListener)

code_comments/comments.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os.path
22
from time import time
3+
from code_comments.api import CodeCommentSystem
34
from code_comments.comment import Comment
45

56
class Comments:
@@ -117,4 +118,9 @@ def insert_comment(db):
117118
self.env.log.debug(sql)
118119
cursor.execute(sql, values)
119120
comment_id[0] = db.get_last_id(cursor, 'code_comments')
121+
122+
for listener in CodeCommentSystem(self.env).change_listeners:
123+
listener.comment_created(
124+
Comments(self.req, self.env).by_id(comment_id[0]))
125+
120126
return comment_id[0]

code_comments/notification.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
from trac.attachment import Attachment
2+
from trac.config import BoolOption
3+
from trac.core import Component, implements
4+
from trac.notification import NotifyEmail
5+
from trac.resource import ResourceNotFound
6+
from trac.versioncontrol import RepositoryManager, NoSuchChangeset
7+
from code_comments.api import ICodeCommentChangeListener
8+
from code_comments.comments import Comments
9+
10+
11+
class CodeCommentChangeListener(Component):
12+
"""
13+
Sends email notifications when comments have been created.
14+
"""
15+
implements(ICodeCommentChangeListener)
16+
17+
# ICodeCommentChangeListener methods
18+
19+
def comment_created(self, comment):
20+
notifier = CodeCommentNotifyEmail(self.env)
21+
notifier.notify(comment)
22+
23+
24+
class CodeCommentNotifyEmail(NotifyEmail):
25+
"""
26+
Sends code comment notifications by email.
27+
"""
28+
29+
notify_self = BoolOption('code_comments', 'notify_self', False,
30+
doc="Send comment notifications to the author of "
31+
"the comment.")
32+
33+
template_name = "code_comment_notify_email.txt"
34+
from_email = "trac+comments@localhost"
35+
36+
def _get_attachment_author(self, parent, parent_id, filename):
37+
"""
38+
Returns the author of a given attachment.
39+
"""
40+
try:
41+
attachment = Attachment(self.env, parent, parent_id, filename)
42+
return attachment.author
43+
except ResourceNotFound:
44+
self.env.log.debug("Invalid attachment, unable to determine "
45+
"author.")
46+
47+
def _get_changeset_author(self, revision, reponame=None):
48+
"""
49+
Returns the author of a changeset for a given revision.
50+
"""
51+
try:
52+
repos = RepositoryManager(self.env).get_repository(reponame)
53+
changeset = repos.get_changeset(revision)
54+
return changeset.author
55+
except NoSuchChangeset:
56+
self.env.log.debug("Invalid changeset, unable to determine author")
57+
58+
def _get_original_author(self, comment):
59+
"""
60+
Returns the author for the target of a given comment.
61+
"""
62+
if comment.type == 'attachment':
63+
parent, parent_id, filename = comment.path.split("/")[1:]
64+
return self._get_attachment_author(parent, parent_id,
65+
filename)
66+
elif (comment.type == 'changeset' or comment.type == "browser"):
67+
# TODO: When support is added for multiple repositories, this
68+
# will need updated
69+
return self._get_changeset_author(comment.revision)
70+
71+
def _get_comment_thread(self, comment):
72+
"""
73+
Returns all comments in the same location as a given comment, sorted
74+
in order of id.
75+
"""
76+
comments = Comments(None, self.env)
77+
args = {'type': comment.type,
78+
'revision': comment.revision,
79+
'path': comment.path,
80+
'line': comment.line}
81+
return comments.search(args, order_by='id')
82+
83+
def _get_commenters(self, comment):
84+
"""
85+
Returns a list of all commenters for the same thing.
86+
"""
87+
comments = Comments(None, self.env)
88+
args = {'type': comment.type,
89+
'revision': comment.revision,
90+
'path': comment.path}
91+
return comments.get_all_comment_authors(comments.search(args))
92+
93+
def get_recipients(self, comment):
94+
"""
95+
Determine who should receive the notification.
96+
97+
Required by NotifyEmail.
98+
99+
Current scheme is as follows:
100+
101+
* For the first comment in a given location, the notification is sent
102+
'to' the original author of the thing being commented on, and 'copied'
103+
to the authors of any other comments on that thing
104+
* For any further comments in a given location, the notification is
105+
sent 'to' the author of the last comment in that location, and
106+
'copied' to both the original author of the thing and the authors of
107+
any other comments on that thing
108+
"""
109+
torcpts = set()
110+
111+
# Get the original author
112+
original_author = self._get_original_author(comment)
113+
114+
# Get other commenters
115+
ccrcpts = set(self._get_commenters(comment))
116+
117+
# Is this a reply, or a new comment?
118+
thread = self._get_comment_thread(comment)
119+
if len(thread) > 1:
120+
# The author of the comment before this one
121+
torcpts.add(thread[-2].author)
122+
# Copy to the original author
123+
ccrcpts.add(original_author)
124+
else:
125+
# This is the first comment in this thread
126+
torcpts.add(original_author)
127+
128+
# Should we notify the comment author?
129+
if not self.notify_self:
130+
torcpts = torcpts.difference([comment.author])
131+
ccrcpts = ccrcpts.difference([comment.author])
132+
133+
# Remove duplicates
134+
ccrcpts = ccrcpts.difference(torcpts)
135+
136+
self.env.log.debug("Sending notification to: %s" % torcpts)
137+
self.env.log.debug("Copying notification to: %s" % ccrcpts)
138+
139+
return (torcpts, ccrcpts)
140+
141+
def notify(self, comment):
142+
from_name = comment.author
143+
144+
# See if we can get a real name for the comment author
145+
for username, name, email in self.env.get_known_users():
146+
if username == comment.author and name:
147+
from_name = name
148+
149+
self.data.update({
150+
"comment": comment,
151+
})
152+
153+
projname = self.config.get("project", "name")
154+
subject = "Re: [%s] %s" % (projname, comment.link_text())
155+
156+
# Temporarily switch the smtp_from_name setting so we can pretend
157+
# the mail came from the author of the comment
158+
try:
159+
self.env.log.debug("Changing smtp_from_name to %s" % from_name)
160+
old_setting = self.config['notification'].get('smtp_from_name')
161+
self.config.set('notification', 'smtp_from_name', from_name)
162+
try:
163+
NotifyEmail.notify(self, comment, subject)
164+
except:
165+
pass
166+
finally:
167+
self.env.log.debug("Changing smtp_from_name back to %s" %
168+
old_setting)
169+
self.config.set('notification', 'smtp_from_name', old_setting)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
${comment.text}
2+
3+
View the comment: ${project.url or abs_href()}${comment.href()}
4+
5+
--
6+
${project.name} <${project.url or abs_href()}>
7+
${project.descr}

setup.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,21 @@
66
77
description='Tool for leaving inline code comments',
88
packages=find_packages(exclude=['*.tests*']),
9-
entry_points = {
9+
entry_points={
1010
'trac.plugins': [
1111
'code_comments = code_comments',
12+
'code_comments.api = code_comments.api',
13+
'code_comments.notification = code_comments.notification',
14+
],
15+
},
16+
package_data={
17+
'code_comments': [
18+
'templates/*.html',
19+
'templates/js/*.html',
20+
'htdocs/*.*',
21+
'htdocs/jquery-ui/*.*',
22+
'htdocs/jquery-ui/images/*.*',
23+
'htdocs/sort/*.*',
1224
],
1325
},
14-
package_data = {'code_comments': ['templates/*.html', 'templates/js/*.html', 'htdocs/*.*','htdocs/jquery-ui/*.*', 'htdocs/jquery-ui/images/*.*', 'htdocs/sort/*.*']},
1526
)

0 commit comments

Comments
 (0)