1 | import re |
---|
2 | from trac.core import * |
---|
3 | from trac.config import ListOption |
---|
4 | from trac.web.api import IRequestFilter, IRequestHandler, Href |
---|
5 | from trac.web.chrome import ITemplateProvider, add_ctxtnav, add_stylesheet, \ |
---|
6 | add_script |
---|
7 | from trac.resource import get_resource_url |
---|
8 | from trac.ticket.api import ITicketChangeListener |
---|
9 | from trac.wiki.api import IWikiChangeListener |
---|
10 | from trac.util.text import to_unicode |
---|
11 | from genshi.builder import tag |
---|
12 | from announcerplugin.api import IAnnouncementSubscriber |
---|
13 | |
---|
14 | class WatchSubscriber(Component): |
---|
15 | |
---|
16 | implements(IRequestFilter, IRequestHandler, IAnnouncementSubscriber, |
---|
17 | ITicketChangeListener, IWikiChangeListener) |
---|
18 | |
---|
19 | watchable_paths = ListOption('announcer', 'watchable_paths', |
---|
20 | 'wiki/*,ticket/*', |
---|
21 | doc='List of URL paths to allow watching. Globs are supported.') |
---|
22 | ctxtnav_names = ListOption('announcer', 'ctxtnav_names', |
---|
23 | ['Watch This','Unwatch This'], |
---|
24 | doc="Text of context navigation entries. " |
---|
25 | "An empty list removes them from the context navigation bar.") |
---|
26 | |
---|
27 | path_match = re.compile(r'/watch/(.*)') |
---|
28 | |
---|
29 | # IRequestHandler methods |
---|
30 | def match_request(self, req): |
---|
31 | if self.path_match.match(req.path_info): |
---|
32 | realm = self.normalise_resource(req.path_info).split('/')[1] |
---|
33 | return "%s_VIEW" % realm.upper() in req.perm |
---|
34 | return False |
---|
35 | |
---|
36 | def process_request(self, req): |
---|
37 | match = self.path_match.match(req.path_info) |
---|
38 | resource = self.normalise_resource(match.groups()[0]) |
---|
39 | realm, _ = resource.split('/', 1) |
---|
40 | req.perm.require('%s_VIEW' % realm.upper()) |
---|
41 | self.toggle_watched(req.session.sid, (not req.authname == \ |
---|
42 | 'anonymous') and 1 or 0, resource, req) |
---|
43 | req.redirect(req.href(resource)) |
---|
44 | |
---|
45 | def toggle_watched(self, sid, authenticated, resource, req=None): |
---|
46 | realm, resource = resource.split('/', 1) |
---|
47 | if self.is_watching(sid, authenticated, realm, resource): |
---|
48 | self.set_unwatch(sid, authenticated, realm, resource) |
---|
49 | self._schedule_notice(req, 'You are no longer receiving ' \ |
---|
50 | 'change notifications about this resource.') |
---|
51 | else: |
---|
52 | self.set_watch(sid, authenticated, realm, resource) |
---|
53 | self._schedule_notice(req, 'You are now receiving ' \ |
---|
54 | 'change notifications about this resource.') |
---|
55 | |
---|
56 | def _schedule_notice(self, req, message): |
---|
57 | req.session['_announcer_watch_message_'] = message |
---|
58 | |
---|
59 | def _add_notice(self, req): |
---|
60 | if '_announcer_watch_message_' in req.session: |
---|
61 | from trac.web.chrome import add_notice |
---|
62 | add_notice(req, req.session['_announcer_watch_message_']) |
---|
63 | del req.session['_announcer_watch_message_'] |
---|
64 | |
---|
65 | def is_watching(self, sid, authenticated, realm, resource): |
---|
66 | db = self.env.get_db_cnx() |
---|
67 | cursor = db.cursor() |
---|
68 | cursor.execute(""" |
---|
69 | SELECT id |
---|
70 | FROM subscriptions |
---|
71 | WHERE sid=%s AND authenticated=%s |
---|
72 | AND enabled=1 AND managed=%s |
---|
73 | AND realm=%s |
---|
74 | AND category=%s |
---|
75 | AND rule=%s |
---|
76 | """, (sid, int(authenticated), 'watcher', realm, '*', |
---|
77 | to_unicode(resource))) |
---|
78 | result = cursor.fetchone() |
---|
79 | if result: |
---|
80 | return True |
---|
81 | else: |
---|
82 | return False |
---|
83 | |
---|
84 | def set_watch(self, sid, authenticated, realm, resource): |
---|
85 | db = self.env.get_db_cnx() |
---|
86 | cursor = db.cursor() |
---|
87 | self.set_unwatch(sid, authenticated, realm, resource, use_db=db) |
---|
88 | cursor.execute(""" |
---|
89 | INSERT INTO subscriptions |
---|
90 | (sid, authenticated, |
---|
91 | enabled, managed, |
---|
92 | realm, category, |
---|
93 | rule, transport) |
---|
94 | VALUES |
---|
95 | (%s, %s, |
---|
96 | 1, %s, |
---|
97 | %s, %s, |
---|
98 | %s, %s) |
---|
99 | """, ( |
---|
100 | sid, int(authenticated), |
---|
101 | 'watcher', realm, '*', |
---|
102 | resource, 'email' |
---|
103 | ) |
---|
104 | ) |
---|
105 | db.commit() |
---|
106 | |
---|
107 | def set_unwatch(self, sid, authenticated, realm, resource, use_db=None): |
---|
108 | if not use_db: |
---|
109 | db = self.env.get_db_cnx() |
---|
110 | else: |
---|
111 | db = use_db |
---|
112 | cursor = db.cursor() |
---|
113 | cursor.execute(""" |
---|
114 | DELETE |
---|
115 | FROM subscriptions |
---|
116 | WHERE sid=%s AND authenticated=%s |
---|
117 | AND enabled=1 AND managed=%s |
---|
118 | AND realm=%s |
---|
119 | AND category=%s |
---|
120 | AND rule=%s |
---|
121 | """, (sid, int(authenticated), 'watcher', realm, '*', |
---|
122 | to_unicode(resource))) |
---|
123 | if not use_db: |
---|
124 | db.commit() |
---|
125 | |
---|
126 | # IRequestFilter methods |
---|
127 | def pre_process_request(self, req, handler): |
---|
128 | return handler |
---|
129 | |
---|
130 | def post_process_request(self, req, template, data, content_type): |
---|
131 | self._add_notice(req) |
---|
132 | |
---|
133 | if req.authname != "anonymous" or (req.authname == 'anonymous' and \ |
---|
134 | 'email' in req.session): |
---|
135 | for pattern in self.watchable_paths: |
---|
136 | path = self.normalise_resource(req.path_info) |
---|
137 | if re.match(pattern, path): |
---|
138 | realm, _ = path.split('/', 1) |
---|
139 | if '%s_VIEW'%realm.upper() not in req.perm: |
---|
140 | return (template, data, content_type) |
---|
141 | self.render_watcher(req) |
---|
142 | break |
---|
143 | return (template, data, content_type) |
---|
144 | |
---|
145 | # Internal methods |
---|
146 | def render_watcher(self, req): |
---|
147 | if not self.ctxtnav_names: |
---|
148 | return |
---|
149 | resource = self.normalise_resource(req.path_info) |
---|
150 | realm, resource = resource.split('/', 1) |
---|
151 | if self.is_watching(req.session.sid, not req.authname == 'anonymous', |
---|
152 | realm, resource): |
---|
153 | action_name = len(self.ctxtnav_names) >= 2 and \ |
---|
154 | self.ctxtnav_names[1] or 'Unwatch This' |
---|
155 | else: |
---|
156 | action_name = len(self.ctxtnav_names) and \ |
---|
157 | self.ctxtnav_names[0] or 'Watch This' |
---|
158 | add_ctxtnav(req, |
---|
159 | tag.a( |
---|
160 | action_name, href=req.href.watch(realm, resource) |
---|
161 | ) |
---|
162 | ) |
---|
163 | |
---|
164 | def normalise_resource(self, resource): |
---|
165 | if isinstance(resource, basestring): |
---|
166 | resource = resource.strip('/') |
---|
167 | # Special-case start page |
---|
168 | if not resource: |
---|
169 | resource = "wiki/WikiStart" |
---|
170 | elif resource == 'wiki': |
---|
171 | resource += '/WikiStart' |
---|
172 | return resource |
---|
173 | return get_resource_url(self.env, resource, Href('')).strip('/') |
---|
174 | |
---|
175 | # IWikiChangeListener |
---|
176 | def wiki_page_added(*args): |
---|
177 | pass |
---|
178 | |
---|
179 | def wiki_page_changed(*args): |
---|
180 | pass |
---|
181 | |
---|
182 | def wiki_page_deleted(self, page): |
---|
183 | db = self.env.get_db_cnx() |
---|
184 | cursor = db.cursor() |
---|
185 | cursor.execute(""" |
---|
186 | DELETE |
---|
187 | FROM subscriptions |
---|
188 | WHERE managed=%s |
---|
189 | AND realm=%s |
---|
190 | AND rule=%s |
---|
191 | """, ('watcher', 'wiki', to_unicode(page.name))) |
---|
192 | db.commit() |
---|
193 | |
---|
194 | def wiki_page_version_deleted(*args): |
---|
195 | pass |
---|
196 | |
---|
197 | # ITicketChangeListener |
---|
198 | def ticket_created(*args): |
---|
199 | pass |
---|
200 | |
---|
201 | def ticket_changed(*args): |
---|
202 | pass |
---|
203 | |
---|
204 | def ticket_deleted(self, ticket): |
---|
205 | db = self.env.get_db_cnx() |
---|
206 | cursor = db.cursor() |
---|
207 | cursor.execute(""" |
---|
208 | DELETE |
---|
209 | FROM subscriptions |
---|
210 | WHERE managed=%s |
---|
211 | AND realm=%s |
---|
212 | AND rule=%s |
---|
213 | """, ('watcher', 'ticket', to_unicode(ticket.id))) |
---|
214 | db.commit() |
---|
215 | |
---|
216 | # IAnnouncementSubscriber |
---|
217 | def get_subscription_realms(self): |
---|
218 | return ('wiki', 'ticket') |
---|
219 | |
---|
220 | def get_subscription_categories(self, realm): |
---|
221 | return ('created', 'changed', 'attachment added') |
---|
222 | |
---|
223 | def get_subscriptions_for_event(self, event): |
---|
224 | if event.realm in self.get_subscription_realms(): |
---|
225 | if event.category in self.get_subscription_categories(event.realm): |
---|
226 | db = self.env.get_db_cnx() |
---|
227 | cursor = db.cursor() |
---|
228 | cursor.execute(""" |
---|
229 | SELECT transport, sid, authenticated |
---|
230 | FROM subscriptions |
---|
231 | WHERE enabled=1 AND managed=%s |
---|
232 | AND realm=%s |
---|
233 | AND category=%s |
---|
234 | AND rule=%s |
---|
235 | """, ('watcher', event.realm, '*', |
---|
236 | to_unicode(self._get_target_identifier(event.realm, |
---|
237 | event.target)))) |
---|
238 | |
---|
239 | for transport, sid, authenticated in cursor.fetchall(): |
---|
240 | self.log.debug("WatchSubscriber added '%s (%s)' because " \ |
---|
241 | "of rule: watched"%(sid,authenticated and \ |
---|
242 | 'authenticated' or 'not authenticated')) |
---|
243 | yield (transport, sid, authenticated, None) |
---|
244 | |
---|
245 | def _get_target_identifier(self, realm, target): |
---|
246 | if realm == "wiki": |
---|
247 | return target.name |
---|
248 | elif realm == "ticket": |
---|
249 | return target.id |
---|
250 | |
---|