| 1 | # -*- coding: iso-8859-1 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2003-2005 Edgewall Software |
|---|
| 4 | # Copyright (C) 2003-2005 Jonas Borgstr�jonas@edgewall.com> |
|---|
| 5 | # All rights reserved. |
|---|
| 6 | # |
|---|
| 7 | # This software is licensed as described in the file COPYING, which |
|---|
| 8 | # you should have received as part of this distribution. The terms |
|---|
| 9 | # are also available at http://trac.edgewall.com/license.html. |
|---|
| 10 | # |
|---|
| 11 | # This software consists of voluntary contributions made by many |
|---|
| 12 | # individuals. For the exact contribution history, see the revision |
|---|
| 13 | # history and logs, available at http://projects.edgewall.com/trac/. |
|---|
| 14 | # |
|---|
| 15 | # Author: Jonas Borgstr�jonas@edgewall.com> |
|---|
| 16 | |
|---|
| 17 | from __future__ import generators |
|---|
| 18 | import os |
|---|
| 19 | import re |
|---|
| 20 | import time |
|---|
| 21 | |
|---|
| 22 | from trac import util |
|---|
| 23 | from trac.attachment import attachment_to_hdf, Attachment |
|---|
| 24 | from trac.core import * |
|---|
| 25 | from trac.env import IEnvironmentSetupParticipant |
|---|
| 26 | from trac.Notify import TicketNotifyEmail |
|---|
| 27 | from trac.ticket import Milestone, Ticket, TicketSystem |
|---|
| 28 | from trac.Timeline import ITimelineEventProvider |
|---|
| 29 | from trac.web import IRequestHandler |
|---|
| 30 | from trac.web.chrome import add_link, add_stylesheet, INavigationContributor |
|---|
| 31 | from trac.wiki import wiki_to_html, wiki_to_oneliner |
|---|
| 32 | |
|---|
| 33 | class NewticketModule(Component): |
|---|
| 34 | |
|---|
| 35 | implements(IEnvironmentSetupParticipant, INavigationContributor, |
|---|
| 36 | IRequestHandler) |
|---|
| 37 | |
|---|
| 38 | # IEnvironmentSetupParticipant methods |
|---|
| 39 | |
|---|
| 40 | def environment_created(self): |
|---|
| 41 | """Create the `site_newticket.cs` template file in the environment.""" |
|---|
| 42 | if self.env.path: |
|---|
| 43 | templates_dir = os.path.join(self.env.path, 'templates') |
|---|
| 44 | if not os.path.exists(templates_dir): |
|---|
| 45 | os.mkdir(templates_dir) |
|---|
| 46 | template_name = os.path.join(templates_dir, 'site_newticket.cs') |
|---|
| 47 | template_file = file(template_name, 'w') |
|---|
| 48 | template_file.write("""<?cs |
|---|
| 49 | #################################################################### |
|---|
| 50 | # New ticket prelude - Included directly above the new ticket form |
|---|
| 51 | ?> |
|---|
| 52 | """) |
|---|
| 53 | |
|---|
| 54 | def environment_needs_upgrade(self, db): |
|---|
| 55 | return False |
|---|
| 56 | |
|---|
| 57 | def upgrade_environment(self, db): |
|---|
| 58 | pass |
|---|
| 59 | |
|---|
| 60 | # INavigationContributor methods |
|---|
| 61 | |
|---|
| 62 | def get_active_navigation_item(self, req): |
|---|
| 63 | return 'newticket' |
|---|
| 64 | |
|---|
| 65 | def get_navigation_items(self, req): |
|---|
| 66 | if not req.perm.has_permission('TICKET_CREATE'): |
|---|
| 67 | return |
|---|
| 68 | yield ('mainnav', 'newticket', |
|---|
| 69 | util.Markup('<a href="%s" accesskey="7">New Ticket</a>', |
|---|
| 70 | self.env.href.newticket())) |
|---|
| 71 | |
|---|
| 72 | # IRequestHandler methods |
|---|
| 73 | |
|---|
| 74 | def match_request(self, req): |
|---|
| 75 | return re.match(r'/newticket/?', req.path_info) is not None |
|---|
| 76 | |
|---|
| 77 | def process_request(self, req): |
|---|
| 78 | req.perm.assert_permission('TICKET_CREATE') |
|---|
| 79 | |
|---|
| 80 | db = self.env.get_db_cnx() |
|---|
| 81 | |
|---|
| 82 | if req.method == 'POST' and not req.args.has_key('preview'): |
|---|
| 83 | self._do_create(req, db) |
|---|
| 84 | |
|---|
| 85 | ticket = Ticket(self.env, db=db) |
|---|
| 86 | ticket.populate(req.args) |
|---|
| 87 | ticket.values.setdefault('reporter', util.get_reporter_id(req)) |
|---|
| 88 | |
|---|
| 89 | if ticket.values.has_key('description'): |
|---|
| 90 | description = wiki_to_html(ticket['description'], self.env, req, db) |
|---|
| 91 | req.hdf['newticket.description_preview'] = description |
|---|
| 92 | |
|---|
| 93 | req.hdf['title'] = 'New Ticket' |
|---|
| 94 | req.hdf['newticket'] = ticket.values |
|---|
| 95 | |
|---|
| 96 | field_names = [field['name'] for field in ticket.fields |
|---|
| 97 | if not field.get('custom')] |
|---|
| 98 | if 'owner' in field_names: |
|---|
| 99 | curr_idx = field_names.index('owner') |
|---|
| 100 | if 'cc' in field_names: |
|---|
| 101 | insert_idx = field_names.index('cc') |
|---|
| 102 | else: |
|---|
| 103 | insert_idx = len(field_names) |
|---|
| 104 | if curr_idx < insert_idx: |
|---|
| 105 | ticket.fields.insert(insert_idx, ticket.fields[curr_idx]) |
|---|
| 106 | del ticket.fields[curr_idx] |
|---|
| 107 | |
|---|
| 108 | for field in ticket.fields: |
|---|
| 109 | name = field['name'] |
|---|
| 110 | del field['name'] |
|---|
| 111 | if name in ('summary', 'reporter', 'description', 'type', 'status', |
|---|
| 112 | 'resolution'): |
|---|
| 113 | field['skip'] = True |
|---|
| 114 | elif name == 'owner': |
|---|
| 115 | field['label'] = 'Assign to' |
|---|
| 116 | elif name == 'milestone': |
|---|
| 117 | # Don't make completed milestones available for selection |
|---|
| 118 | options = field['options'][:] |
|---|
| 119 | for option in field['options']: |
|---|
| 120 | milestone = Milestone(self.env, option, db=db) |
|---|
| 121 | if milestone.is_completed: |
|---|
| 122 | options.remove(option) |
|---|
| 123 | field['options'] = options |
|---|
| 124 | req.hdf['newticket.fields.' + name] = field |
|---|
| 125 | |
|---|
| 126 | add_stylesheet(req, 'common/css/ticket.css') |
|---|
| 127 | return 'newticket.cs', None |
|---|
| 128 | |
|---|
| 129 | # Internal methods |
|---|
| 130 | |
|---|
| 131 | def _do_create(self, req, db): |
|---|
| 132 | if not req.args.get('summary'): |
|---|
| 133 | raise TracError('Tickets must contain a summary.') |
|---|
| 134 | |
|---|
| 135 | ticket = Ticket(self.env, db=db) |
|---|
| 136 | ticket.values.setdefault('reporter', util.get_reporter_id(req)) |
|---|
| 137 | ticket.populate(req.args) |
|---|
| 138 | ticket.insert(db=db) |
|---|
| 139 | db.commit() |
|---|
| 140 | |
|---|
| 141 | # Notify |
|---|
| 142 | try: |
|---|
| 143 | tn = TicketNotifyEmail(self.env) |
|---|
| 144 | tn.notify(req, ticket, newticket=True) |
|---|
| 145 | except Exception, e: |
|---|
| 146 | self.log.exception("Failure sending notification on creation of " |
|---|
| 147 | "ticket #%s: %s" % (ticket.id, e)) |
|---|
| 148 | |
|---|
| 149 | # Redirect the user to the newly created ticket |
|---|
| 150 | req.redirect(self.env.href.ticket(ticket.id)) |
|---|
| 151 | |
|---|
| 152 | |
|---|
| 153 | class TicketModule(Component): |
|---|
| 154 | |
|---|
| 155 | implements(INavigationContributor, IRequestHandler, ITimelineEventProvider) |
|---|
| 156 | |
|---|
| 157 | # INavigationContributor methods |
|---|
| 158 | |
|---|
| 159 | def get_active_navigation_item(self, req): |
|---|
| 160 | return 'tickets' |
|---|
| 161 | |
|---|
| 162 | def get_navigation_items(self, req): |
|---|
| 163 | return [] |
|---|
| 164 | |
|---|
| 165 | # IRequestHandler methods |
|---|
| 166 | |
|---|
| 167 | def match_request(self, req): |
|---|
| 168 | match = re.match(r'/ticket/([0-9]+)', req.path_info) |
|---|
| 169 | if match: |
|---|
| 170 | req.args['id'] = match.group(1) |
|---|
| 171 | return True |
|---|
| 172 | |
|---|
| 173 | def process_request(self, req): |
|---|
| 174 | req.perm.assert_permission('TICKET_VIEW') |
|---|
| 175 | |
|---|
| 176 | action = req.args.get('action', 'view') |
|---|
| 177 | |
|---|
| 178 | if not req.args.has_key('id'): |
|---|
| 179 | req.redirect(self.env.href.wiki()) |
|---|
| 180 | |
|---|
| 181 | db = self.env.get_db_cnx() |
|---|
| 182 | id = int(req.args.get('id')) |
|---|
| 183 | |
|---|
| 184 | ticket = Ticket(self.env, id, db=db) |
|---|
| 185 | reporter_id = util.get_reporter_id(req) |
|---|
| 186 | |
|---|
| 187 | if req.method == 'POST': |
|---|
| 188 | if not req.args.has_key('preview'): |
|---|
| 189 | self._do_save(req, db, ticket) |
|---|
| 190 | else: |
|---|
| 191 | # Use user supplied values |
|---|
| 192 | ticket.populate(req.args) |
|---|
| 193 | req.hdf['ticket.action'] = action |
|---|
| 194 | req.hdf['ticket.ts'] = req.args.get('ts') |
|---|
| 195 | req.hdf['ticket.reassign_owner'] = req.args.get('reassign_owner') \ |
|---|
| 196 | or req.authname |
|---|
| 197 | req.hdf['ticket.resolve_resolution'] = req.args.get('resolve_resolution') |
|---|
| 198 | reporter_id = req.args.get('author') |
|---|
| 199 | comment = req.args.get('comment') |
|---|
| 200 | if comment: |
|---|
| 201 | req.hdf['ticket.comment'] = comment |
|---|
| 202 | # Wiki format a preview of comment |
|---|
| 203 | req.hdf['ticket.comment_preview'] = wiki_to_html(comment, |
|---|
| 204 | self.env, |
|---|
| 205 | req, db) |
|---|
| 206 | else: |
|---|
| 207 | req.hdf['ticket.reassign_owner'] = req.authname |
|---|
| 208 | # Store a timestamp in order to detect "mid air collisions" |
|---|
| 209 | req.hdf['ticket.ts'] = ticket.time_changed |
|---|
| 210 | |
|---|
| 211 | self._insert_ticket_data(req, db, ticket, reporter_id) |
|---|
| 212 | |
|---|
| 213 | # If the ticket is being shown in the context of a query, add |
|---|
| 214 | # links to help navigate in the query result set |
|---|
| 215 | if 'query_tickets' in req.session: |
|---|
| 216 | tickets = req.session['query_tickets'].split() |
|---|
| 217 | if str(id) in tickets: |
|---|
| 218 | idx = tickets.index(str(ticket.id)) |
|---|
| 219 | if idx > 0: |
|---|
| 220 | add_link(req, 'first', self.env.href.ticket(tickets[0]), |
|---|
| 221 | 'Ticket #%s' % tickets[0]) |
|---|
| 222 | add_link(req, 'prev', self.env.href.ticket(tickets[idx - 1]), |
|---|
| 223 | 'Ticket #%s' % tickets[idx - 1]) |
|---|
| 224 | if idx < len(tickets) - 1: |
|---|
| 225 | add_link(req, 'next', self.env.href.ticket(tickets[idx + 1]), |
|---|
| 226 | 'Ticket #%s' % tickets[idx + 1]) |
|---|
| 227 | add_link(req, 'last', self.env.href.ticket(tickets[-1]), |
|---|
| 228 | 'Ticket #%s' % tickets[-1]) |
|---|
| 229 | add_link(req, 'up', req.session['query_href']) |
|---|
| 230 | |
|---|
| 231 | add_stylesheet(req, 'common/css/ticket.css') |
|---|
| 232 | return 'ticket.cs', None |
|---|
| 233 | |
|---|
| 234 | # ITimelineEventProvider methods |
|---|
| 235 | |
|---|
| 236 | def get_timeline_filters(self, req): |
|---|
| 237 | if req.perm.has_permission('TICKET_VIEW'): |
|---|
| 238 | yield ('ticket', 'Ticket changes') |
|---|
| 239 | |
|---|
| 240 | def get_timeline_events(self, req, start, stop, filters): |
|---|
| 241 | if 'ticket' in filters: |
|---|
| 242 | format = req.args.get('format') |
|---|
| 243 | sql = [] |
|---|
| 244 | |
|---|
| 245 | # New tickets |
|---|
| 246 | sql.append("SELECT time,id,'','new',type,summary,reporter,summary" |
|---|
| 247 | " FROM ticket WHERE time>=%s AND time<=%s") |
|---|
| 248 | |
|---|
| 249 | # Reopened tickets |
|---|
| 250 | sql.append("SELECT t1.time,t1.ticket,'','reopened',t.type," |
|---|
| 251 | " t2.newvalue,t1.author,t.summary " |
|---|
| 252 | " FROM ticket_change t1" |
|---|
| 253 | " LEFT OUTER JOIN ticket_change t2 ON (t1.time=t2.time" |
|---|
| 254 | " AND t1.ticket=t2.ticket AND t2.field='comment')" |
|---|
| 255 | " LEFT JOIN ticket t on t.id = t1.ticket " |
|---|
| 256 | " WHERE t1.field='status' AND t1.newvalue='reopened'" |
|---|
| 257 | " AND t1.time>=%s AND t1.time<=%s") |
|---|
| 258 | |
|---|
| 259 | # Closed tickets |
|---|
| 260 | sql.append("SELECT t1.time,t1.ticket,t2.newvalue,'closed',t.type," |
|---|
| 261 | " t3.newvalue,t1.author,t.summary" |
|---|
| 262 | " FROM ticket_change t1" |
|---|
| 263 | " INNER JOIN ticket_change t2 ON t1.ticket=t2.ticket" |
|---|
| 264 | " AND t1.time=t2.time" |
|---|
| 265 | " LEFT OUTER JOIN ticket_change t3 ON t1.time=t3.time" |
|---|
| 266 | " AND t1.ticket=t3.ticket AND t3.field='comment'" |
|---|
| 267 | " LEFT JOIN ticket t on t.id = t1.ticket " |
|---|
| 268 | " WHERE t1.field='status' AND t1.newvalue='closed'" |
|---|
| 269 | " AND t2.field='resolution'" |
|---|
| 270 | " AND t1.time>=%s AND t1.time<=%s") |
|---|
| 271 | |
|---|
| 272 | db = self.env.get_db_cnx() |
|---|
| 273 | cursor = db.cursor() |
|---|
| 274 | cursor.execute(" UNION ALL ".join(sql), (start, stop, start, stop, |
|---|
| 275 | start, stop)) |
|---|
| 276 | kinds = {'new': 'newticket', 'reopened': 'newticket', |
|---|
| 277 | 'closed': 'closedticket'} |
|---|
| 278 | verbs = {'new': 'created', 'reopened': 'reopened', |
|---|
| 279 | 'closed': 'closed'} |
|---|
| 280 | for t, id, resolution, status, type, message, author, summary \ |
|---|
| 281 | in cursor: |
|---|
| 282 | title = util.Markup('Ticket <em title="%s">#%s</em> (%s) %s by ' |
|---|
| 283 | '%s', summary, id, type, verbs[status], |
|---|
| 284 | author) |
|---|
| 285 | if format == 'rss': |
|---|
| 286 | href = self.env.abs_href.ticket(id) |
|---|
| 287 | if status != 'new': |
|---|
| 288 | message = wiki_to_html(message or '--', self.env, req, |
|---|
| 289 | db) |
|---|
| 290 | else: |
|---|
| 291 | message = util.escape(message) |
|---|
| 292 | else: |
|---|
| 293 | href = self.env.href.ticket(id) |
|---|
| 294 | if status != 'new': |
|---|
| 295 | message = util.Markup(': ').join(filter(None, [ |
|---|
| 296 | resolution, |
|---|
| 297 | wiki_to_oneliner(message, self.env, db, |
|---|
| 298 | shorten=True) |
|---|
| 299 | ])) |
|---|
| 300 | else: |
|---|
| 301 | message = util.escape(util.shorten_line(message)) |
|---|
| 302 | yield kinds[status], href, title, t, author, message |
|---|
| 303 | |
|---|
| 304 | # Internal methods |
|---|
| 305 | |
|---|
| 306 | def _do_save(self, req, db, ticket): |
|---|
| 307 | if req.perm.has_permission('TICKET_CHGPROP'): |
|---|
| 308 | # TICKET_CHGPROP gives permission to edit the ticket |
|---|
| 309 | if not req.args.get('summary'): |
|---|
| 310 | raise TracError('Tickets must contain summary.') |
|---|
| 311 | |
|---|
| 312 | if req.args.has_key('description') or req.args.has_key('reporter'): |
|---|
| 313 | req.perm.assert_permission('TICKET_ADMIN') |
|---|
| 314 | |
|---|
| 315 | ticket.populate(req.args) |
|---|
| 316 | else: |
|---|
| 317 | req.perm.assert_permission('TICKET_APPEND') |
|---|
| 318 | |
|---|
| 319 | # Mid air collision? |
|---|
| 320 | if int(req.args.get('ts')) != ticket.time_changed: |
|---|
| 321 | raise TracError("Sorry, can not save your changes. " |
|---|
| 322 | "This ticket has been modified by someone else " |
|---|
| 323 | "since you started", 'Mid Air Collision') |
|---|
| 324 | |
|---|
| 325 | # Do any action on the ticket? |
|---|
| 326 | action = req.args.get('action') |
|---|
| 327 | actions = TicketSystem(self.env).get_available_actions(ticket, req.perm) |
|---|
| 328 | if action not in actions: |
|---|
| 329 | raise TracError('Invalid action') |
|---|
| 330 | |
|---|
| 331 | # TODO: this should not be hard-coded like this |
|---|
| 332 | if action == 'accept': |
|---|
| 333 | ticket['status'] = 'assigned' |
|---|
| 334 | ticket['owner'] = req.authname |
|---|
| 335 | if action == 'resolve': |
|---|
| 336 | ticket['status'] = 'closed' |
|---|
| 337 | ticket['resolution'] = req.args.get('resolve_resolution') |
|---|
| 338 | elif action == 'reassign': |
|---|
| 339 | ticket['owner'] = req.args.get('reassign_owner') |
|---|
| 340 | ticket['status'] = 'new' |
|---|
| 341 | elif action == 'reopen': |
|---|
| 342 | ticket['status'] = 'reopened' |
|---|
| 343 | ticket['resolution'] = '' |
|---|
| 344 | |
|---|
| 345 | now = int(time.time()) |
|---|
| 346 | ticket.save_changes(req.args.get('author', req.authname), |
|---|
| 347 | req.args.get('comment'), when=now, db=db) |
|---|
| 348 | db.commit() |
|---|
| 349 | |
|---|
| 350 | try: |
|---|
| 351 | tn = TicketNotifyEmail(self.env) |
|---|
| 352 | tn.notify(req, ticket, newticket=False, modtime=now) |
|---|
| 353 | except Exception, e: |
|---|
| 354 | self.log.exception("Failure sending notification on change to " |
|---|
| 355 | "ticket #%s: %s" % (ticket.id, e)) |
|---|
| 356 | |
|---|
| 357 | req.redirect(self.env.href.ticket(ticket.id)) |
|---|
| 358 | |
|---|
| 359 | def _insert_ticket_data(self, req, db, ticket, reporter_id): |
|---|
| 360 | """Insert ticket data into the hdf""" |
|---|
| 361 | req.hdf['ticket'] = ticket.values |
|---|
| 362 | req.hdf['ticket.id'] = ticket.id |
|---|
| 363 | req.hdf['ticket.href'] = self.env.href.ticket(ticket.id) |
|---|
| 364 | |
|---|
| 365 | for field in TicketSystem(self.env).get_ticket_fields(): |
|---|
| 366 | if field['type'] in ('radio', 'select'): |
|---|
| 367 | value = ticket.values.get(field['name']) |
|---|
| 368 | options = field['options'] |
|---|
| 369 | if value and not value in options: |
|---|
| 370 | # Current ticket value must be visible even if its not in the |
|---|
| 371 | # possible values |
|---|
| 372 | options.append(value) |
|---|
| 373 | field['options'] = options |
|---|
| 374 | name = field['name'] |
|---|
| 375 | del field['name'] |
|---|
| 376 | if name in ('summary', 'reporter', 'description', 'type', 'status', |
|---|
| 377 | 'resolution', 'owner'): |
|---|
| 378 | field['skip'] = True |
|---|
| 379 | req.hdf['ticket.fields.' + name] = field |
|---|
| 380 | |
|---|
| 381 | req.hdf['ticket.reporter_id'] = reporter_id |
|---|
| 382 | req.hdf['title'] = '#%d (%s)' % (ticket.id, ticket['summary']) |
|---|
| 383 | req.hdf['ticket.description.formatted'] = wiki_to_html(ticket['description'], |
|---|
| 384 | self.env, req, db) |
|---|
| 385 | |
|---|
| 386 | req.hdf['ticket.opened'] = util.format_datetime(ticket.time_created) |
|---|
| 387 | req.hdf['ticket.opened_delta'] = util.pretty_timedelta(ticket.time_created) |
|---|
| 388 | if ticket.time_changed != ticket.time_created: |
|---|
| 389 | req.hdf['ticket.lastmod'] = util.format_datetime(ticket.time_changed) |
|---|
| 390 | req.hdf['ticket.lastmod_delta'] = util.pretty_timedelta(ticket.time_changed) |
|---|
| 391 | |
|---|
| 392 | changelog = ticket.get_changelog(db=db) |
|---|
| 393 | curr_author = None |
|---|
| 394 | curr_date = 0 |
|---|
| 395 | changes = [] |
|---|
| 396 | for date, author, field, old, new in changelog: |
|---|
| 397 | if date != curr_date or author != curr_author: |
|---|
| 398 | changes.append({ |
|---|
| 399 | 'date': util.format_datetime(date), |
|---|
| 400 | 'author': author, |
|---|
| 401 | 'fields': {} |
|---|
| 402 | }) |
|---|
| 403 | curr_date = date |
|---|
| 404 | curr_author = author |
|---|
| 405 | if field == 'comment': |
|---|
| 406 | changes[-1]['comment'] = wiki_to_html(new, self.env, req, db) |
|---|
| 407 | elif field == 'description': |
|---|
| 408 | changes[-1]['fields'][field] = '' |
|---|
| 409 | else: |
|---|
| 410 | changes[-1]['fields'][field] = {'old': old, |
|---|
| 411 | 'new': new} |
|---|
| 412 | req.hdf['ticket.changes'] = changes |
|---|
| 413 | |
|---|
| 414 | # List attached files |
|---|
| 415 | for idx, attachment in util.enum(Attachment.select(self.env, 'ticket', |
|---|
| 416 | ticket.id)): |
|---|
| 417 | hdf = attachment_to_hdf(self.env, db, req, attachment) |
|---|
| 418 | req.hdf['ticket.attachments.%s' % idx] = hdf |
|---|
| 419 | if req.perm.has_permission('TICKET_APPEND'): |
|---|
| 420 | req.hdf['ticket.attach_href'] = self.env.href.attachment('ticket', |
|---|
| 421 | ticket.id) |
|---|
| 422 | |
|---|
| 423 | # Add the possible actions to hdf |
|---|
| 424 | actions = TicketSystem(self.env).get_available_actions(ticket, req.perm) |
|---|
| 425 | for action in actions: |
|---|
| 426 | req.hdf['ticket.actions.' + action] = '1' |
|---|
| 427 | |
|---|
| 428 | |
|---|
| 429 | class UpdateDetailsForTimeline(Component): |
|---|
| 430 | """Provide all details about ticket changes in the Timeline""" |
|---|
| 431 | |
|---|
| 432 | implements(ITimelineEventProvider) |
|---|
| 433 | |
|---|
| 434 | # ITimelineEventProvider methods |
|---|
| 435 | |
|---|
| 436 | def get_timeline_filters(self, req): |
|---|
| 437 | if not self.config.getbool('timeline', 'ticket_show_details'): |
|---|
| 438 | return |
|---|
| 439 | if req.perm.has_permission('TICKET_VIEW'): |
|---|
| 440 | yield ('ticket_details', 'Ticket details', False) |
|---|
| 441 | |
|---|
| 442 | def get_timeline_events(self, req, start, stop, filters): |
|---|
| 443 | if 'ticket_details' in filters: |
|---|
| 444 | db = self.env.get_db_cnx() |
|---|
| 445 | cursor = db.cursor() |
|---|
| 446 | cursor.execute("SELECT tc.time,tc.ticket,t.type,tc.field, " |
|---|
| 447 | " tc.oldvalue,tc.newvalue,tc.author,t.summary " |
|---|
| 448 | "FROM ticket_change tc" |
|---|
| 449 | " INNER JOIN ticket t ON t.id = tc.ticket " |
|---|
| 450 | "AND tc.time>=%s AND tc.time<=%s ORDER BY tc.time" |
|---|
| 451 | % (start, stop)) |
|---|
| 452 | previous_update = None |
|---|
| 453 | updates = [] |
|---|
| 454 | ticket_change = False |
|---|
| 455 | for time,id,type,field,oldvalue,newvalue,author,summary in cursor: |
|---|
| 456 | if not previous_update or (time,id,author) != previous_update[:3]: |
|---|
| 457 | if previous_update and not ticket_change: |
|---|
| 458 | updates.append((previous_update,field_changes,comment)) |
|---|
| 459 | ticket_change = False |
|---|
| 460 | field_changes = [] |
|---|
| 461 | comment = '' |
|---|
| 462 | previous_update = (time,id,author,type,summary) |
|---|
| 463 | if field == 'comment': |
|---|
| 464 | comment = newvalue |
|---|
| 465 | elif field == 'status' and newvalue in ['reopened', 'closed']: |
|---|
| 466 | ticket_change = True |
|---|
| 467 | else: |
|---|
| 468 | field_changes.append(field) |
|---|
| 469 | if previous_update and not ticket_change: |
|---|
| 470 | updates.append((previous_update,field_changes,comment)) |
|---|
| 471 | |
|---|
| 472 | absurls = req.args.get('format') == 'rss' # Kludge |
|---|
| 473 | for (t,id,author,type,summary),field_changes,comment in updates: |
|---|
| 474 | if absurls: |
|---|
| 475 | href = self.env.abs_href.ticket(id) |
|---|
| 476 | else: |
|---|
| 477 | href = self.env.href.ticket(id) |
|---|
| 478 | title = util.Markup('Ticket <em title="%s">#%s</em> (%s) ' |
|---|
| 479 | 'updated by %s', summary, id, type, author) |
|---|
| 480 | message = util.Markup() |
|---|
| 481 | if len(field_changes) > 0: |
|---|
| 482 | message = util.Markup(', '.join(field_changes) + \ |
|---|
| 483 | ' changed.<br />') |
|---|
| 484 | message += wiki_to_oneliner(comment, self.env, db, |
|---|
| 485 | shorten=True, absurls=absurls) |
|---|
| 486 | yield 'editedticket', href, title, t, author, message |
|---|