Ticket #2703: comment_threading-r3361.patch
| File comment_threading-r3361.patch, 19.8 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 visibility: hidden; 52 } 53 h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, 54 h4:hover .anchor, h5:hover .anchor, h6:hover .anchor { 55 visibility: visible; 56 } 57 45 58 @media screen { 46 59 a.ext-link .icon { 47 60 background: url(../extlink.gif) left center no-repeat; … … 277 290 } 278 291 279 292 blockquote.citation { 280 margin: 0;293 margin: -0.6em 0; 281 294 border-style: solid; 282 295 border-width: 0 0 0 2px; 283 border-color: #b44;284 296 padding-left: .5em; 285 297 } 286 298 299 .depth1 { border-color: #b44; } 300 .depth2 { border-color: #4b4; } 301 .depth3 { border-color: #44b; } 302 287 303 table.wiki { 288 304 border: 2px solid #ccc; 289 305 border-collapse: collapse; -
htdocs/css/ticket.css
46 46 font-size: 100%; 47 47 font-weight: normal; 48 48 } 49 .inlinebuttons input { 50 float: right; 51 position: relative; 52 font-size: 70%; 53 border-width: 1px; 54 margin: 0 .5em .1em 1.5em; 55 padding: 0.1em; 56 background-color: #f6f6f6; 57 visibility: hidden; 58 } 59 div.change:hover .inlinebuttons input { 60 visibility: visible; 61 } 62 49 63 #changelog .changes { list-style: square; margin-left: 2em; padding: 0 } 50 64 #changelog .comment { margin-left: 2em } 51 65 -
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 = '' -
trac/wiki/tests/wiki-tests.txt
1096 1096 >> start 2nd level 1097 1097 > first level 1098 1098 ------------------------------ 1099 <blockquote class="citation ">1099 <blockquote class="citation depth1"> 1100 1100 <p> 1101 1101 This is the quoted text 1102 1102 </p> 1103 <blockquote class="citation ">1103 <blockquote class="citation depth2"> 1104 1104 <p> 1105 1105 a nested quote 1106 1106 </p> … … 1109 1109 <p> 1110 1110 A comment on the above 1111 1111 </p> 1112 <blockquote class="citation ">1113 <blockquote class="citation ">1112 <blockquote class="citation depth1"> 1113 <blockquote class="citation depth2"> 1114 1114 <p> 1115 1115 start 2nd level 1116 1116 </p> -
trac/wiki/formatter.py
614 614 def open_one_quote(d): 615 615 self._quote_stack.append(d) 616 616 self._set_tab(d) 617 class_attr = citation and ' class="citation"' or '' 617 class_attr = '' 618 if citation: 619 class_attr = ' class="citation depth%d"' % ((d-1) % 3 + 1) 618 620 self.out.write('<blockquote%s>' % class_attr + os.linesep) 619 621 if citation: 620 622 for d in range(quote_depth+1, depth+1): -
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 ?></span> 105 <a href="#comment:<?cs var:change.cnum ?>" class="anchor" 106 title="Permalink to comment:<?cs var:change.cnum ?>">¶</a><?cs 107 /if ?><?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 ?>
