Skip to content

Commit 9d87726

Browse files
committed
Merge pull request #42 from schwuk/notifications
Send notifications when comments are created For now send them only to other participants of the thread, but this builds some infrastructure for sending them to more users.
2 parents 9490f89 + 6560040 commit 9d87726

File tree

7 files changed

+223
-3
lines changed

7 files changed

+223
-3
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Installation
1818
Pick an `.egg` file from the Downloads section and place it in the `plugins/`
1919
directory of your Trac install.
2020

21+
Alternatively build your own egg by checking out the repository and running
22+
`python setup.py bdist_egg` in your working copy. Copy the resultant .egg to
23+
your `plugins` directory.
24+
2125
Trac Code Comments plugin requres at least python 2.4 and runs on Trac 0.12.
2226

2327
Features
@@ -47,6 +51,9 @@ ticket.
4751
* Comments/ticket cross-reference – to remember which comments are already in
4852
tickets and which are not.
4953

54+
* Notifications – if you have configured Trac to email ticket notifications
55+
then comment notifications will just work!
56+
5057
Screenshots
5158
-----------
5259

code_comments/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from code_comments import comment
22
from code_comments import comments
33
from code_comments import db
4+
from code_comments import notification
45
from code_comments import web
56
from code_comments import comment_macro
6-
from code_comments import ticket_event_listener
7+
from code_comments import ticket_event_listener

code_comments/api.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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)
13+
14+
def comment_created(self, comment):
15+
"""
16+
Emits comment_created event to all listeners.
17+
"""
18+
for listener in self.change_listeners:
19+
listener.comment_created(comment)

code_comments/comments.py

Lines changed: 5 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,8 @@ 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+
CodeCommentSystem(self.env).comment_created(
123+
Comments(self.req, self.env).by_id(comment_id[0]))
124+
120125
return comment_id[0]

code_comments/notification.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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+
return (torcpts, ccrcpts)
137+
138+
def _get_author_name(self, comment):
139+
"""
140+
Get the real name of the user who made the comment. If it cannot be
141+
determined, return their username.
142+
"""
143+
for username, name, email in self.env.get_known_users():
144+
if username == comment.author and name:
145+
return name
146+
147+
return comment.author
148+
149+
def notify(self, comment):
150+
self.comment_author = self._get_author_name(comment)
151+
152+
self.data.update({
153+
"comment": comment,
154+
"comment_url": self.env.abs_href() + comment.href(),
155+
"project_url": self.env.project_url or self.env.abs_href(),
156+
})
157+
158+
projname = self.config.get("project", "name")
159+
subject = "Re: [%s] %s" % (projname, comment.link_text())
160+
161+
try:
162+
NotifyEmail.notify(self, comment, subject)
163+
except Exception, e:
164+
self.env.log.error("Failure sending notification on creation of "
165+
"comment #%d: %s", comment.id, e)
166+
167+
def send(self, torcpts, ccrcpts):
168+
"""
169+
Override NotifyEmail.send() so we can provide from_name.
170+
"""
171+
self.from_name = self.comment_author
172+
NotifyEmail.send(self, torcpts, ccrcpts)
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: ${comment_url}
4+
5+
--
6+
${project.name} <${project_url}>
7+
${project.descr}

setup.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,19 @@
66
author_email='nikolay@automattic.com, tott@automattic.com',
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',
1212
],
1313
},
14-
package_data = {'code_comments': ['templates/*.html', 'templates/js/*.html', 'htdocs/*.*','htdocs/jquery-ui/*.*', 'htdocs/jquery-ui/images/*.*', 'htdocs/sort/*.*']},
14+
package_data={
15+
'code_comments': [
16+
'templates/*.html',
17+
'templates/js/*.html',
18+
'htdocs/*.*',
19+
'htdocs/jquery-ui/*.*',
20+
'htdocs/jquery-ui/images/*.*',
21+
'htdocs/sort/*.*',
22+
],
23+
},
1524
)

0 commit comments

Comments
 (0)