1
+ import json
2
+ import re
3
+
1
4
from trac .admin import IAdminCommandProvider
2
5
from trac .attachment import Attachment , IAttachmentChangeListener
3
6
from trac .core import Component , implements
4
7
from trac .versioncontrol import (
5
8
RepositoryManager , NoSuchChangeset , IRepositoryChangeListener )
9
+ from trac .web .api import HTTPNotFound , IRequestHandler , ITemplateStreamFilter
10
+
11
+ from genshi .builder import tag
12
+ from genshi .filters import Transformer
6
13
7
14
from code_comments .api import ICodeCommentChangeListener
8
15
from code_comments .comments import Comments
@@ -45,8 +52,10 @@ def select(cls, env, args={}, notify=None):
45
52
Retrieve existing subscription(s).
46
53
"""
47
54
select = 'SELECT * FROM code_comments_subscriptions'
55
+
48
56
if notify :
49
57
args ['notify' ] = bool (notify )
58
+
50
59
if len (args ) > 0 :
51
60
select += ' WHERE '
52
61
criteria = []
@@ -61,6 +70,7 @@ def select(cls, env, args={}, notify=None):
61
70
value = int (value )
62
71
criteria .append (template .format (key , value ))
63
72
select += ' AND ' .join (criteria )
73
+
64
74
cursor = env .get_read_db ().cursor ()
65
75
cursor .execute (select )
66
76
for row in cursor :
@@ -141,9 +151,9 @@ def _from_row(cls, env, row):
141
151
return None
142
152
143
153
@classmethod
144
- def _from_dict (cls , env , dict_ ):
154
+ def _from_dict (cls , env , dict_ , create = True ):
145
155
"""
146
- Creates a subscription from a dict.
156
+ Retrieves or (optionally) creates a subscription from a dict.
147
157
"""
148
158
subscription = None
149
159
@@ -161,16 +171,16 @@ def _from_dict(cls, env, dict_):
161
171
for _subscription in subscriptions :
162
172
if subscription is None :
163
173
subscription = _subscription
164
- env .log .info ('Subscription already exists : [%d] %s' ,
174
+ env .log .info ('Subscription found : [%d] %s' ,
165
175
subscription .id , subscription )
166
176
else :
167
177
# The unique constraint on the table should prevent this ever
168
178
# occurring
169
179
env .log .warning ('Multiple subscriptions found: [%d] %s' ,
170
180
subscription .id , subscription )
171
181
172
- # Create a new subscription if we didn't find one
173
- if subscription is None :
182
+ # (Optionally) create a new subscription if we didn't find one
183
+ if subscription is None and create :
174
184
subscription = cls (env , dict_ )
175
185
subscription .insert ()
176
186
env .log .info ('Subscription created: [%d] %s' ,
@@ -298,6 +308,53 @@ def for_comment(cls, env, comment, notify=None):
298
308
299
309
return cls .select (env , args , notify )
300
310
311
+ @classmethod
312
+ def for_request (cls , env , req , create = False ):
313
+ """
314
+ Return a **single** subscription for a HTTP request.
315
+ """
316
+ reponame = req .args .get ('reponame' )
317
+ rm = RepositoryManager (env )
318
+ repos = rm .get_repository (reponame )
319
+
320
+ path = req .args .get ('path' ) or ''
321
+ rev = req .args .get ('rev' ) or repos .youngest_rev
322
+
323
+ dict_ = {
324
+ 'user' : req .authname ,
325
+ 'type' : req .args .get ('realm' ),
326
+ 'path' : '' ,
327
+ 'rev' : '' ,
328
+ 'repos' : '' ,
329
+ }
330
+
331
+ if dict_ ['type' ] == 'attachment' :
332
+ dict_ ['path' ] = path
333
+
334
+ if dict_ ['type' ] == 'changeset' :
335
+ dict_ ['rev' ] = path [1 :]
336
+ dict_ ['repos' ] = repos .reponame
337
+
338
+ if dict_ ['type' ] == 'browser' :
339
+ if len (path ) == 0 :
340
+ dict_ ['path' ] = '/'
341
+ else :
342
+ dict_ ['path' ] = path [1 :]
343
+ dict_ ['rev' ] = rev
344
+ dict_ ['repos' ] = repos .reponame
345
+
346
+ return cls ._from_dict (env , dict_ , create = create )
347
+
348
+
349
+ class SubscriptionJSONEncoder (json .JSONEncoder ):
350
+ """
351
+ JSON Encoder for a Subscription object.
352
+ """
353
+ def default (self , o ):
354
+ data = o .__dict__ .copy ()
355
+ del data ['env' ]
356
+ return data
357
+
301
358
302
359
class SubscriptionAdmin (Component ):
303
360
"""
@@ -388,3 +445,70 @@ def changeset_modified(self, repos, changeset, old_changeset):
388
445
389
446
def comment_created (self , comment ):
390
447
Subscription .from_comment (self .env , comment )
448
+
449
+
450
+ class SubscriptionModule (Component ):
451
+ implements (IRequestHandler , ITemplateStreamFilter )
452
+
453
+ # IRequestHandler methods
454
+
455
+ def match_request (self , req ):
456
+ match = re .match (r'\/subscription\/(\w+)(\/?.*)$' , req .path_info )
457
+ if match :
458
+ if match .group (1 ):
459
+ req .args ['realm' ] = match .group (1 )
460
+ if match .group (2 ):
461
+ req .args ['path' ] = match .group (2 )
462
+ return True
463
+
464
+ def process_request (self , req ):
465
+ if req .method == 'POST' :
466
+ return self ._do_POST (req )
467
+ elif req .method == 'PUT' :
468
+ return self ._do_PUT (req )
469
+ return self ._do_GET (req )
470
+
471
+ # ITemplateStreamFilter methods
472
+
473
+ def filter_stream (self , req , method , filename , stream , data ):
474
+ if re .match (r'^/(changeset|browser|attachment).*' , req .path_info ):
475
+ filter = Transformer ('//h1' )
476
+ stream |= filter .before (self ._subscription_button ())
477
+ return stream
478
+
479
+ # Internal methods
480
+
481
+ def _do_GET (self , req ):
482
+ subscription = Subscription .for_request (self .env , req )
483
+ if subscription is None :
484
+ req .send ('' , 'application/json' , 204 )
485
+ req .send (json .dumps (subscription , cls = SubscriptionJSONEncoder ),
486
+ 'application/json' )
487
+
488
+ def _do_POST (self , req ):
489
+ subscription = Subscription .for_request (self .env , req , create = True )
490
+ status = 201
491
+ req .send (json .dumps (subscription , cls = SubscriptionJSONEncoder ),
492
+ 'application/json' , status )
493
+
494
+ def _do_PUT (self , req ):
495
+ subscription = Subscription .for_request (self .env , req )
496
+ if subscription is None :
497
+ raise HTTPNotFound ('Subscription to /%s%s for %s not found' ,
498
+ req .args .get ('realm' ), req .args .get ('path' ),
499
+ req .authname )
500
+ content = req .read ()
501
+ if len (content ) > 0 :
502
+ data = json .loads (content )
503
+ subscription .notify = data ['notify' ]
504
+ subscription .update ()
505
+ req .send (json .dumps (subscription , cls = SubscriptionJSONEncoder ),
506
+ 'application/json' )
507
+
508
+ def _subscription_button (self ):
509
+ """
510
+ Generates a (disabled) button to connect JavaScript to.
511
+ """
512
+ return tag .button ('Subscribe' , id_ = 'subscribe' , disabled = True ,
513
+ title = ('Code comment subscriptions require '
514
+ 'JavaScript to be enabled' ))
0 commit comments