Ticket #2703: comment_threading-r3361.2.patch
| File comment_threading-r3361.2.patch, 18.6 KB (added by cboos, 2 years ago) |
|---|
-
htdocs/css/trac.css
42 42 color: inherit; 43 43 } 44 44 45 /* Heading anchors */ 46 .anchor:link, .anchor:visited { 47 border: none; 48 color: #d7d7d7; 49 font-size: .8em; 50 vertical-align: text-top; 51 } 52 * > .anchor:link, * > .anchor:visited { 53 visibility: hidden; 54 } 55 h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, 56 h4:hover .anchor, h5:hover .anchor, h6:hover .anchor { 57 visibility: visible; 58 } 59 45 60 @media screen { 46 61 a.ext-link .icon { 47 62 background: url(../extlink.gif) left center no-repeat; … … 277 292 } 278 293 279 294 blockquote.citation { 280 margin: 0;295 margin: -0.6em 0; 281 296 border-style: solid; 282 297 border-width: 0 0 0 2px; 298 padding-left: .5em; 283 299 border-color: #b44; 284 padding-left: .5em;285 300 } 301 .citation blockquote.citation { border-color: #4b4; } 302 .citation .citation blockquote.citation { border-color: #44b; } 303 .citation .citation .citation blockquote.citation { border-color: #c55; } 286 304 287 305 table.wiki { 288 306 border: 2px solid #ccc; -
htdocs/css/ticket.css
46 46 font-size: 100%; 47 47 font-weight: normal; 48 48 } 49 .inlinebuttons input { 50 float: right; 51 font-size: 70%; 52 border-width: 1px; 53 margin: 0 .5em .1em 1.5em; 54 padding: 0.1em; 55 background-color: #f6f6f6; 56 } 57 .inlinebuttons > input { /* rule ignored by IE */ 58 visibility: hidden; 59 } 60 div.change:hover .inlinebuttons input { 61 visibility: visible; 62 } 63 49 64 #changelog .changes { list-style: square; margin-left: 2em; padding: 0 } 50 65 #changelog .comment { margin-left: 2em } 51 66 -
htdocs/css/wiki.css
22 22 #overview .ipnr { color: #999; font-size: 80% } 23 23 #overview .comment { padding: 1em 0 0 } 24 24 25 /* Heading anchors */26 .anchor:link, .anchor:visited {27 border: none;28 color: #d7d7d7;29 font-size: .8em;30 vertical-align: text-top;31 visibility: hidden;32 }33 h1:hover .anchor, h2:hover .anchor, h3:hover .anchor,34 h4:hover .anchor, h5:hover .anchor, h6:hover .anchor {35 visibility: visible;36 }37 38 25 /* Styles for the page history table 39 26 (extends the styles for "table.listing") */ 40 27 #wikihist td { padding: 0 .5em } -
trac/ticket/api.py
186 186 187 187 def get_link_resolvers(self): 188 188 return [('bug', self._format_link), 189 ('ticket', self._format_link)] 189 ('ticket', self._format_link), 190 ('comment', self._format_comment_link)] 190 191 191 192 def get_wiki_syntax(self): 192 193 yield ( … … 215 216 return html.A(class_='missing ticket', rel='nofollow', 216 217 href=formatter.href.ticket(target))[label] 217 218 219 def _format_comment_link(self, formatter, ns, target, label): 220 type, id, cnum = 'ticket', '1', 0 221 href = None 222 if ':' in target: 223 elts = target.split(':') 224 if len(elts) == 3: 225 type, id, cnum = elts 226 href = formatter.href(type, id) 227 else: 228 # FIXME: the formatter should know which object the text being 229 # formatted belongs to 230 if formatter.req: 231 path_info = formatter.req.path_info.strip('/').split('/', 2) 232 if len(path_info) == 2: 233 type, id = path_info[:2] 234 href = formatter.href(type, id) 235 cnum = target 236 if href: 237 return html.A(label, href="%s#comment:%s" % (href, cnum), 238 title="Comment %s for %s:%s" % (cnum, type, id)) 239 else: 240 return label 241 218 242 # ISearchSource methods 219 243 220 244 def get_search_filters(self, req): -
trac/ticket/web_ui.py
269 269 if comment: 270 270 req.hdf['ticket.comment'] = comment 271 271 # Wiki format a preview of comment 272 req.hdf['ticket.comment_preview'] = wiki_to_html(comment, 273 self.env, 274 req, db) 272 req.hdf['ticket.comment_preview'] = wiki_to_html( 273 comment, self.env, req, db) 275 274 else: 276 275 req.hdf['ticket.reassign_owner'] = req.authname 277 276 # Store a timestamp in order to detect "mid air collisions" … … 512 511 ticket['resolution'] = '' 513 512 514 513 now = int(time.time()) 514 cnum = req.args.get('cnum') 515 replyto = req.args.get('replyto') 516 internal_cnum = cnum 517 if cnum and replyto: # record parent.child relationship 518 internal_cnum = '%s.%s' % (replyto, cnum) 515 519 ticket.save_changes(req.args.get('author', req.authname), 516 req.args.get('comment'), when=now, db=db) 520 req.args.get('comment'), when=now, db=db, 521 cnum=internal_cnum) 517 522 db.commit() 518 523 519 524 try: … … 523 528 self.log.exception("Failure sending notification on change to " 524 529 "ticket #%s: %s" % (ticket.id, e)) 525 530 526 req.redirect(req.href.ticket(ticket.id)) 531 fragment = cnum and '#comment:'+cnum or '' 532 req.redirect(req.href.ticket(ticket.id) + fragment) 527 533 528 534 def _insert_ticket_data(self, req, db, ticket, reporter_id): 529 535 """Insert ticket data into the hdf""" 536 replyto = req.args.get('replyto') 537 req.hdf['title'] = '#%d (%s)' % (ticket.id, ticket['summary']) 530 538 req.hdf['ticket'] = ticket.values 531 req.hdf['ticket.id'] = ticket.id 532 req.hdf['ticket.href'] = req.href.ticket(ticket.id) 539 req.hdf['ticket'] = { 540 'id': ticket.id, 541 'href': req.href.ticket(ticket.id), 542 'replyto': replyto 543 } 533 544 545 # -- Ticket fields 546 534 547 for field in TicketSystem(self.env).get_ticket_fields(): 535 548 if field['type'] in ('radio', 'select'): 536 549 value = ticket.values.get(field['name']) … … 548 561 req.hdf['ticket.fields.' + name] = field 549 562 550 563 req.hdf['ticket.reporter_id'] = reporter_id 551 req.hdf['title'] = '#%d (%s)' % (ticket.id, ticket['summary']) 552 req.hdf['ticket.description.formatted'] = wiki_to_html(ticket['description'], 553 self.env, req, db) 564 req.hdf['ticket.description.formatted'] = wiki_to_html( 565 ticket['description'], self.env, req, db) 554 566 555 567 req.hdf['ticket.opened'] = format_datetime(ticket.time_created) 556 568 req.hdf['ticket.opened_delta'] = pretty_timedelta(ticket.time_created) 557 569 if ticket.time_changed != ticket.time_created: 558 req.hdf['ticket.lastmod'] = format_datetime(ticket.time_changed) 559 req.hdf['ticket.lastmod_delta'] = pretty_timedelta(ticket.time_changed) 570 req.hdf['ticket'] = { 571 'lastmod': format_datetime(ticket.time_changed), 572 'lastmod_delta': pretty_timedelta(ticket.time_changed) 573 } 560 574 575 # -- Ticket Change History 576 561 577 changelog = ticket.get_changelog(db=db) 562 curr_author = None563 curr_date = 0578 autonum = 0 # used for "root" numbers 579 replies = {} 564 580 changes = [] 565 for date, author, field, old, new in changelog: 566 if date != curr_date or author != curr_author: 567 changes.append({ 581 last_uid = current = None 582 for date, author, field, old, new, permanent in changelog: 583 uid = date, author, permanent 584 if uid != last_uid: 585 last_uid = uid 586 current = { 568 587 'date': format_datetime(date), 569 588 'author': author, 570 589 'fields': {} 571 }) 572 curr_date = date 573 curr_author = author 590 } 591 changes.append(current) 592 if permanent: 593 autonum += 1 594 current['cnum'] = autonum 574 595 if field == 'comment': 575 changes[-1]['comment'] = wiki_to_html(new, self.env, req, db) 596 current['comment'] = wiki_to_html(new, self.env, req, db) 597 if permanent: 598 this_num = str(autonum) 599 if old: 600 if '.' in old: # retrieve parent.child relationship 601 parent_num, this_num = old.split('.', 1) 602 current['replyto'] = parent_num 603 replies.setdefault(parent_num, []).append(this_num) 604 else: 605 this_num = old 606 assert this_num == str(autonum) 607 # if we replied to this comment, quote it (with '>' prefix) 608 if replyto == this_num and not 'comment' in req.args: 609 req.hdf['ticket.comment'] = '\n'.join( 610 ['Replying to [comment:%s %s]:' % \ 611 (replyto, author)] + 612 ['> %s' % line for line in new.splitlines()] + ['']) 576 613 elif field == 'description': 577 c hanges[-1]['fields'][field] = ''614 current['fields'][field] = '' 578 615 else: 579 changes[-1]['fields'][field] = {'old': old, 580 'new': new} 581 req.hdf['ticket.changes'] = changes 616 current['fields'][field] = {'old': old, 'new': new} 617 req.hdf['ticket'] = { 618 'changes': changes, 619 'replies': replies, 620 'cnum': autonum + 1 621 } 582 622 583 # List attached files 623 # -- Ticket Attachments 624 584 625 req.hdf['ticket.attachments'] = attachments_to_hdf(self.env, req, db, 585 626 'ticket', ticket.id) 586 627 if req.perm.has_permission('TICKET_APPEND'): -
trac/ticket/tests/model.py
196 196 ticket['component'] = 'bar' 197 197 ticket['milestone'] = 'foo' 198 198 ticket.save_changes('jane', 'Testing', when=42) 199 for t, author, field, old, new in ticket.get_changelog():200 self.assertEqual((42, 'jane' ), (t, author))199 for t, author, field, old, new, permanent in ticket.get_changelog(): 200 self.assertEqual((42, 'jane', True), (t, author, permanent)) 201 201 if field == 'component': 202 202 self.assertEqual(('foo', 'bar'), (old, new)) 203 203 elif field == 'milestone': … … 214 214 ticket['component'] = 'bar' 215 215 ticket['component'] = 'foo' 216 216 ticket.save_changes('jane', 'Testing', when=42) 217 for t, author, field, old, new in ticket.get_changelog():218 self.assertEqual((42, 'jane' ), (t, author))217 for t, author, field, old, new, permanent in ticket.get_changelog(): 218 self.assertEqual((42, 'jane', True), (t, author, permanent)) 219 219 if field == 'comment': 220 220 self.assertEqual(('', 'Testing'), (old, new)) 221 221 else: -
trac/ticket/model.py
180 180 self._old = {} 181 181 return self.id 182 182 183 def save_changes(self, author, comment, when=0, db=None ):183 def save_changes(self, author, comment, when=0, db=None, cnum=''): 184 184 """ 185 185 Store ticket changes in the database. The ticket must already exist in 186 186 the database. … … 244 244 if comment: 245 245 cursor.execute("INSERT INTO ticket_change " 246 246 "(ticket,time,author,field,oldvalue,newvalue) " 247 "VALUES (%s,%s,%s,'comment', '',%s)",248 (self.id, when, author, c omment))247 "VALUES (%s,%s,%s,'comment',%s,%s)", 248 (self.id, when, author, cnum, comment)) 249 249 250 250 cursor.execute("UPDATE ticket SET changetime=%s WHERE id=%s", 251 251 (when, self.id)) … … 265 265 db = self._get_db(db) 266 266 cursor = db.cursor() 267 267 if when: 268 cursor.execute("SELECT time,author,field,oldvalue,newvalue "268 cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 " 269 269 "FROM ticket_change WHERE ticket=%s AND time=%s " 270 270 "UNION " 271 "SELECT time,author,'attachment',null,filename "271 "SELECT time,author,'attachment',null,filename,0 " 272 272 "FROM attachment WHERE id=%s AND time=%s " 273 273 "UNION " 274 "SELECT time,author,'comment',null,description "274 "SELECT time,author,'comment',null,description,0 " 275 275 "FROM attachment WHERE id=%s AND time=%s " 276 276 "ORDER BY time", 277 277 (self.id, when, str(self.id), when, self.id, when)) 278 278 else: 279 cursor.execute("SELECT time,author,field,oldvalue,newvalue "279 cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 " 280 280 "FROM ticket_change WHERE ticket=%s " 281 281 "UNION " 282 "SELECT time,author,'attachment',null,filename "282 "SELECT time,author,'attachment',null,filename,0 " 283 283 "FROM attachment WHERE id=%s " 284 284 "UNION " 285 "SELECT time,author,'comment',null,description "285 "SELECT time,author,'comment',null,description,0 " 286 286 "FROM attachment WHERE id=%s " 287 287 "ORDER BY time", (self.id, str(self.id), self.id)) 288 288 log = [] 289 for t, author, field, oldvalue, newvalue in cursor: 290 log.append((int(t), author, field, oldvalue or '', newvalue or '')) 289 for t, author, field, oldvalue, newvalue, permanent in cursor: 290 log.append((int(t), author, field, oldvalue or '', newvalue or '', 291 permanent)) 291 292 return log 292 293 293 294 def delete(self, db=None): -
trac/ticket/notification.py
75 75 changes = '' 76 76 if not self.newticket and modtime: # Ticket change 77 77 changelog = ticket.get_changelog(modtime) 78 for date, author, field, old, new in changelog:78 for date, author, field, old, new, permanent in changelog: 79 79 self.hdf.set_unescaped('ticket.change.author', author) 80 80 pfx = 'ticket.change.%s' % field 81 81 newv = '' -
templates/ticket.cs
80 80 <?cs call:list_of_attachments(ticket.attachments, ticket.attach_href) ?> 81 81 <?cs /if ?> 82 82 83 <?cs def:commentref(prefix, cnum) ?> 84 <a href="#comment:<?cs var:cnum ?>"><small><?cs var:prefix ?><?cs var:cnum ?></small></a> 85 <?cs /def ?> 86 83 87 <?cs if:len(ticket.changes) ?><h2>Change History</h2> 84 88 <div id="changelog"><?cs 85 89 each:change = ticket.changes ?> 86 <h3 id="change_<?cs var:name(change) ?>" class="change"><?cs 87 var:change.date ?>: Modified by <?cs var:change.author ?></h3><?cs 90 <div class="change"> 91 <h3 <?cs if:change.cnum ?>id="comment:<?cs var:change.cnum ?>"<?cs /if ?>><?cs 92 var:change.date ?> changed by <?cs var:change.author ?> <?cs 93 if:change.cnum ?><?cs 94 set:nreplies = len(ticket.replies[change.cnum]) ?><?cs 95 if:nreplies || change.replyto ?><span class="threading"> — <?cs 96 if:change.replyto ?>in reply to: <?cs 97 call:commentref('↑', change.replyto) ?><?cs if nreplies ?> – <?cs /if ?><?cs 98 /if ?><?cs 99 if nreplies ?><?cs 100 call:plural('follow-up', nreplies) ?>: <?cs 101 each:reply = ticket.replies[change.cnum] ?><?cs 102 call:commentref('↓', reply) ?><?cs 103 /each ?><?cs 104 /if ?><?cs 105 /if ?></span> 106 <a href="#comment:<?cs var:change.cnum ?>" class="anchor" 107 title="Permalink to comment:<?cs var:change.cnum ?>">¶</a><?cs 108 /if ?> 109 </h3><?cs 110 if:change.cnum ?> 111 <form method="get" action="<?cs var:ticket.href ?>#comment"><div class="inlinebuttons"> 112 <input type="hidden" name="replyto" value="<?cs var:change.cnum ?>" /> 113 <input type="submit" value="Reply" title="Reply to comment <?cs var:change.cnum ?>" /></div> 114 </form><?cs 115 /if ?><?cs 88 116 if:len(change.fields) ?> 89 117 <ul class="changes"><?cs 90 118 each:field = change.fields ?> … … 100 128 /each ?> 101 129 </ul><?cs 102 130 /if ?> 103 <div class="comment"><?cs var:change.comment ?></div><?cs 104 /each ?></div><?cs 131 <div class="comment"><?cs var:change.comment ?></div> 132 </div><?cs 133 /each ?> 134 </div><?cs 105 135 /if ?> 106 136 107 137 <?cs if:trac.acl.TICKET_CHGPROP || trac.acl.TICKET_APPEND ?> … … 277 307 278 308 <div class="buttons"> 279 309 <input type="hidden" name="ts" value="<?cs var:ticket.ts ?>" /> 310 <input type="hidden" name="replyto" value="<?cs var:ticket.replyto ?>" /> 311 <input type="hidden" name="cnum" value="<?cs var:ticket.cnum ?>" /> 280 312 <input type="submit" name="preview" value="Preview" accesskey="r" /> 281 313 <input type="submit" value="Submit changes" /> 282 314 </div> -
templates/macros.cs
190 190 <input type="submit" value="Attach File" /> 191 191 </div></form><?cs 192 192 /if ?><?cs if:len(attachments) ?></div><?cs /if ?><?cs 193 /def ?><?cs 194 195 def:plural(base, count) ?><?cs 196 var:base ?><?cs if:count != 1 ?>s<?cs /if ?><?cs 193 197 /def ?>
