| 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: ticket.py 15264 2016-02-11 04:22:34Z rjollos $ |
|---|
| 26 | """ |
|---|
| 27 | |
|---|
| 28 | __url__ = ur"$URL: //trac-hacks.org/svn/watchlistplugin/0.12/tracwatchlist/ticket.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 trac.core import * |
|---|
| 34 | from genshi.builder import tag |
|---|
| 35 | from trac.ticket.model import Ticket |
|---|
| 36 | from trac.ticket.api import TicketSystem |
|---|
| 37 | from trac.util.datefmt import pretty_timedelta, \ |
|---|
| 38 | datetime, utc, to_timestamp |
|---|
| 39 | from trac.util.text import to_unicode, obfuscate_email_address |
|---|
| 40 | from trac.wiki.formatter import format_to_oneliner |
|---|
| 41 | from trac.mimeview.api import Context |
|---|
| 42 | from trac.web.chrome import Chrome |
|---|
| 43 | from trac.resource import Resource |
|---|
| 44 | from trac.attachment import Attachment |
|---|
| 45 | |
|---|
| 46 | from trac.util.datefmt import format_datetime as trac_format_datetime |
|---|
| 47 | |
|---|
| 48 | from tracwatchlist.api import BasicWatchlist |
|---|
| 49 | from tracwatchlist.translation import add_domain, _, N_, T_, t_, tag_, ngettext |
|---|
| 50 | from tracwatchlist.render import render_property_diff |
|---|
| 51 | from tracwatchlist.util import moreless, format_datetime, LC_TIME,\ |
|---|
| 52 | decode_range_sql |
|---|
| 53 | |
|---|
| 54 | |
|---|
| 55 | class TicketWatchlist(BasicWatchlist): |
|---|
| 56 | """Watchlist entry for tickets.""" |
|---|
| 57 | realms = ['ticket'] |
|---|
| 58 | fields = {'ticket':{ |
|---|
| 59 | 'author' : T_("Author"), |
|---|
| 60 | 'changes' : N_("Changes"), |
|---|
| 61 | # TRANSLATOR: '#' stands for 'number'. |
|---|
| 62 | # This is the header label for a column showing the number |
|---|
| 63 | # of the latest comment. |
|---|
| 64 | 'commentnum': N_("Comment #"), |
|---|
| 65 | 'unwatch' : N_("U"), |
|---|
| 66 | 'notify' : N_("Notify"), |
|---|
| 67 | 'comment' : T_("Comment"), |
|---|
| 68 | 'attachment': T_("Attachments"), |
|---|
| 69 | # Plus further pairs imported at __init__. |
|---|
| 70 | }} |
|---|
| 71 | |
|---|
| 72 | default_fields = {'ticket':[ |
|---|
| 73 | 'id', 'changetime', 'author', 'changes', 'commentnum', |
|---|
| 74 | 'unwatch', 'notify', 'comment', |
|---|
| 75 | ]} |
|---|
| 76 | sort_key = {'ticket':int} |
|---|
| 77 | |
|---|
| 78 | tagsystem = None |
|---|
| 79 | |
|---|
| 80 | def __init__(self): |
|---|
| 81 | try: # Only works for Trac 0.12, but is not needed for Trac 0.11 anyway |
|---|
| 82 | self.fields['ticket'].update( self.env[TicketSystem].get_ticket_field_labels() ) |
|---|
| 83 | except (KeyError, AttributeError): |
|---|
| 84 | pass |
|---|
| 85 | self.fields['ticket']['id'] = self.get_realm_label('ticket') |
|---|
| 86 | |
|---|
| 87 | try: # Try to support the Tags Plugin |
|---|
| 88 | from tractags.api import TagSystem |
|---|
| 89 | self.tagsystem = self.env[TagSystem] |
|---|
| 90 | except ImportError, e: |
|---|
| 91 | pass |
|---|
| 92 | else: |
|---|
| 93 | if self.tagsystem: |
|---|
| 94 | self.fields['ticket']['tags'] = _("Tags") |
|---|
| 95 | |
|---|
| 96 | |
|---|
| 97 | def get_realm_label(self, realm, n_plural=1, astitle=False): |
|---|
| 98 | if astitle: |
|---|
| 99 | # TRANSLATOR: 'ticket(s)' as title |
|---|
| 100 | return ngettext("Ticket", "Tickets", n_plural) |
|---|
| 101 | else: |
|---|
| 102 | # TRANSLATOR: 'ticket(s)' inside a sentence |
|---|
| 103 | return ngettext("ticket", "tickets", n_plural) |
|---|
| 104 | |
|---|
| 105 | |
|---|
| 106 | def _get_sql(self, resids, fuzzy, var='id'): |
|---|
| 107 | if isinstance(resids,basestring): |
|---|
| 108 | sql = decode_range_sql( resids ) % {'var':var} |
|---|
| 109 | args = [] |
|---|
| 110 | else: |
|---|
| 111 | args = resids |
|---|
| 112 | if (len(resids) == 1): |
|---|
| 113 | sql = ' ' + var + '=%s ' |
|---|
| 114 | else: |
|---|
| 115 | sql = ' ' + var + ' IN (' + ','.join(('%s',) * len(resids)) + ') ' |
|---|
| 116 | return sql, args |
|---|
| 117 | |
|---|
| 118 | |
|---|
| 119 | def resources_exists(self, realm, resids, fuzzy=0): |
|---|
| 120 | if not resids: |
|---|
| 121 | return [] |
|---|
| 122 | sql, args = self._get_sql(resids, fuzzy) |
|---|
| 123 | if not sql: |
|---|
| 124 | return [] |
|---|
| 125 | db = self.env.get_db_cnx() |
|---|
| 126 | cursor = db.cursor() |
|---|
| 127 | cursor.execute(""" |
|---|
| 128 | SELECT id |
|---|
| 129 | FROM ticket |
|---|
| 130 | WHERE |
|---|
| 131 | """ + sql, args) |
|---|
| 132 | return [ unicode(v[0]) for v in cursor.fetchall() ] |
|---|
| 133 | |
|---|
| 134 | |
|---|
| 135 | def watched_resources(self, realm, resids, user, wl, fuzzy=0): |
|---|
| 136 | if not resids: |
|---|
| 137 | return [] |
|---|
| 138 | sql, args = self._get_sql(resids, fuzzy, 'CAST(resid AS decimal)') |
|---|
| 139 | if not sql: |
|---|
| 140 | return [] |
|---|
| 141 | db = self.env.get_db_cnx() |
|---|
| 142 | cursor = db.cursor() |
|---|
| 143 | cursor.log = self.log |
|---|
| 144 | cursor.execute(""" |
|---|
| 145 | SELECT resid |
|---|
| 146 | FROM watchlist |
|---|
| 147 | WHERE wluser=%s AND realm='ticket' AND ( |
|---|
| 148 | """ + sql + " )", [user] + args) |
|---|
| 149 | return [ unicode(v[0]) for v in cursor.fetchall() ] |
|---|
| 150 | |
|---|
| 151 | |
|---|
| 152 | def unwatched_resources(self, realm, resids, user, wl, fuzzy=0): |
|---|
| 153 | if not resids: |
|---|
| 154 | return [] |
|---|
| 155 | sql, args = self._get_sql(resids, fuzzy) |
|---|
| 156 | if not sql: |
|---|
| 157 | return [] |
|---|
| 158 | db = self.env.get_db_cnx() |
|---|
| 159 | cursor = db.cursor() |
|---|
| 160 | cursor.log = self.log |
|---|
| 161 | cursor.execute(""" |
|---|
| 162 | SELECT id |
|---|
| 163 | FROM ticket |
|---|
| 164 | WHERE id NOT in ( |
|---|
| 165 | SELECT CAST(resid as decimal) |
|---|
| 166 | FROM watchlist |
|---|
| 167 | WHERE wluser=%s AND realm='ticket' |
|---|
| 168 | ) AND ( |
|---|
| 169 | """ + sql + " )", [user] + args) |
|---|
| 170 | return [ unicode(v[0]) for v in cursor.fetchall() ] |
|---|
| 171 | |
|---|
| 172 | |
|---|
| 173 | def get_list(self, realm, wl, req, fields=None): |
|---|
| 174 | db = self.env.get_db_cnx() |
|---|
| 175 | cursor = db.cursor() |
|---|
| 176 | context = Context.from_request(req) |
|---|
| 177 | locale = getattr(req, 'locale', None) or LC_TIME |
|---|
| 178 | |
|---|
| 179 | ticketlist = [] |
|---|
| 180 | extradict = {} |
|---|
| 181 | if not fields: |
|---|
| 182 | fields = set(self.default_fields['ticket']) |
|---|
| 183 | else: |
|---|
| 184 | fields = set(fields) |
|---|
| 185 | |
|---|
| 186 | if 'changetime' in fields: |
|---|
| 187 | max_changetime = datetime(1970,1,1,tzinfo=utc) |
|---|
| 188 | min_changetime = datetime.now(utc) |
|---|
| 189 | if 'time' in fields: |
|---|
| 190 | max_time = datetime(1970,1,1,tzinfo=utc) |
|---|
| 191 | min_time = datetime.now(utc) |
|---|
| 192 | |
|---|
| 193 | |
|---|
| 194 | for sid,last_visit in wl.get_watched_resources( 'ticket', req.authname ): |
|---|
| 195 | ticketdict = {} |
|---|
| 196 | try: |
|---|
| 197 | ticket = Ticket(self.env, sid, db) |
|---|
| 198 | exists = ticket.exists |
|---|
| 199 | except: |
|---|
| 200 | exists = False |
|---|
| 201 | |
|---|
| 202 | if not exists: |
|---|
| 203 | ticketdict['deleted'] = True |
|---|
| 204 | if 'id' in fields: |
|---|
| 205 | ticketdict['id'] = sid |
|---|
| 206 | ticketdict['ID'] = '#' + sid |
|---|
| 207 | if 'author' in fields: |
|---|
| 208 | ticketdict['author'] = '?' |
|---|
| 209 | if 'changetime' in fields: |
|---|
| 210 | ticketdict['changedsincelastvisit'] = 1 |
|---|
| 211 | ticketdict['changetime'] = '?' |
|---|
| 212 | ticketdict['ichangetime'] = 0 |
|---|
| 213 | if 'time' in fields: |
|---|
| 214 | ticketdict['time'] = '?' |
|---|
| 215 | ticketdict['itime'] = 0 |
|---|
| 216 | if 'comment' in fields: |
|---|
| 217 | ticketdict['comment'] = tag.strong(t_("deleted"), class_='deleted') |
|---|
| 218 | if 'notify' in fields: |
|---|
| 219 | ticketdict['notify'] = wl.is_notify(req, 'ticket', sid) |
|---|
| 220 | if 'description' in fields: |
|---|
| 221 | ticketdict['description'] = '' |
|---|
| 222 | if 'owner' in fields: |
|---|
| 223 | ticketdict['owner'] = '' |
|---|
| 224 | if 'reporter' in fields: |
|---|
| 225 | ticketdict['reporter'] = '' |
|---|
| 226 | ticketlist.append(ticketdict) |
|---|
| 227 | continue |
|---|
| 228 | |
|---|
| 229 | render_elt = lambda x: x |
|---|
| 230 | if not (Chrome(self.env).show_email_addresses or \ |
|---|
| 231 | 'EMAIL_VIEW' in req.perm(ticket.resource)): |
|---|
| 232 | render_elt = obfuscate_email_address |
|---|
| 233 | |
|---|
| 234 | # Copy all requested fields from ticket |
|---|
| 235 | if fields: |
|---|
| 236 | for f in fields: |
|---|
| 237 | ticketdict[f] = ticket.values.get(f,u'') |
|---|
| 238 | else: |
|---|
| 239 | ticketdict = ticket.values.copy() |
|---|
| 240 | |
|---|
| 241 | changetime = ticket.time_changed |
|---|
| 242 | if wl.options['attachment_changes']: |
|---|
| 243 | for attachment in Attachment.select(self.env, 'ticket', sid, db): |
|---|
| 244 | if attachment.date > changetime: |
|---|
| 245 | changetime = attachment.date |
|---|
| 246 | if 'attachment' in fields: |
|---|
| 247 | attachments = [] |
|---|
| 248 | for attachment in Attachment.select(self.env, 'ticket', sid, db): |
|---|
| 249 | wikitext = u'[attachment:"' + u':'.join([attachment.filename,'ticket',sid]) + u'" ' + attachment.filename + u']' |
|---|
| 250 | attachments.extend([tag(', '), format_to_oneliner(self.env, context, wikitext, shorten=False)]) |
|---|
| 251 | if attachments: |
|---|
| 252 | attachments.reverse() |
|---|
| 253 | attachments.pop() |
|---|
| 254 | ticketdict['attachment'] = moreless(attachments, 5) |
|---|
| 255 | |
|---|
| 256 | # Changes are special. Comment, commentnum and last author are included in them. |
|---|
| 257 | if 'changes' in fields or 'author' in fields or 'comment' in fields or 'commentnum' in fields: |
|---|
| 258 | changes = [] |
|---|
| 259 | # If there are now changes the reporter is the last author |
|---|
| 260 | author = ticket.values['reporter'] |
|---|
| 261 | commentnum = u"0" |
|---|
| 262 | comment = u"" |
|---|
| 263 | want_changes = 'changes' in fields |
|---|
| 264 | for date,cauthor,field,oldvalue,newvalue,permanent in ticket.get_changelog(changetime,db): |
|---|
| 265 | author = cauthor |
|---|
| 266 | if field == 'comment': |
|---|
| 267 | if 'commentnum' in fields: |
|---|
| 268 | ticketdict['commentnum'] = to_unicode(oldvalue) |
|---|
| 269 | if 'comment' in fields: |
|---|
| 270 | comment = to_unicode(newvalue) |
|---|
| 271 | comment = moreless(comment, 200) |
|---|
| 272 | ticketdict['comment'] = comment |
|---|
| 273 | if not want_changes: |
|---|
| 274 | break |
|---|
| 275 | else: |
|---|
| 276 | if want_changes: |
|---|
| 277 | label = self.fields['ticket'].get(field,u'') |
|---|
| 278 | if label: |
|---|
| 279 | changes.extend( |
|---|
| 280 | [ tag(tag.strong(label), ' ', |
|---|
| 281 | render_property_diff(self.env, req, ticket, field, oldvalue, newvalue) |
|---|
| 282 | ), tag('; ') ]) |
|---|
| 283 | if want_changes: |
|---|
| 284 | # Remove the last tag('; '): |
|---|
| 285 | if changes: |
|---|
| 286 | changes.pop() |
|---|
| 287 | changes = moreless(changes, 5) |
|---|
| 288 | ticketdict['changes'] = tag(changes) |
|---|
| 289 | |
|---|
| 290 | if 'id' in fields: |
|---|
| 291 | ticketdict['id'] = sid |
|---|
| 292 | ticketdict['ID'] = format_to_oneliner(self.env, context, '#' + sid, shorten=True) |
|---|
| 293 | if 'cc' in fields: |
|---|
| 294 | if render_elt == obfuscate_email_address: |
|---|
| 295 | ticketdict['cc'] = ', '.join([ render_elt(c) for c in ticketdict['cc'].split(', ') ]) |
|---|
| 296 | if 'author' in fields: |
|---|
| 297 | ticketdict['author'] = render_elt(author) |
|---|
| 298 | if 'changetime' in fields: |
|---|
| 299 | ichangetime = to_timestamp( changetime ) |
|---|
| 300 | ticketdict.update( |
|---|
| 301 | changetime = format_datetime( changetime, locale=locale, tzinfo=req.tz ), |
|---|
| 302 | ichangetime = ichangetime, |
|---|
| 303 | changedsincelastvisit = (last_visit < ichangetime and 1 or 0), |
|---|
| 304 | changetime_delta = pretty_timedelta( changetime ), |
|---|
| 305 | changetime_link = req.href.timeline(precision='seconds', |
|---|
| 306 | from_=trac_format_datetime ( changetime, 'iso8601', tzinfo=req.tz))) |
|---|
| 307 | if changetime > max_changetime: |
|---|
| 308 | max_changetime = changetime |
|---|
| 309 | if changetime < min_changetime: |
|---|
| 310 | min_changetime = changetime |
|---|
| 311 | if 'time' in fields: |
|---|
| 312 | time = ticket.time_created |
|---|
| 313 | ticketdict.update( |
|---|
| 314 | time = format_datetime( time, locale=locale, tzinfo=req.tz ), |
|---|
| 315 | itime = to_timestamp( time ), |
|---|
| 316 | time_delta = pretty_timedelta( time ), |
|---|
| 317 | time_link = req.href.timeline(precision='seconds', |
|---|
| 318 | from_=trac_format_datetime ( time, 'iso8601', tzinfo=req.tz ))) |
|---|
| 319 | if time > max_time: |
|---|
| 320 | max_time = time |
|---|
| 321 | if time < min_time: |
|---|
| 322 | min_time = time |
|---|
| 323 | if 'description' in fields: |
|---|
| 324 | description = ticket.values['description'] |
|---|
| 325 | description = moreless(description, 200) |
|---|
| 326 | ticketdict['description'] = description |
|---|
| 327 | if 'notify' in fields: |
|---|
| 328 | ticketdict['notify'] = wl.is_notify(req, 'ticket', sid) |
|---|
| 329 | if 'owner' in fields: |
|---|
| 330 | ticketdict['owner'] = render_elt(ticket.values['owner']) |
|---|
| 331 | if 'reporter' in fields: |
|---|
| 332 | ticketdict['reporter'] = render_elt(ticket.values['reporter']) |
|---|
| 333 | if 'tags' in fields and self.tagsystem: |
|---|
| 334 | tags = [] |
|---|
| 335 | for t in self.tagsystem.get_tags(req, Resource('ticket', sid)): |
|---|
| 336 | tags.extend([tag.a(t,href=req.href('tags',q=t)), tag(', ')]) |
|---|
| 337 | if tags: |
|---|
| 338 | tags.pop() |
|---|
| 339 | ticketdict['tags'] = moreless(tags, 10) |
|---|
| 340 | |
|---|
| 341 | ticketlist.append(ticketdict) |
|---|
| 342 | |
|---|
| 343 | if 'changetime' in fields: |
|---|
| 344 | extradict['max_changetime'] = format_datetime( max_changetime, locale=locale, tzinfo=req.tz ) |
|---|
| 345 | extradict['min_changetime'] = format_datetime( min_changetime, locale=locale, tzinfo=req.tz ) |
|---|
| 346 | if 'time' in fields: |
|---|
| 347 | extradict['max_time'] = format_datetime( max_time, locale=locale, tzinfo=req.tz ) |
|---|
| 348 | extradict['min_time'] = format_datetime( min_time, locale=locale, tzinfo=req.tz ) |
|---|
| 349 | |
|---|
| 350 | return ticketlist, extradict |
|---|
| 351 | |
|---|
| 352 | _EXTRA_STRINGS = [ _("%(value)s added") ] |
|---|
| 353 | |
|---|
| 354 | # EOF |
|---|