Skip to content

Commit 19d3b06

Browse files
committed
Merge pull request #44 from schwuk/comment-subscriptions
Extract comment subscriptions as their own module
2 parents 9d87726 + fdc1a2e commit 19d3b06

File tree

5 files changed

+477
-86
lines changed

5 files changed

+477
-86
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ your `plugins` directory.
2424

2525
Trac Code Comments plugin requres at least python 2.4 and runs on Trac 0.12.
2626

27+
Enable all the modules through the admin web UI or by editing `trac.ini`.
28+
29+
30+
Upgrading
31+
---------
32+
33+
Install the latest version of the plugin (as above).
34+
35+
Run `trac-admin <path-to-environment> upgrade` to update the database.
36+
37+
Enable any new modules through the admin web UI or by editing `trac.ini`.
38+
39+
Run `trac-admin <path-to-environment> subscription seed` to create
40+
subsriptionfor existing attachments, changesets and comments.
41+
42+
2743
Features
2844
--------
2945

@@ -54,6 +70,9 @@ tickets and which are not.
5470
* Notifications – if you have configured Trac to email ticket notifications
5571
then comment notifications will just work!
5672

73+
* Subscriptions – Authors of changesets and attachments, and anyone who
74+
creates a comment are subscribed to notifications of comments; to have changeset authors automatically subscribed, your repositories must be configured for [synchronisation](http://trac.edgewall.org/wiki/TracRepositoryAdmin#Synchronization) with Trac
75+
5776
Screenshots
5877
-----------
5978

code_comments/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from code_comments import comment
2+
from code_comments import comment_macro
23
from code_comments import comments
34
from code_comments import db
45
from code_comments import notification
5-
from code_comments import web
6-
from code_comments import comment_macro
6+
from code_comments import subscription
77
from code_comments import ticket_event_listener
8+
from code_comments import web

code_comments/db.py

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from trac.core import *
1+
from trac.core import Component, implements
22
from trac.db.schema import Table, Column, Index
33
from trac.env import IEnvironmentSetupParticipant
44
from trac.db.api import DatabaseManager
55

66
# Database version identifier for upgrades.
7-
db_version = 2
7+
db_version = 3
88

99
# Database schema
1010
schema = {
@@ -21,61 +21,91 @@
2121
Index(['path']),
2222
Index(['author']),
2323
],
24+
'code_comments_subscriptions': Table('code_comments_subscriptions',
25+
key=('id', 'user', 'type', 'path',
26+
'repos', 'rev'))[
27+
Column('id', auto_increment=True),
28+
Column('user'),
29+
Column('type'),
30+
Column('path'),
31+
Column('repos'),
32+
Column('rev'),
33+
Column('notify', type='bool'),
34+
Index(['user']),
35+
Index(['path']),
36+
],
2437
}
2538

39+
2640
def to_sql(env, table):
2741
""" Convenience function to get the to_sql for the active connector."""
2842
dc = DatabaseManager(env)._get_connector()[0]
2943
return dc.to_sql(table)
3044

45+
3146
def create_tables(env, db):
3247
cursor = db.cursor()
3348
for table_name in schema:
3449
for stmt in to_sql(env, schema[table_name]):
3550
cursor.execute(stmt)
36-
cursor.execute("INSERT into system values ('code_comments_schema_version', %s)",
37-
str(db_version))
51+
cursor.execute(
52+
"INSERT INTO system VALUES ('code_comments_schema_version', %s)",
53+
str(db_version))
54+
3855

3956
# Upgrades
57+
4058
def upgrade_from_1_to_2(env, db):
4159
# Add the new column "type"
4260
@env.with_transaction()
43-
def add_type_column( db ):
61+
def add_type_column(db):
4462
cursor = db.cursor()
45-
cursor.execute( 'ALTER TABLE code_comments ADD COLUMN type TEXT' )
63+
cursor.execute('ALTER TABLE code_comments ADD COLUMN type TEXT')
4664

4765
# Convert all the current comments to the new schema
4866
@env.with_transaction()
49-
def convert_comments( db ):
67+
def convert_comments(db):
5068
comments = {}
5169
cursor = db.cursor()
52-
cursor.execute( 'SELECT id, path FROM code_comments' )
70+
cursor.execute('SELECT id, path FROM code_comments')
5371
comments = cursor.fetchall()
5472
# options:
5573
# 1: comment on file (path != "" && path != "attachment")
5674
# 2: comment on changeset (path == "")
5775
# 3: comment on attachment (path == "attachment")
5876
for comment in comments:
5977
path = comment[1]
60-
is_comment_to_attachment = path.startswith( 'attachment' )
78+
is_comment_to_attachment = path.startswith('attachment')
6179
is_comment_to_file = not is_comment_to_attachment and '' != path
6280
is_comment_to_changeset = '' == path
6381
cursor = db.cursor()
6482
update = 'UPDATE code_comments SET type={0} WHERE id={1}'
6583
sql = ''
6684

