| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | """ |
|---|
| 3 | = Watchlist Plugin for Trac = |
|---|
| 4 | Plugin Website: http://trac-hacks.org/wiki/WatchlistPlugin |
|---|
| 5 | Trac website: http://trac.edgewall.org/ |
|---|
| 6 | |
|---|
| 7 | Copyright (c) 2008-2010 by Martin Scharrer <martin@scharrer-online.de> |
|---|
| 8 | All rights reserved. |
|---|
| 9 | |
|---|
| 10 | The i18n support was added by Steffen Hoffmann <hoff.st@web.de>. |
|---|
| 11 | |
|---|
| 12 | This program is free software: you can redistribute it and/or modify |
|---|
| 13 | it under the terms of the GNU General Public License as published by |
|---|
| 14 | the Free Software Foundation, either version 3 of the License, or |
|---|
| 15 | (at your option) any later version. |
|---|
| 16 | |
|---|
| 17 | This program is distributed in the hope that it will be useful, |
|---|
| 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 20 | GNU General Public License for more details. |
|---|
| 21 | |
|---|
| 22 | For a copy of the GNU General Public License see |
|---|
| 23 | <http://www.gnu.org/licenses/>. |
|---|
| 24 | |
|---|
| 25 | $Id: plugin.py 15264 2016-02-11 04:22:34Z rjollos $ |
|---|
| 26 | """ |
|---|
| 27 | |
|---|
| 28 | __url__ = ur"$URL: //trac-hacks.org/svn/watchlistplugin/0.12/tracwatchlist/plugin.py $"[6:-2] |
|---|
| 29 | __author__ = ur"$Author: rjollos $"[9:-2] |
|---|
| 30 | __revision__ = int("0" + ur"$Rev: 15264 $"[6:-2].strip('M')) |
|---|
| 31 | __date__ = ur"$Date: 2016-02-11 04:22:34 +0000 (Thu, 11 Feb 2016) $"[7:-2] |
|---|
| 32 | |
|---|
| 33 | from pkg_resources import resource_filename |
|---|
| 34 | from datetime import datetime |
|---|
| 35 | |
|---|
| 36 | from trac.core import * |
|---|
| 37 | from genshi.builder import tag |
|---|
| 38 | from trac.config import BoolOption, ListOption |
|---|
| 39 | from trac.util.text import to_unicode |
|---|
| 40 | from trac.web.api import IRequestFilter, IRequestHandler, \ |
|---|
| 41 | RequestDone, HTTPNotFound, HTTPBadRequest |
|---|
| 42 | from trac.web.chrome import ITemplateProvider, add_ctxtnav, \ |
|---|
| 43 | add_notice |
|---|
| 44 | from trac.wiki.model import WikiPage |
|---|
| 45 | |
|---|
| 46 | from tracwatchlist.api import IWatchlistProvider |
|---|
| 47 | from tracwatchlist.translation import add_domain, _, N_, t_, tag_, gettext,\ |
|---|
| 48 | i18n_enabled |
|---|
| 49 | from tracwatchlist.util import ensure_string, ensure_iter, LC_TIME,\ |
|---|
| 50 | datetime_format, current_timestamp |
|---|
| 51 | from tracwatchlist.manual import WatchlistManual |
|---|
| 52 | |
|---|
| 53 | |
|---|
| 54 | class WatchlistPlugin(Component): |
|---|
| 55 | """Main class of the Trac WatchlistPlugin. |
|---|
| 56 | |
|---|
| 57 | Displays watchlist for wiki pages, ticket and possible other Trac realms. |
|---|
| 58 | |
|---|
| 59 | For documentation see http://trac-hacks.org/wiki/WatchlistPlugin. |
|---|
| 60 | """ |
|---|
| 61 | providers = ExtensionPoint(IWatchlistProvider) |
|---|
| 62 | |
|---|
| 63 | implements( IRequestHandler, IRequestFilter, ITemplateProvider ) |
|---|
| 64 | |
|---|
| 65 | bool_options = [ |
|---|
| 66 | BoolOption('watchlist', 'attachment_changes' , True , doc=N_("Take attachment changes into account")), |
|---|
| 67 | BoolOption('watchlist', 'notifications' , False, doc=N_("Notifications")), |
|---|
| 68 | BoolOption('watchlist', 'display_notify_navitems' , False, doc=N_("Display notification navigation items")), |
|---|
| 69 | BoolOption('watchlist', 'display_notify_column' , True , doc=N_("Display notification column in watchlist tables")), |
|---|
| 70 | BoolOption('watchlist', 'notify_by_default' , False, doc=N_("Enable notifications by default for all watchlist entries")), |
|---|
| 71 | BoolOption('watchlist', 'stay_at_resource' , False, doc=N_("The user stays at the resource after a watch/unwatch operation and the watchlist page is not displayed")), |
|---|
| 72 | BoolOption('watchlist', 'stay_at_resource_notify' , True , doc=N_("The user stays at the resource after a notify/do-not-notify operation and the watchlist page is not displayed")), |
|---|
| 73 | BoolOption('watchlist', 'show_messages_on_resource_page' , True , doc=N_("Action messages are shown on resource pages")), |
|---|
| 74 | BoolOption('watchlist', 'show_messages_on_watchlist_page' , True , doc=N_("Action messages are shown when going to the watchlist page")), |
|---|
| 75 | BoolOption('watchlist', 'show_messages_while_on_watchlist_page', True , doc=N_("Show action messages while on watchlist page")), |
|---|
| 76 | BoolOption('watchlist', 'autocomplete_inputs' , True , doc=N_("Autocomplete input fields (add/remove resources)")), |
|---|
| 77 | BoolOption('watchlist', 'dynamic_tables' , True , doc=N_("Dynamic watchlist tables")), |
|---|
| 78 | BoolOption('watchlist', 'datetime_picker' , True , doc=N_("Provide date/time picker application")), |
|---|
| 79 | BoolOption('watchlist', 'individual_column_filtering' , True , doc=N_("Individual column filtering")), |
|---|
| 80 | ] |
|---|
| 81 | |
|---|
| 82 | list_options = [ |
|---|
| 83 | ListOption('watchlist', 'realm_order', 'wiki,ticket', doc=N_("Display only the given watchlist sections in the given order")) |
|---|
| 84 | ] |
|---|
| 85 | |
|---|
| 86 | wsub = None |
|---|
| 87 | |
|---|
| 88 | |
|---|
| 89 | def __init__(self): |
|---|
| 90 | # bind the 'watchlist' catalog to the specified locale directory |
|---|
| 91 | try: |
|---|
| 92 | locale_dir = resource_filename(__name__, 'locale') |
|---|
| 93 | except KeyError: |
|---|
| 94 | pass |
|---|
| 95 | else: |
|---|
| 96 | add_domain(self.env.path, locale_dir) |
|---|
| 97 | |
|---|
| 98 | # |
|---|
| 99 | self.realms = [] |
|---|
| 100 | self.realm_handler = {} |
|---|
| 101 | for provider in self.providers: |
|---|
| 102 | for realm in provider.get_realms(): |
|---|
| 103 | assert realm not in self.realms |
|---|
| 104 | self.realms.append(realm) |
|---|
| 105 | self.realm_handler[realm] = provider |
|---|
| 106 | |
|---|
| 107 | try: |
|---|
| 108 | # Import methods from WatchSubscriber of the AnnouncerPlugin |
|---|
| 109 | from announcerplugin.subscribers.watchers import WatchSubscriber |
|---|
| 110 | self.wsub = self.env[WatchSubscriber] |
|---|
| 111 | if self.wsub: |
|---|
| 112 | self.log.debug("WS: WatchSubscriber found in announcerplugin") |
|---|
| 113 | except Exception, e: |
|---|
| 114 | try: |
|---|
| 115 | # Import fallback methods for AnnouncerPlugin 1.0 |
|---|
| 116 | from announcer.opt.subscribers import WatchSubscriber |
|---|
| 117 | self.wsub = self.env[WatchSubscriber] |
|---|
| 118 | if self.wsub: |
|---|
| 119 | self.log.debug("WS: WatchSubscriber found in announcer") |
|---|
| 120 | except Exception, ee: |
|---|
| 121 | self.log.debug("WS! " + str(e)) |
|---|
| 122 | self.log.debug("WS! " + str(ee)) |
|---|
| 123 | self.wsub = None |
|---|
| 124 | |
|---|
| 125 | |
|---|
| 126 | ## User settings ######################################################## |
|---|
| 127 | def get_settings(self, user): |
|---|
| 128 | settings = {} |
|---|
| 129 | settings['booloptions'] = dict([ |
|---|
| 130 | ( option.name, self.config.getbool('watchlist',option.name,option.default) ) |
|---|
| 131 | for option in self.bool_options ]) |
|---|
| 132 | settings['booloptions_doc'] = dict([ (option.name,gettext(option.__doc__)) for option in self.bool_options ]) |
|---|
| 133 | settings['booloptions_order'] = [ option.name for option in self.bool_options ] |
|---|
| 134 | settings['listoptions'] = dict([ |
|---|
| 135 | ( option.name, self.config.getlist('watchlist',option.name,option.default) ) |
|---|
| 136 | for option in self.list_options ]) |
|---|
| 137 | settings['listoptions_doc'] = dict([ (option.name,gettext(option.__doc__)) for option in self.list_options ]) |
|---|
| 138 | settings['listoptions_order'] = [ option.name for option in self.list_options ] |
|---|
| 139 | usersettings = self._get_user_settings(user) |
|---|
| 140 | if 'booloptions' in usersettings: |
|---|
| 141 | settings['booloptions'].update( usersettings['booloptions'] ) |
|---|
| 142 | del usersettings['booloptions'] |
|---|
| 143 | for l in settings['listoptions'].keys(): |
|---|
| 144 | if l in usersettings: |
|---|
| 145 | settings['listoptions'][l] = usersettings[l] |
|---|
| 146 | del usersettings[l] |
|---|
| 147 | settings.update( usersettings ) |
|---|
| 148 | return settings |
|---|
| 149 | |
|---|
| 150 | |
|---|
| 151 | def _delete_user_settings(self, user): |
|---|
| 152 | """Deletes all user settings in 'watchlist_settings' table. |
|---|
| 153 | This can be done to reset all settings to the default values |
|---|
| 154 | and to resolve possible errors with wrongly stored settings. |
|---|
| 155 | This can happen while using the develop version of this plugin.""" |
|---|
| 156 | db = self.env.get_db_cnx() |
|---|
| 157 | cursor = db.cursor() |
|---|
| 158 | |
|---|
| 159 | cursor.execute(""" |
|---|
| 160 | DELETE |
|---|
| 161 | FROM watchlist_settings |
|---|
| 162 | WHERE wluser=%s |
|---|
| 163 | """, (user,)) |
|---|
| 164 | db.commit() |
|---|
| 165 | return |
|---|
| 166 | |
|---|
| 167 | |
|---|
| 168 | def _save_user_settings(self, user, settings): |
|---|
| 169 | """Saves user settings in 'watchlist_settings' table. |
|---|
| 170 | Only saving of all user settings is supported at the moment.""" |
|---|
| 171 | db = self.env.get_db_cnx() |
|---|
| 172 | cursor = db.cursor() |
|---|
| 173 | options = settings['booloptions'] |
|---|
| 174 | |
|---|
| 175 | settingsstr = "&".join([ "=".join([k,unicode(v)]) |
|---|
| 176 | for k,v in options.iteritems()]) |
|---|
| 177 | |
|---|
| 178 | cursor.execute(""" |
|---|
| 179 | DELETE |
|---|
| 180 | FROM watchlist_settings |
|---|
| 181 | WHERE wluser=%s |
|---|
| 182 | """, (user,)) |
|---|
| 183 | |
|---|
| 184 | cursor.execute(""" |
|---|
| 185 | INSERT |
|---|
| 186 | INTO watchlist_settings (wluser,name,type,settings) |
|---|
| 187 | VALUES (%s,'booloptions','ListOfBool',%s) |
|---|
| 188 | """, (user, settingsstr) ) |
|---|
| 189 | |
|---|
| 190 | cursor.executemany(""" |
|---|
| 191 | INSERT |
|---|
| 192 | INTO watchlist_settings (wluser,name,type,settings) |
|---|
| 193 | VALUES (%s,%s,'ListOfStrings',%s) |
|---|
| 194 | """, [(user, realm + '_fields', ','.join(settings[realm + '_fields'])) |
|---|
| 195 | for realm in self.realms if realm + '_fields' in settings ] ) |
|---|
| 196 | |
|---|
| 197 | cursor.executemany(""" |
|---|
| 198 | INSERT |
|---|
| 199 | INTO watchlist_settings (wluser,name,type,settings) |
|---|
| 200 | VALUES (%s,%s,'ListOfStrings',%s) |
|---|
| 201 | """, [(user, name, ','.join(value)) |
|---|
| 202 | for name,value in settings.get('listoptions',{}).iteritems() ]) |
|---|
| 203 | |
|---|
| 204 | db.commit() |
|---|
| 205 | return True |
|---|
| 206 | |
|---|
| 207 | |
|---|
| 208 | def _get_user_settings(self, user): |
|---|
| 209 | db = self.env.get_db_cnx() |
|---|
| 210 | cursor = db.cursor() |
|---|
| 211 | cursor.execute(""" |
|---|
| 212 | SELECT name,type,settings |
|---|
| 213 | FROM watchlist_settings |
|---|
| 214 | WHERE wluser=%s |
|---|
| 215 | """, (user,)) |
|---|
| 216 | |
|---|
| 217 | settings = dict() |
|---|
| 218 | for name,type,settingsstr in cursor.fetchall(): |
|---|
| 219 | if type == 'ListOfBool': |
|---|
| 220 | settings[name] = dict([ |
|---|
| 221 | (k,v=='True') for k,v in |
|---|
| 222 | [ kv.split('=') for kv in settingsstr.split("&") ] ]) |
|---|
| 223 | elif type == 'ListOfStrings': |
|---|
| 224 | settings[name] = filter(None,settingsstr.split(',')) |
|---|
| 225 | else: |
|---|
| 226 | settings[name] = settingsstr |
|---|
| 227 | return settings |
|---|
| 228 | |
|---|
| 229 | |
|---|
| 230 | ## Change/access watch status ########################################### |
|---|
| 231 | def has_watchlist(self, user): |
|---|
| 232 | """Checks if user has a non-empty watchlist.""" |
|---|
| 233 | db = self.env.get_db_cnx() |
|---|
| 234 | cursor = db.cursor() |
|---|
| 235 | cursor.execute(""" |
|---|
| 236 | SELECT count(*) |
|---|
| 237 | FROM watchlist |
|---|
| 238 | WHERE wluser=%s; |
|---|
| 239 | """, (user,) |
|---|
| 240 | ) |
|---|
| 241 | count = cursor.fetchone() |
|---|
| 242 | if not count or not count[0]: |
|---|
| 243 | return False |
|---|
| 244 | else: |
|---|
| 245 | return True |
|---|
| 246 | |
|---|
| 247 | |
|---|
| 248 | def get_watched_resources(self, realm, user, db=None): |
|---|
| 249 | """Returns list of resources watched by the given user in the given realm. |
|---|
| 250 | The list contains a list with the resource id and the last time it |
|---|
| 251 | got visited.""" |
|---|
| 252 | db = db or self.env.get_db_cnx() |
|---|
| 253 | cursor = db.cursor() |
|---|
| 254 | cursor.execute(""" |
|---|
| 255 | SELECT resid,lastvisit |
|---|
| 256 | FROM watchlist |
|---|
| 257 | WHERE realm=%s AND wluser=%s |
|---|
| 258 | """, (realm, user)) |
|---|
| 259 | return cursor.fetchall() |
|---|
| 260 | |
|---|
| 261 | |
|---|
| 262 | def is_watching(self, realm, resid, user, db=None): |
|---|
| 263 | """Checks if user watches the given resource(s). |
|---|
| 264 | Returns True/False for a single resource or |
|---|
| 265 | a list of watched resources.""" |
|---|
| 266 | db = db or self.env.get_db_cnx() |
|---|
| 267 | cursor = db.cursor() |
|---|
| 268 | if getattr(resid, '__iter__', False): |
|---|
| 269 | reses = list(resid) |
|---|
| 270 | if not reses: |
|---|
| 271 | return [] |
|---|
| 272 | cursor.execute(""" |
|---|
| 273 | SELECT resid |
|---|
| 274 | FROM watchlist |
|---|
| 275 | WHERE wluser=%s AND realm=%s AND |
|---|
| 276 | resid IN ( |
|---|
| 277 | """ + ",".join(("%s",) * len(reses)) + ")", |
|---|
| 278 | [user,realm] + reses) |
|---|
| 279 | return [ res[0] for res in cursor.fetchall() ] |
|---|
| 280 | else: |
|---|
| 281 | cursor.execute(""" |
|---|
| 282 | SELECT count(*) |
|---|
| 283 | FROM watchlist |
|---|
| 284 | WHERE realm=%s AND resid=%s AND wluser=%s; |
|---|
| 285 | """, (realm, to_unicode(resid), user) |
|---|
| 286 | ) |
|---|
| 287 | count = cursor.fetchone() |
|---|
| 288 | if not count or not count[0]: |
|---|
| 289 | return False |
|---|
| 290 | else: |
|---|
| 291 | return True |
|---|
| 292 | |
|---|
| 293 | |
|---|
| 294 | def watch(self, realm, resid, user, lastvisit=0, db=None): |
|---|
| 295 | """Adds given resources to watchlist. |
|---|
| 296 | They must not be watched already.""" |
|---|
| 297 | db = db or self.env.get_db_cnx() |
|---|
| 298 | cursor = db.cursor() |
|---|
| 299 | cursor.executemany(""" |
|---|
| 300 | INSERT |
|---|
| 301 | INTO watchlist (wluser, realm, resid, lastvisit) |
|---|
| 302 | VALUES (%s,%s,%s,%s) |
|---|
| 303 | """, [(user, realm, res, lastvisit) for res in ensure_iter(resid)]) |
|---|
| 304 | |
|---|
| 305 | |
|---|
| 306 | def unwatch(self, realm, resid, user, db=None): |
|---|
| 307 | db = db or self.env.get_db_cnx() |
|---|
| 308 | cursor = db.cursor() |
|---|
| 309 | cursor.log = self.log |
|---|
| 310 | self.log.debug("resid = " + unicode(resid)) |
|---|
| 311 | reses = list(ensure_iter(resid)) |
|---|
| 312 | cursor.execute(""" |
|---|
| 313 | DELETE |
|---|
| 314 | FROM watchlist |
|---|
| 315 | WHERE wluser=%s AND realm=%s AND |
|---|
| 316 | resid IN ( |
|---|
| 317 | """ + ",".join(("%s",) * len(reses)) + ")", |
|---|
| 318 | [user,realm] + reses) |
|---|
| 319 | |
|---|
| 320 | |
|---|
| 321 | def visiting(self, realm, resid, user, db=None): |
|---|
| 322 | """Marks the given resource as visited just now.""" |
|---|
| 323 | db = db or self.env.get_db_cnx() |
|---|
| 324 | cursor = db.cursor() |
|---|
| 325 | now = current_timestamp() |
|---|
| 326 | cursor.execute(""" |
|---|
| 327 | UPDATE watchlist |
|---|
| 328 | SET lastvisit=%s |
|---|
| 329 | WHERE realm=%s AND resid=%s AND wluser=%s; |
|---|
| 330 | """, (now, realm, to_unicode(resid), user) |
|---|
| 331 | ) |
|---|
| 332 | db.commit() |
|---|
| 333 | return |
|---|
| 334 | |
|---|
| 335 | |
|---|
| 336 | ## Change/access notification status #################################### |
|---|
| 337 | def is_notify(self, req, realm, resid): |
|---|
| 338 | try: |
|---|
| 339 | return self.wsub.is_watching(req.session.sid, True, realm, resid) |
|---|
| 340 | except AttributeError: |
|---|
| 341 | return False |
|---|
| 342 | except Exception, e: |
|---|
| 343 | self.log.error("is_notify error: " + str(e)) |
|---|
| 344 | return False |
|---|
| 345 | |
|---|
| 346 | |
|---|
| 347 | def set_notify(self, req, realm, resid): |
|---|
| 348 | try: |
|---|
| 349 | self.wsub.set_watch(req.session.sid, True, realm, resid) |
|---|
| 350 | except AttributeError: |
|---|
| 351 | return False |
|---|
| 352 | except Exception, e: |
|---|
| 353 | self.log.error("set_notify error: " + str(e)) |
|---|
| 354 | |
|---|
| 355 | |
|---|
| 356 | def unset_notify(self, req, realm, resid): |
|---|
| 357 | try: |
|---|
| 358 | self.wsub.set_unwatch(req.session.sid, True, realm, resid) |
|---|
| 359 | except AttributeError: |
|---|
| 360 | return False |
|---|
| 361 | except Exception, e: |
|---|
| 362 | self.log.error("unset_notify error: " + str(e)) |
|---|
| 363 | |
|---|
| 364 | |
|---|
| 365 | ## Methods for IRequestHandler ########################################## |
|---|
| 366 | def match_request(self, req): |
|---|
| 367 | return req.path_info == "/watchlist" |
|---|
| 368 | |
|---|
| 369 | |
|---|
| 370 | def process_request(self, req): |
|---|
| 371 | """Processes requests to the '/watchlist' path.""" |
|---|
| 372 | user = to_unicode( req.authname ) |
|---|
| 373 | |
|---|
| 374 | # Reject anonymous users |
|---|
| 375 | if not user or user == 'anonymous': |
|---|
| 376 | # TRANSLATOR: Link part of |
|---|
| 377 | # "Please %(log_in)s to view or change your watchlist" |
|---|
| 378 | log_in=tag.a(_("log in"), href=req.href('login')) |
|---|
| 379 | if tag_ == None: |
|---|
| 380 | # For Trac 0.11 |
|---|
| 381 | raise HTTPNotFound( |
|---|
| 382 | tag("Please ", log_in, " to view or change your watchlist")) |
|---|
| 383 | else: |
|---|
| 384 | # For Trac 0.12 |
|---|
| 385 | raise HTTPNotFound( |
|---|
| 386 | tag_("Please %(log_in)s to view or change your watchlist", |
|---|
| 387 | log_in=log_in)) |
|---|
| 388 | |
|---|
| 389 | # Get and format request arguments |
|---|
| 390 | realm = to_unicode( req.args.get('realm', u'') ) |
|---|
| 391 | resid = ensure_string( req.args.get('resid', u'') ).strip() |
|---|
| 392 | action = req.args.get('action','view') |
|---|
| 393 | async = req.args.get('async', 'false') == 'true' |
|---|
| 394 | |
|---|
| 395 | # Handle AJAX search early to speed up things |
|---|
| 396 | if action == "search": |
|---|
| 397 | """AJAX search request. At the moment only used to get list |
|---|
| 398 | of all not watched resources.""" |
|---|
| 399 | handler = self.realm_handler[realm] |
|---|
| 400 | query = to_unicode( req.args.get('q', u'') ).strip() |
|---|
| 401 | group = to_unicode( req.args.get('group', u'notwatched') ) |
|---|
| 402 | if not query: |
|---|
| 403 | req.send('', 'text/plain', 200 ) |
|---|
| 404 | if group == 'notwatched': |
|---|
| 405 | result = list(handler.unwatched_resources(realm, query, user, self, fuzzy=1)) |
|---|
| 406 | else: |
|---|
| 407 | result = list(handler.watched_resources(realm, query, user, self, fuzzy=1)) |
|---|
| 408 | result.sort(cmp=handler.get_sort_cmp(realm), |
|---|
| 409 | key=handler.get_sort_key(realm)) |
|---|
| 410 | req.send( unicode(u'\n'.join(result) + u'\n').encode("utf-8"), |
|---|
| 411 | 'text/plain', 200 ) |
|---|
| 412 | |
|---|
| 413 | # DB cursor |
|---|
| 414 | db = self.env.get_db_cnx() |
|---|
| 415 | cursor = db.cursor() |
|---|
| 416 | |
|---|
| 417 | wldict = dict() |
|---|
| 418 | for k,v in req.args.iteritems(): |
|---|
| 419 | try: |
|---|
| 420 | wldict[str(k)] = v |
|---|
| 421 | except: |
|---|
| 422 | pass |
|---|
| 423 | |
|---|
| 424 | wldict['action'] = action |
|---|
| 425 | |
|---|
| 426 | onwatchlistpage = req.environ.get('HTTP_REFERER','').find( |
|---|
| 427 | req.href.watchlist()) != -1 |
|---|
| 428 | |
|---|
| 429 | settings = self.get_settings( user ) |
|---|
| 430 | options = settings['booloptions'] |
|---|
| 431 | self.options = options |
|---|
| 432 | # Needed here to get updated settings |
|---|
| 433 | if action == "save": |
|---|
| 434 | newoptions = req.args.get('booloptions',[]) |
|---|
| 435 | for k in settings['booloptions'].keys(): |
|---|
| 436 | settings['booloptions'][k] = k in newoptions |
|---|
| 437 | for realm in self.realms: |
|---|
| 438 | settings[realm + '_fields'] = req.args.get(realm + '_fields', '').split(',') |
|---|
| 439 | for l in settings['listoptions']: |
|---|
| 440 | if l in req.args: |
|---|
| 441 | settings['listoptions'][l] = [ e.strip() for e in req.args.get(l).split(',')] |
|---|
| 442 | self._save_user_settings(req.authname, settings) |
|---|
| 443 | |
|---|
| 444 | # Clear session cache for nav items |
|---|
| 445 | try: |
|---|
| 446 | # Clear session cache for nav items, so that the post processor |
|---|
| 447 | # rereads the settings |
|---|
| 448 | del req.session['watchlist_display_notify_navitems'] |
|---|
| 449 | except: |
|---|
| 450 | pass |
|---|
| 451 | req.redirect(req.href('watchlist')) |
|---|
| 452 | elif action == "defaultsettings": |
|---|
| 453 | # Only execute if sent using the watchlist preferences form |
|---|
| 454 | if onwatchlistpage and req.method == 'POST': |
|---|
| 455 | self._delete_user_settings(req.authname) |
|---|
| 456 | req.redirect(req.href('watchlist')) |
|---|
| 457 | |
|---|
| 458 | redirectback_notify = options['stay_at_resource_notify'] and not \ |
|---|
| 459 | onwatchlistpage and not async |
|---|
| 460 | if action == "notifyon": |
|---|
| 461 | if not self.res_exists(realm, resid): |
|---|
| 462 | raise HTTPNotFound(t_("Page %(name)s not found", name=resid)) |
|---|
| 463 | elif self.wsub and options['notifications']: |
|---|
| 464 | self.set_notify(req, realm, resid) |
|---|
| 465 | db.commit() |
|---|
| 466 | if redirectback_notify: |
|---|
| 467 | if options['show_messages_on_resource_page']: |
|---|
| 468 | req.session['watchlist_notify_message'] = _( |
|---|
| 469 | """ |
|---|
| 470 | You are now receiving change notifications |
|---|
| 471 | about this resource. |
|---|
| 472 | """ |
|---|
| 473 | ) |
|---|
| 474 | req.redirect(req.href(realm,resid)) |
|---|
| 475 | if async: |
|---|
| 476 | req.send("",'text/plain', 200) |
|---|
| 477 | else: |
|---|
| 478 | req.redirect(req.href('watchlist')) |
|---|
| 479 | elif action == "notifyoff": |
|---|
| 480 | if self.wsub and options['notifications']: |
|---|
| 481 | self.unset_notify(req, realm, resid) |
|---|
| 482 | db.commit() |
|---|
| 483 | if redirectback_notify: |
|---|
| 484 | if options['show_messages_on_resource_page']: |
|---|
| 485 | req.session['watchlist_notify_message'] = _( |
|---|
| 486 | """ |
|---|
| 487 | You are no longer receiving |
|---|
| 488 | change notifications about this resource. |
|---|
| 489 | """ |
|---|
| 490 | ) |
|---|
| 491 | req.redirect(req.href(realm,resid)) |
|---|
| 492 | raise RequestDone |
|---|
| 493 | if async: |
|---|
| 494 | req.send("",'text/plain', 200) |
|---|
| 495 | else: |
|---|
| 496 | req.redirect(req.href('watchlist')) |
|---|
| 497 | |
|---|
| 498 | redirectback = options['stay_at_resource'] and not onwatchlistpage |
|---|
| 499 | if action == "watch": |
|---|
| 500 | handler = self.realm_handler[realm] |
|---|
| 501 | not_found_res = list() |
|---|
| 502 | found_res = set() |
|---|
| 503 | for rid in resid.split(u','): |
|---|
| 504 | rid = rid.strip() |
|---|
| 505 | if not rid: |
|---|
| 506 | continue |
|---|
| 507 | reses = set(handler.resources_exists(realm, rid)) |
|---|
| 508 | if len(reses) == 0: |
|---|
| 509 | not_found_res.append(rid) |
|---|
| 510 | else: |
|---|
| 511 | found_res.update(reses) |
|---|
| 512 | watched_res = set(self.is_watching(realm, found_res, user)) |
|---|
| 513 | new_res = found_res.difference(watched_res) |
|---|
| 514 | already_watched_res = found_res.intersection(watched_res) |
|---|
| 515 | comp=handler.get_sort_cmp(realm) |
|---|
| 516 | key=handler.get_sort_key(realm) |
|---|
| 517 | wldict['already_watched_res'] = sorted(already_watched_res, cmp=comp, key=key) |
|---|
| 518 | wldict['new_res'] = sorted(new_res, cmp=comp, key=key) |
|---|
| 519 | wldict['not_found_res'] = not_found_res |
|---|
| 520 | |
|---|
| 521 | if new_res: |
|---|
| 522 | self.watch(realm, new_res, user, db=db) |
|---|
| 523 | db.commit() |
|---|
| 524 | |
|---|
| 525 | if options['show_messages_on_resource_page'] and not onwatchlistpage and redirectback: |
|---|
| 526 | req.session['watchlist_message'] = _( |
|---|
| 527 | "You are now watching this resource." |
|---|
| 528 | ) |
|---|
| 529 | if self.wsub and options['notifications'] and options['notify_by_default']: |
|---|
| 530 | for res in new_res: |
|---|
| 531 | self.set_notify(req, realm, res) |
|---|
| 532 | db.commit() |
|---|
| 533 | if redirectback and len(new_res) == 1: |
|---|
| 534 | req.redirect(req.href(realm, new_res.pop())) |
|---|
| 535 | action = 'view' |
|---|
| 536 | |
|---|
| 537 | elif action == "unwatch": |
|---|
| 538 | handler = self.realm_handler[realm] |
|---|
| 539 | not_found_res = list() |
|---|
| 540 | not_watched_res = list() |
|---|
| 541 | del_res = list() |
|---|
| 542 | resids = resid.strip().split(u',') |
|---|
| 543 | for rid in resids: |
|---|
| 544 | rid = rid.strip() |
|---|
| 545 | if not rid: |
|---|
| 546 | continue |
|---|
| 547 | reses = list(handler.watched_resources(realm, rid, user, self)) |
|---|
| 548 | if len(reses) == 0: |
|---|
| 549 | reses = list(handler.resources_exists(realm, rid)) |
|---|
| 550 | if len(reses) == 0: |
|---|
| 551 | not_found_res.append(rid) |
|---|
| 552 | else: |
|---|
| 553 | not_watched_res.extend(reses) |
|---|
| 554 | else: |
|---|
| 555 | del_res.extend(reses) |
|---|
| 556 | comp=handler.get_sort_cmp(realm) |
|---|
| 557 | key=handler.get_sort_key(realm) |
|---|
| 558 | wldict['del_res'] = sorted(del_res, cmp=comp, key=key) |
|---|
| 559 | wldict['not_watched_res'] = sorted(not_watched_res, cmp=comp, key=key) |
|---|
| 560 | wldict['not_found_res'] = sorted(not_found_res, cmp=comp, key=key) |
|---|
| 561 | |
|---|
| 562 | if del_res: |
|---|
| 563 | self.unwatch(realm, del_res, user, db=db) |
|---|
| 564 | elif len(resids) == 1: |
|---|
| 565 | # If there where no maches and only own resid try to delete it |
|---|
| 566 | # anyway. Might be a delete wiki page. |
|---|
| 567 | self.log.debug("resid = " + unicode(resid)) |
|---|
| 568 | self.unwatch(realm, [resid], user, db=db) |
|---|
| 569 | |
|---|
| 570 | # Unset notification |
|---|
| 571 | if self.wsub and options['notifications'] and options['notify_by_default']: |
|---|
| 572 | for res in del_res: |
|---|
| 573 | self.unset_notify(req, realm, res) |
|---|
| 574 | db.commit() |
|---|
| 575 | # Send an empty response for asynchronous requests |
|---|
| 576 | if async: |
|---|
| 577 | req.send("",'text/plain', 200) |
|---|
| 578 | # Redirect back to resource if so configured: |
|---|
| 579 | if redirectback and len(del_res) == 1: |
|---|
| 580 | if options['show_messages_on_resource_page']: |
|---|
| 581 | req.session['watchlist_message'] = _( |
|---|
| 582 | "You are no longer watching this resource." |
|---|
| 583 | ) |
|---|
| 584 | req.redirect(req.href(realm,del_res[0])) |
|---|
| 585 | action = 'view' |
|---|
| 586 | |
|---|
| 587 | # Up to here all watchlist actions except 'view' got handled and |
|---|
| 588 | # either redirected the request or set the action to 'view'. |
|---|
| 589 | if action != "view": |
|---|
| 590 | raise HTTPBadRequest(_("Invalid watchlist action '%(action)s'!", action=action)) |
|---|
| 591 | # Display watchlist page: |
|---|
| 592 | |
|---|
| 593 | if onwatchlistpage: |
|---|
| 594 | wldict['show_messages'] = options['show_messages_while_on_watchlist_page'] |
|---|
| 595 | else: |
|---|
| 596 | wldict['show_messages'] = options['show_messages_on_watchlist_page'] |
|---|
| 597 | |
|---|
| 598 | wldict['perm'] = req.perm |
|---|
| 599 | offset = datetime.now(req.tz).utcoffset() |
|---|
| 600 | if offset is None: |
|---|
| 601 | offset = 0 |
|---|
| 602 | else: |
|---|
| 603 | offset = offset.days * 24 * 60 + offset.seconds / 60 |
|---|
| 604 | wldict['tzoffset'] = offset |
|---|
| 605 | wldict['i18n'] = i18n_enabled |
|---|
| 606 | wldict['realms'] = [ r for r in settings['listoptions']['realm_order'] if r in self.realms ] |
|---|
| 607 | wldict['notifications'] = bool(self.wsub and options['notifications'] and options['display_notify_column']) |
|---|
| 608 | wldict['booloptions'] = options |
|---|
| 609 | wldict['booloptions_doc'] = settings['booloptions_doc'] |
|---|
| 610 | wldict['booloptions_order'] = settings['booloptions_order'] |
|---|
| 611 | wldict['listoptions'] = settings['listoptions'] |
|---|
| 612 | wldict['listoptions_doc'] = settings['listoptions_doc'] |
|---|
| 613 | wldict['listoptions_order'] = settings['listoptions_order'] |
|---|
| 614 | wldict['wlgettext'] = gettext |
|---|
| 615 | locale = getattr(req, 'locale', None) or LC_TIME |
|---|
| 616 | wldict['datetime_format'] = datetime_format(locale=locale) |
|---|
| 617 | wldict['_'] = _ |
|---|
| 618 | wldict['t_'] = t_ |
|---|
| 619 | def get_label(realm, n_plural=1, astitle=False): |
|---|
| 620 | return self.realm_handler[realm].get_realm_label(realm, n_plural, astitle) |
|---|
| 621 | wldict['get_label'] = get_label |
|---|
| 622 | |
|---|
| 623 | wldict['available_fields'] = {} |
|---|
| 624 | wldict['default_fields'] = {} |
|---|
| 625 | for r in self.realms: |
|---|
| 626 | wldict['available_fields'][r],wldict['default_fields'][r] = self.realm_handler[r].get_fields(r) |
|---|
| 627 | wldict['active_fields'] = {} |
|---|
| 628 | for r in self.realms: |
|---|
| 629 | cols = settings.get(r + '_fields',[]) |
|---|
| 630 | if not cols: |
|---|
| 631 | cols = wldict['default_fields'].get(r,[]) |
|---|
| 632 | wldict['active_fields'][r] = cols |
|---|
| 633 | |
|---|
| 634 | # TRANSLATOR: Link to help/manual page of the plug-in |
|---|
| 635 | add_ctxtnav(req, _("Help"), href=( |
|---|
| 636 | self.env[WatchlistManual] and req.href.watchlist('manual') |
|---|
| 637 | or 'http://www.trac-hacks.org/wiki/WatchlistPlugin')) |
|---|
| 638 | for xrealm in wldict['realms']: |
|---|
| 639 | xhandler = self.realm_handler[xrealm] |
|---|
| 640 | if xhandler.has_perm(xrealm, req.perm): |
|---|
| 641 | wldict[str(xrealm) + 'list'], wldict[str(xrealm) + 'data'] = xhandler.get_list(xrealm, self, req, wldict['active_fields'][xrealm]) |
|---|
| 642 | name = xhandler.get_realm_label(xrealm, n_plural=1000, astitle=True) |
|---|
| 643 | # TRANSLATOR: Navigation link to point to watchlist section of this realm |
|---|
| 644 | # (e.g. 'Wikis', 'Tickets'). |
|---|
| 645 | add_ctxtnav(req, _("Watched %(realm_plural)s", realm_plural=name), |
|---|
| 646 | href='#' + xrealm + 's') |
|---|
| 647 | add_ctxtnav(req, t_("Preferences"), href='#preferences') |
|---|
| 648 | return ("watchlist.html", wldict, "text/html") |
|---|
| 649 | |
|---|
| 650 | |
|---|
| 651 | ## Methods for IRequestFilter ########################################### |
|---|
| 652 | def pre_process_request(self, req, handler): |
|---|
| 653 | return handler |
|---|
| 654 | |
|---|
| 655 | |
|---|
| 656 | def post_process_request(self, req, template, data, content_type): |
|---|
| 657 | """Executed after EVERY request is processed. |
|---|
| 658 | Used to add navigation bars, display messages |
|---|
| 659 | and to note visits to watched resources.""" |
|---|
| 660 | user = to_unicode( req.authname ) |
|---|
| 661 | if not user or user == "anonymous": |
|---|
| 662 | return (template, data, content_type) |
|---|
| 663 | |
|---|
| 664 | # Extract realm and resid from path: |
|---|
| 665 | parts = req.path_info[1:].split('/',1) |
|---|
| 666 | |
|---|
| 667 | try: |
|---|
| 668 | realm, resid = parts[:2] |
|---|
| 669 | except: |
|---|
| 670 | # Handle special case for '/' and '/wiki' |
|---|
| 671 | if parts[0] == 'wiki' or (parts[0] == '' and |
|---|
| 672 | 'WikiModule' == self.env.config.get('trac','default_handler') ): |
|---|
| 673 | realm, resid = 'wiki', 'WikiStart' |
|---|
| 674 | else: |
|---|
| 675 | realm, resid = parts[0], '' |
|---|
| 676 | |
|---|
| 677 | if realm not in self.realms or not \ |
|---|
| 678 | self.realm_handler[realm].has_perm(realm, req.perm): |
|---|
| 679 | return (template, data, content_type) |
|---|
| 680 | |
|---|
| 681 | notify = 'False' |
|---|
| 682 | # The notification setting is stored in the session to avoid rereading |
|---|
| 683 | # the whole user settings for every page displayed |
|---|
| 684 | try: |
|---|
| 685 | notify = req.session['watchlist_display_notify_navitems'] |
|---|
| 686 | except KeyError: |
|---|
| 687 | settings = self.get_settings(user) |
|---|
| 688 | options = settings['booloptions'] |
|---|
| 689 | notify = (self.wsub and options['notifications'] |
|---|
| 690 | and options['display_notify_navitems']) and 'True' or 'False' |
|---|
| 691 | req.session['watchlist_display_notify_navitems'] = notify |
|---|
| 692 | |
|---|
| 693 | try: |
|---|
| 694 | add_notice(req, req.session['watchlist_message']) |
|---|
| 695 | del req.session['watchlist_message'] |
|---|
| 696 | except KeyError: |
|---|
| 697 | pass |
|---|
| 698 | try: |
|---|
| 699 | add_notice(req, req.session['watchlist_notify_message']) |
|---|
| 700 | del req.session['watchlist_notify_message'] |
|---|
| 701 | except KeyError: |
|---|
| 702 | pass |
|---|
| 703 | |
|---|
| 704 | if self.is_watching(realm, resid, user): |
|---|
| 705 | add_ctxtnav(req, _("Unwatch"), |
|---|
| 706 | href=req.href('watchlist', action='unwatch', |
|---|
| 707 | resid=resid, realm=realm), |
|---|
| 708 | title=_("Remove %(document)s from watchlist", |
|---|
| 709 | document=realm)) |
|---|
| 710 | self.visiting(realm, resid, user) |
|---|
| 711 | else: |
|---|
| 712 | add_ctxtnav(req, _("Watch"), |
|---|
| 713 | href=req.href('watchlist', action='watch', |
|---|
| 714 | resid=resid, realm=realm), |
|---|
| 715 | title=_("Add %(document)s to watchlist", document=realm)) |
|---|
| 716 | if notify == 'True': |
|---|
| 717 | if self.is_notify(req, realm, resid): |
|---|
| 718 | add_ctxtnav(req, _("Do not Notify me"), |
|---|
| 719 | href=req.href('watchlist', action='notifyoff', |
|---|
| 720 | resid=resid, realm=realm), |
|---|
| 721 | title=_("Do not notify me if %(document)s changes", |
|---|
| 722 | document=realm)) |
|---|
| 723 | else: |
|---|
| 724 | add_ctxtnav(req, _("Notify me"), |
|---|
| 725 | href=req.href('watchlist', action='notifyon', |
|---|
| 726 | resid=resid, realm=realm), |
|---|
| 727 | title=_("Notify me if %(document)s changes", |
|---|
| 728 | document=realm)) |
|---|
| 729 | |
|---|
| 730 | return (template, data, content_type) |
|---|
| 731 | |
|---|
| 732 | |
|---|
| 733 | def res_exists(self, realm, resid): |
|---|
| 734 | return self.realm_handler[realm].resources_exists(realm, [resid]) |
|---|
| 735 | |
|---|
| 736 | |
|---|
| 737 | ## Methods for ITemplateProvider ######################################## |
|---|
| 738 | def get_htdocs_dirs(self): |
|---|
| 739 | return [('watchlist', resource_filename(__name__, 'htdocs'))] |
|---|
| 740 | |
|---|
| 741 | |
|---|
| 742 | def get_templates_dirs(self): |
|---|
| 743 | return [ resource_filename(__name__, 'templates') ] |
|---|
| 744 | |
|---|
| 745 | # EOF |
|---|