1+ import json
2+ import re
3+
14from trac .admin import IAdminCommandProvider
25from trac .attachment import Attachment , IAttachmentChangeListener
36from trac .core import Component , implements
47from trac .versioncontrol import (
58 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
613
714from code_comments .api import ICodeCommentChangeListener
815from code_comments .comments import Comments
@@ -45,8 +52,10 @@ def select(cls, env, args={}, notify=None):
4552 Retrieve existing subscription(s).
4653 """
4754 select = 'SELECT * FROM code_comments_subscriptions'
55+
4856 if notify :
4957 args ['notify' ] = bool (notify )
58+
5059 if len (args ) > 0 :
5160 select += ' WHERE '
5261 criteria = []
@@ -61,6 +70,7 @@ def select(cls, env, args={}, notify=None):
6170 value = int (value )
6271 criteria .append (template .format (key , value ))
6372 select += ' AND ' .join (criteria )
73+
6474 cursor = env .get_read_db ().cursor ()
6575 cursor .execute (select )
6676 for row in cursor :
@@ -141,9 +151,9 @@ def _from_row(cls, env, row):
141151 return None
142152
143153 @classmethod
144- def _from_dict (cls , env , dict_ ):
154+ def _from_dict (cls , env , dict_ , create = True ):
145155 """
146- Creates a subscription from a dict.
156+ Retrieves or (optionally) creates a subscription from a dict.
147157 """
148158 subscription = None
149159
@@ -161,16 +171,16 @@ def _from_dict(cls, env, dict_):
161171 for _subscription in subscriptions :
162172 if subscription is None :
163173 subscription = _subscription
164- env .log .info ('Subscription already exists : [%d] %s' ,
174+ env .log .info ('Subscription found : [%d] %s' ,
165175 subscription .id , subscription )
166176 else :
167177 # The unique constraint on the table should prevent this ever
168178 # occurring
169179 env .log .warning ('Multiple subscriptions found: [%d] %s' ,
170180 subscription .id , subscription )
171181
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 :
174184 subscription = cls (env , dict_ )
175185 subscription .insert ()
176186 env .log .info ('Subscription created: [%d] %s' ,
@@ -298,6 +308,53 @@ def for_comment(cls, env, comment, notify=None):
298308
299309 return cls .select (env , args , notify )
300310
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+
301358
302359class SubscriptionAdmin (Component ):
303360 """
@@ -388,3 +445,70 @@ def changeset_modified(self, repos, changeset, old_changeset):
388445
389446 def comment_created (self , comment ):
390447 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