6785
if is_comment_to_changeset:
68-
sql = update.format( "'changeset'", str( comment[0] ) )
86+
sql = update.format("'changeset'", str(comment[0]))
6987
elif is_comment_to_attachment:
70-
sql = update.format( "'attachment'", str(comment[0] ) )
88+
sql = update.format("'attachment'", str(comment[0]))
7189
elif is_comment_to_file:
72-
sql = update.format( "'browser'", str(comment[0] ) )
90+
sql = update.format("'browser'", str(comment[0]))
91+
92+
cursor.execute(sql)
93+
94+
95+
def upgrade_from_2_to_3(env, db):
96+
# Add the new table
97+
@env.with_transaction()
98+
def add_subscriptions_table(db):
99+
cursor = db.cursor()
100+
for stmt in to_sql(env, schema['code_comments_subscriptions']):
101+
cursor.execute(stmt)
73102

74-
cursor.execute( sql )
75103

76104
upgrade_map = {
77-
2: upgrade_from_1_to_2
78-
}
105+
2: upgrade_from_1_to_2,
106+
3: upgrade_from_2_to_3,
107+
}
108+
79109

80110
class CodeCommentsSetup(Component):
81111
"""Component that deals with database setup and upgrades."""
@@ -87,13 +117,17 @@ def environment_created(self):
87117
pass
88118

89119
def environment_needs_upgrade(self, db):
90-
"""Called when Trac checks whether the environment needs to be upgraded.
91-
Returns `True` if upgrade is needed, `False` otherwise."""
120+
"""
121+
Called when Trac checks whether the environment needs to be upgraded.
122+
Returns `True` if upgrade is needed, `False` otherwise.
123+
"""
92124
return self._get_version(db) != db_version
93125

94126
def upgrade_environment(self, db):
95-
"""Actually perform an environment upgrade, but don't commit as
96-
that is done by the common upgrade procedure when all plugins are done."""
127+
"""
128+
Actually perform an environment upgrade, but don't commit as
129+
that is done by the common upgrade procedure when all plugins are done.
130+
"""
97131
current_ver = self._get_version(db)
98132
if current_ver == 0:
99133
create_tables(self.env, db)
@@ -102,8 +136,9 @@ def upgrade_environment(self, db):
102136
upgrade_map[current_ver+1](self.env, db)
103137
current_ver += 1
104138
cursor = db.cursor()
105-
cursor.execute("UPDATE system SET value=%s WHERE name='code_comments_schema_version'",
106-
str(db_version))
139+
cursor.execute(
140+
"UPDATE system SET value=%s WHERE name='code_comments_schema_version'",
141+
str(db_version))
107142

108143
def _get_version(self, db):
109144
cursor = db.cursor()

code_comments/notification.py

Lines changed: 9 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
from trac.attachment import Attachment
21
from trac.config import BoolOption
32
from trac.core import Component, implements
43
from trac.notification import NotifyEmail
5-
from trac.resource import ResourceNotFound
6-
from trac.versioncontrol import RepositoryManager, NoSuchChangeset
4+
75
from code_comments.api import ICodeCommentChangeListener
86
from code_comments.comments import Comments
7+
from code_comments.subscription import Subscription
98

109

1110
class CodeCommentChangeListener(Component):
@@ -33,41 +32,6 @@ class CodeCommentNotifyEmail(NotifyEmail):
3332
template_name = "code_comment_notify_email.txt"
3433
from_email = "trac+comments@localhost"
3534

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-
7135
def _get_comment_thread(self, comment):
7236
"""
7337
Returns all comments in the same location as a given comment, sorted
@@ -80,16 +44,6 @@ def _get_comment_thread(self, comment):
8044
'line': comment.line}
8145
return comments.search(args, order_by='id')
8246

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-
9347
def get_recipients(self, comment):
9448
"""
9549
Determine who should receive the notification.
@@ -99,31 +53,23 @@ def get_recipients(self, comment):
9953
Current scheme is as follows:
10054
10155
* 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
56+
to any subscribers to that resource
10457
* 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
58+
sent to the author of the last comment in that location, and any other
59+
subscribers for that resource
10860
"""
10961
torcpts = set()
62+
ccrcpts = set()
11063

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))
64+
for subscription in Subscription.for_comment(self.env, comment,
65+
notify=True):
66+
torcpts.add(subscription.user)
11667

11768
# Is this a reply, or a new comment?
11869
thread = self._get_comment_thread(comment)
11970
if len(thread) > 1:
12071
# The author of the comment before this one
12172
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)
12773

12874
# Should we notify the comment author?
12975
if not self.notify_self:

0 commit comments

Comments
 (0)