Skip to content

Commit f7f206e

Browse files
committed
Added trac-admin command to seed subscriptions
Circular dependencies prevented the database upgrade from _nicely_ handling the creation of subscriptions for existing resources, so all logic and code has been moved to a new `subscription` module. Various routines for creating subscriptions have been extracted into standalone methods, and the code refactored to use objects instead of direct database queries.
1 parent 7931e83 commit f7f206e

4 files changed

Lines changed: 189 additions & 104 deletions

File tree

README.md

Lines changed: 16 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

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: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from trac.db.schema import Table, Column, Index
33
from trac.env import IEnvironmentSetupParticipant
44
from trac.db.api import DatabaseManager
5-
from trac.versioncontrol import RepositoryManager
65

76
# Database version identifier for upgrades.
87
db_version = 3
@@ -102,107 +101,6 @@ def add_subscriptions_table(db):
102101
for stmt in to_sql(env, schema['code_comments_subscriptions']):
103102
cursor.execute(stmt)
104103

105-
@env.with_transaction()
106-
def add_attachment_subscriptions(db):
107-
"""
108-
Create a subscription for all existing attachments.
109-
"""
110-
cursor = db.cursor()
111-
cursor.execute("SELECT type, id, filename, author FROM attachment")
112-
attachments = cursor.fetchall()
113-
for attachment in attachments:
114-
sub = {
115-
'user': attachment[3],
116-
'role': 'author',
117-
'type': 'attachment',
118-
'path': "/{0}/{1}/{2}".format(*attachment),
119-
'repos': '',
120-
'rev': '',
121-
'notify': 'always',
122-
}
123-
add_subscription(env, **sub)
124-
125-
@env.with_transaction()
126-
def add_revision_subscriptions(db):
127-
"""
128-
Create a subscription for all existing revisions.
129-
"""
130-
cursor = db.cursor()
131-
cursor.execute("SELECT repos, rev, author FROM revision")
132-
revisions = cursor.fetchall()
133-
for revision in revisions:
134-
sub = {
135-
'user': revision[2],
136-
'role': 'author',
137-
'type': 'revision',
138-
'path': '',
139-
'repos': revision[0],
140-
'rev': revision[1],
141-
'notify': 'always',
142-
}
143-
add_subscription(env, **sub)
144-
145-
@env.with_transaction()
146-
def add_comment_subscriptions(db):
147-
"""
148-
Create a subscription for all existing comments.
149-
"""
150-
cursor = db.cursor()
151-
cursor.execute("SELECT DISTINCT author, type, path, revision FROM "
152-
"code_comments")
153-
comments = cursor.fetchall()
154-
for comment in comments:
155-
sub = {
156-
'user': comment[0],
157-
'role': 'commenter',
158-
'type': comment[1],
159-
'notify': 'always',
160-
}
161-
162-
# Munge attachments
163-
if sub['type'] == 'attachment':
164-
sub['path'] = comment[2].split(':')[1]
165-
sub['repos'] = ''
166-
sub['rev'] = ''
167-
168-
# Munge changesets and browser
169-
if sub['type'] == 'changeset' or sub['type'] == 'browser':
170-
sub['type'] = 'revision'
171-
sub['path'] = comment[2]
172-
repo = RepositoryManager(env).get_repository(None)
173-
try:
174-
sub['repos'] = repo.id
175-
sub['rev'] = repo.db_rev(int(comment[3]))
176-
finally:
177-
repo.close()
178-
179-
add_subscription(env, **sub)
180-
181-
182-
def add_subscription(env, **kwargs):
183-
"""
184-
Helper method for create a code comment subscription.
185-
"""
186-
@env.with_transaction()
187-
def do_add_subscription(db):
188-
cursor = db.cursor()
189-
# We need to avoid creating duplicate subscriptions here due to
190-
# the transaction (duplicates will raise an IntegrityError)
191-
query = ("SELECT COUNT(*) FROM code_comments_subscriptions WHERE "
192-
"user = '{user}' AND type = '{type}' AND path = '{path}' "
193-
"AND repos = '{repos}' AND rev = '{rev}' AND "
194-
"notify = '{notify}'").format(**kwargs)
195-
cursor.execute(query)
196-
already_subscribed = cursor.fetchone()[0]
197-
if not already_subscribed:
198-
sql = ("INSERT INTO code_comments_subscriptions ('user', "
199-
"'role', 'type', 'path', 'repos', 'rev', 'notify') "
200-
"VALUES ('{user}', '{role}', '{type}', '{path}', "
201-
"'{repos}', '{rev}', '{notify}')").format(**kwargs)
202-
cursor.execute(sql)
203-
env.log.info(
204-
"Subscribing {user} to {type} as {role}.".format(**kwargs))
205-
206104

207105
upgrade_map = {
208106
2: upgrade_from_1_to_2,

code_comments/subscription.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
from trac.admin import IAdminCommandProvider
2+
from trac.core import Component, implements
3+
from trac.versioncontrol import RepositoryManager, NoSuchChangeset
4+
5+
from code_comments.comments import Comments
6+
7+
8+
def create_subscription(env, user, role, type_, path, rev, repos=None,
9+
notify='always'):
10+
"""
11+
Create a code comment subscription for a given user.
12+
13+
:param str user: The user to subscribe
14+
:param str role: The type of subscription: "author" or "commenter"
15+
:param str_ type: What the subscription is to e.g., "attachment"
16+
:param path: The path of the subscription
17+
:type path: str or None
18+
:param repos: The name of repository
19+
:type repos: str or None
20+
:param rev: The revision
21+
:type repos: int or None
22+
:param str notify: Whether the subscription issues a notification
23+
("always") or not ("never")
24+
:return: The id of the new subscription
25+
:rtype: int
26+
"""
27+
sub = {
28+
'user': user,
29+
'role': role,
30+
'type': type_,
31+
'path': path or '',
32+
'notify': notify,
33+
}
34+
35+
if type_ in ('changeset', 'browser'):
36+
_repo = RepositoryManager(env).get_repository(repos)
37+
try:
38+
sub['repos'] = _repo.reponame
39+
if rev:
40+
sub['rev'] = _repo.db_rev(rev)
41+
else:
42+
sub['rev'] = 'any' # wildcard
43+
finally:
44+
_repo.close()
45+
else:
46+
sub['repos'] = ''
47+
sub['rev'] = ''
48+
49+
@env.with_transaction()
50+
def insert_subscription(db):
51+
cursor = db.cursor()
52+
select = ("SELECT id FROM code_comments_subscriptions WHERE "
53+
"user = '{user}' AND type = '{type}' AND "
54+
"path = '{path}' AND repos = '{repos}' AND "
55+
"rev = '{rev}' AND notify = '{notify}'").format(**sub)
56+
cursor.execute(select)
57+
subs = cursor.fetchall()
58+
if len(subs) > 0:
59+
# There shouldn't really ever be more than one result
60+
env.log.debug(
61+
'Subscription for {type} already exists'.format(**sub))
62+
return subs[0]
63+
else:
64+
fields = ', '.join(sub.keys())
65+
values_template = ', '.join(['%s'] * len(sub))
66+
insert = ("INSERT INTO code_comments_subscriptions "
67+
"({0}) VALUES ({1})").format(fields, values_template)
68+
cursor.execute(insert, sub.values())
69+
env.log.debug(
70+
'Subscription for {type} created'.format(**sub))
71+
return db.get_last_id(cursor, 'code_comments_subscriptions')
72+
73+
74+
def create_subscription_from_changeset(env, changeset):
75+
sub = {
76+
'user': changeset.author,
77+
'role': 'author',
78+
'type_': 'changeset',
79+
'path': None,
80+
'repos': changeset.repos.reponame,
81+
'rev': changeset.rev,
82+
'notify': 'always',
83+
}
84+
create_subscription(env, **sub)
85+
86+
87+
def create_subscription_from_comment(env, comment):
88+
sub = {
89+
'user': comment.author,
90+
'role': 'commenter',
91+
'type_': comment.type,
92+
'notify': 'always'
93+
}
94+
95+
# Munge attachments
96+
if comment.type == 'attachment':
97+
sub['path'] = comment.path.split(':')[1]
98+
sub['repos'] = None,
99+
sub['rev'] = None
100+
101+
# Munge changesets and browser
102+
if comment.type in ('changeset', 'browser'):
103+
if comment.type == 'browser':
104+
sub['path'] = comment.path
105+
else:
106+
sub['path'] = None
107+
repo = RepositoryManager(env).get_repository(None)
108+
try:
109+
sub['repos'] = repo.reponame
110+
try:
111+
_cs = repo.get_changeset(comment.revision)
112+
sub['rev'] = _cs.rev
113+
except NoSuchChangeset:
114+
# Invalid changeset
115+
return None
116+
finally:
117+
repo.close()
118+
119+
return create_subscription(env, **sub)
120+
121+
122+
class SubscriptionAdmin(Component):
123+
"""
124+
trac-admin command provider for subscription administration.
125+
"""
126+
implements(IAdminCommandProvider)
127+
128+
# IAdminCommandProvider methods
129+
130+
def get_admin_commands(self):
131+
yield ('subscription seed', '',
132+
"""Seeds subscriptions for existing attachments, changesets,
133+
and comments.
134+
""",
135+
None, self._do_seed)
136+
137+
def _do_seed(self):
138+
# Create a subscription for all existing attachments
139+
cursor = self.env.get_read_db().cursor()
140+
cursor.execute("SELECT type, id, filename, author FROM attachment")
141+
attachments = cursor.fetchall()
142+
for attachment in attachments:
143+
sub = {
144+
'user': attachment[3],
145+
'role': 'author',
146+
'type_': 'attachment',
147+
'path': "/{0}/{1}/{2}".format(*attachment),
148+
'repos': None,
149+
'rev': None,
150+
'notify': 'always',
151+
}
152+
create_subscription(self.env, **sub)
153+
154+
# Create a subscription for all existing revisions
155+
rm = RepositoryManager(self.env)
156+
repos = rm.get_real_repositories()
157+
for repo in repos:
158+
_rev = repo.get_oldest_rev()
159+
while _rev:
160+
try:
161+
_cs = repo.get_changeset(_rev)
162+
create_subscription_from_changeset(self.env, _cs)
163+
except NoSuchChangeset:
164+
pass
165+
_rev = repo.next_rev(_rev)
166+
167+
# Create a subscription for all existing comments
168+
comments = Comments(None, self.env).all()
169+
for comment in comments:
170+
create_subscription_from_comment(self.env, comment)

0 commit comments

Comments
 (0)