Changeset 10687
- Timestamp:
- May 4, 2011, 12:02:15 AM (13 years ago)
- Location:
- trunk
- Files:
-
- 10 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/trac/htdocs/css/ticket.css
r10664 r10687 117 117 #trac-comment-editor .wikitoolbar { margin-left: -1px } 118 118 #trac-add-comment :link, #trac-add-comment :visited { color: #b00 } 119 .trac-new { border-left: 0.31em solid #c0f0c0; padding-left: 0.31em; } 119 120 #changelog h3, #ticketchange h3 { 120 121 border-bottom: 1px solid #d7d7d7; -
trunk/trac/htdocs/js/threaded_comments.js
r10510 r10687 3 3 var toggle = $('#trac-threaded-toggle'); 4 4 toggle.click(function() { 5 if ($(this).checked()) {6 if (!comments)7 5 $(this).toggleClass('checked'); 6 if ($(this).hasClass('checked')) { 7 comments = $("div.change"); 8 8 comments.each(function() { 9 9 var children = $("a.follow-up", this).map(function() { -
trunk/trac/ticket/model.py
r10681 r10687 256 256 257 257 def save_changes(self, author=None, comment=None, when=None, db=None, 258 cnum='' ):258 cnum='', replyto=None): 259 259 """ 260 260 Store ticket changes in the database. The ticket must already exist in … … 264 264 :since 0.13: the `db` parameter is no longer needed and will be removed 265 265 in version 0.14 266 :since 0.13: the `cnum` parameter is deprecated, and threading should 267 be controlled with the `replyto` argument 266 268 """ 267 269 assert self.exists, "Cannot update a new ticket" … … 297 299 298 300 with self.env.db_transaction as db: 301 db("UPDATE ticket SET changetime=%s WHERE id=%s", 302 (when_ts, self.id)) 303 299 304 # find cnum if it isn't provided 300 comment_num = cnum 301 if not comment_num: 305 if not cnum: 302 306 num = 0 303 307 for ts, old in db(""" … … 315 319 except ValueError: 316 320 num += 1 317 comment_num = str(num + 1) 321 cnum = str(num + 1) 322 if replyto: 323 cnum = '%s.%s' % (replyto, cnum) 318 324 319 325 # store fields … … 345 351 (ticket,time,author,field,oldvalue,newvalue) 346 352 VALUES (%s,%s,%s,'comment',%s,%s) 347 """, (self.id, when_ts, author, comment_num, comment)) 348 349 db("UPDATE ticket SET changetime=%s WHERE id=%s", 350 (when_ts, self.id)) 353 """, (self.id, when_ts, author, cnum, comment)) 351 354 352 355 old_values = self._old … … 356 359 for listener in TicketSystem(self.env).change_listeners: 357 360 listener.ticket_changed(self, comment, author, old_values) 358 return True361 return int(cnum.rsplit('.', 1)[-1]) 359 362 360 363 def get_changelog(self, when=None, db=None): … … 421 424 listener.ticket_deleted(self) 422 425 423 def get_change(self, cnum, db=None): 424 """Return a ticket change by its number. 425 426 :since 0.13: the `db` parameter is no longer needed and will be removed 427 in version 0.14 428 """ 429 row = self._find_change(cnum) 430 if row: 431 ts, author, comment = row 432 fields = {} 433 change = {'date': from_utimestamp(ts), 434 'author': author, 'fields': fields} 435 for field, author, old, new in self.env.db_query(""" 436 SELECT field, author, oldvalue, newvalue 437 FROM ticket_change WHERE ticket=%s AND time=%s 438 """, (self.id, ts)): 439 fields[field] = {'author': author, 'old': old, 'new': new} 426 def get_change(self, cnum=None, cdate=None, db=None): 427 """Return a ticket change by its number or date. 428 429 :since 0.13: the `db` parameter is no longer needed and will be removed 430 in version 0.14 431 """ 432 if cdate is None: 433 row = self._find_change(cnum) 434 if not row: 435 return 436 cdate = from_utimestamp(row[0]) 437 ts = to_utimestamp(cdate) 438 fields = {} 439 change = {'date': cdate, 'fields': fields} 440 for field, author, old, new in self.env.db_query(""" 441 SELECT field, author, oldvalue, newvalue 442 FROM ticket_change WHERE ticket=%s AND time=%s 443 """, (self.id, ts)): 444 fields[field] = {'author': author, 'old': old, 'new': new} 445 if field == 'comment': 446 change['author'] = author 447 elif not field.startswith('_'): 448 change.setdefault('author', author) 449 if fields: 440 450 return change 441 451 442 def delete_change(self, cnum): 443 """Delete a ticket change.""" 444 row = self._find_change(cnum) 445 if not row: 446 return 447 ts = row[0] 452 def delete_change(self, cnum=None, cdate=None): 453 """Delete a ticket change identified by its number or date.""" 454 if cdate is None: 455 row = self._find_change(cnum) 456 if not row: 457 return 458 cdate = from_utimestamp(row[0]) 459 ts = to_utimestamp(cdate) 448 460 with self.env.db_transaction as db: 449 461 # Find modified fields and their previous value … … 546 558 self.values['changetime'] = when 547 559 548 def get_comment_history(self, cnum, db=None): 549 """Retrieve the edit history of comment `cnum`. 550 551 :since 0.13: the `db` parameter is no longer needed and will be removed 552 in version 0.14 553 """ 554 row = self._find_change(cnum) 555 if row: 560 def get_comment_history(self, cnum=None, cdate=None, db=None): 561 """Retrieve the edit history of a comment identified by its number or 562 date. 563 564 :since 0.13: the `db` parameter is no longer needed and will be removed 565 in version 0.14 566 """ 567 if cdate is None: 568 row = self._find_change(cnum) 569 if not row: 570 return 556 571 ts0, author0, last_comment = row 557 with self.env.db_query as db: 558 # Get all fields of the form "_comment%d" 559 rows = db("""SELECT field, author, oldvalue, newvalue 560 FROM ticket_change 561 WHERE ticket=%%s AND time=%%s AND field %s 562 """ % db.like(), 563 (self.id, ts0, db.like_escape('_comment') + '%')) 564 rows = sorted((int(field[8:]), author, old, new) 565 for field, author, old, new in rows) 566 history = [] 567 for rev, author, comment, ts in rows: 568 history.append((rev, from_utimestamp(long(ts0)), author0, 569 comment)) 570 ts0, author0 = ts, author 571 history.sort() 572 rev = history[-1][0] + 1 if history else 0 572 else: 573 ts0, author0, last_comment = to_utimestamp(cdate), None, None 574 with self.env.db_query as db: 575 # Get last comment and author if not available 576 if last_comment is None: 577 last_comment = '' 578 for author0, last_comment in db(""" 579 SELECT author, newvalue FROM ticket_change 580 WHERE ticket=%s AND time=%s AND field='comment' 581 """, (self.id, ts0)): 582 break 583 if author0 is None: 584 for author0, last_comment in db(""" 585 SELECT author, new FROM ticket_change 586 WHERE ticket=%%s AND time=%%s AND NOT field %s LIMIT 1 587 """ % db.like(), 588 (self.id, ts0, db.like_escape('_') + '%')): 589 break 590 else: 591 return 592 593 # Get all fields of the form "_comment%d" 594 rows = db("""SELECT field, author, oldvalue, newvalue 595 FROM ticket_change 596 WHERE ticket=%%s AND time=%%s AND field %s 597 """ % db.like(), 598 (self.id, ts0, db.like_escape('_comment') + '%')) 599 rows = sorted((int(field[8:]), author, old, new) 600 for field, author, old, new in rows) 601 history = [] 602 for rev, author, comment, ts in rows: 573 603 history.append((rev, from_utimestamp(long(ts0)), author0, 574 last_comment)) 575 return history 604 comment)) 605 ts0, author0 = ts, author 606 history.sort() 607 rev = history[-1][0] + 1 if history else 0 608 history.append((rev, from_utimestamp(long(ts0)), author0, 609 last_comment)) 610 return history 576 611 577 612 def _find_change(self, cnum): -
trunk/trac/ticket/templates/ticket.html
r10638 r10687 40 40 $("#propertyform").autoSubmit({preview: '1'}, function(data, reply) { 41 41 var items = $(reply); 42 // Update ticket box 42 43 $("#ticket").replaceWith(items.filter('#ticket')); 43 var changes = $("#ticketchange-content").html(items.filter('ul.changes, div.comment')); 44 var show_preview = changes.children().length != 0; 44 // Unthread and update changelog 45 var threaded_toggle = $('#trac-threaded-toggle'); 46 if (threaded_toggle.checked()) 47 threaded_toggle.click(); 48 $("#changelog").replaceWith(items.filter("#changelog")); 49 // Show warning 50 var new_changes = $("#changelog .trac-new"); 51 $("#trac-edit-warning").toggle(new_changes.length != 0); 52 if (new_changes.length != 0) 53 $("#changelog").parent().show().removeClass("collapsed"); 54 // Update view time 55 $("#propertyform input[name='view_time']").replaceWith(items.filter("input[name='view_time']")); 56 // Update preview 57 var preview = $("#ticketchange").html(items.filter('#preview').children()); 58 var show_preview = preview.children().length != 0; 45 59 $("#ticketchange").toggle(show_preview); 60 // Collapse property form if comment editor has focus 46 61 if (show_preview && comment_focused) 47 62 $("#modify").parent().addClass("collapsed"); … … 53 68 /*]]>*/ 54 69 <py:if test="preview_mode"> 55 $("#changelog").parent().toggleClass("collapsed");56 70 $("#attachments").toggleClass("collapsed"); 57 71 $("#trac-add-comment").scrollToTop(); … … 118 132 </py:if> 119 133 120 <div py:if="ticket.exists and changes">134 <div style="${'display: none' if not changes else None}"> 121 135 <form id="trac-threaded-form" method="get" action="" style="display: none"> 122 136 <div> … … 129 143 130 144 <div id="changelog"> 131 <py:for each="change in changes" 132 py:with="can_edit_comment = has_edit_comment or (authname and authname != 'anonymous' 133 and authname == change.author); 134 show_editor = can_edit_comment and str(change.cnum) == cnum_edit; 135 show_history = str(change.cnum) == cnum_hist; 136 max_version = max(change.comment_history); 137 comment_version = int(cversion or 0) if show_history else max_version; 138 show_buttons = not show_editor and comment_version == max_version"> 139 <div class="change${not show_buttons and ' trac-nobuttons' or ''}" 140 id="${'trac-change-%d' % change.cnum if 'cnum' in change else None}"> 141 <h3 class="change"> 142 <span class="threading" py:if="'cnum' in change" 143 py:with="change_replies = replies.get(str(change.cnum), [])"> 144 <span id="comment:$change.cnum" class="cnum">${commentref('comment:', change.cnum)}</span> 145 <py:if test="change_replies or 'replyto' in change"> 146 <py:if test="'replyto' in change"> 147 in reply to: ${commentref('↑ ', change.replyto)} 148 <py:if test="change_replies">; </py:if> 149 </py:if> 150 <py:if test="change_replies"> 151 <i18n:choose numeral="len(change_replies)"> 152 <span i18n:singular="">follow-up:</span> 153 <span i18n:plural="">follow-ups:</span> 154 </i18n:choose> 155 <py:for each="reply in change_replies"> 156 ${commentref('↓ ', reply, 'follow-up')} 157 </py:for></py:if> 158 </py:if> 159 </span> 160 <i18n:msg params="date, author">Changed ${pretty_dateinfo(change.date)} by ${authorinfo(change.author)}</i18n:msg> 161 </h3> 162 <py:if test="show_buttons"> 163 <form py:if="'cnum' in change and can_edit_comment" method="get" action="#comment:${change.cnum}"> 164 <div class="inlinebuttons"> 165 <input type="hidden" name="cnum_edit" value="${change.cnum}"/> 166 <input type="submit" value="${_('Edit')}" title="${_('Edit comment %(cnum)s', cnum=change.cnum)}"/> 167 </div> 168 </form> 169 <form py:if="'cnum' in change and can_append" id="reply-to-comment-${change.cnum}" 170 method="get" action="#comment"> 171 <div class="inlinebuttons"> 172 <input type="hidden" name="replyto" value="${change.cnum}"/> 173 <input type="submit" value="${_('Reply')}" title="${_('Reply to comment %(cnum)s', cnum=change.cnum)}"/> 174 </div> 175 </form> 176 </py:if> 145 <py:for each="change in changes"> 146 <div class="change${' trac-new' if change.date > start_time and 'attachment' not in change.fields else None}" 147 id="${'trac-change-%d-%d' % (change.cnum, to_utimestamp(change.date)) if 'cnum' in change else None}"> 177 148 <xi:include href="ticket_change.html"/> 178 <div py:if="not show_editor and len(change.comment_history) > 1" py:choose=""179 class="trac-lastedit ${'trac-shade' if comment_version != max_version else None}">180 <i18n:msg params="version, date, author" py:when="comment_version != max_version">181 Version ${comment_version}, edited ${pretty_dateinfo(change.comment_history[comment_version].date)}182 by ${authorinfo(change.comment_history[comment_version].author)}183 </i18n:msg>184 <i18n:msg params="date, author" py:otherwise="">185 Last edited ${pretty_dateinfo(change.comment_history[comment_version].date)}186 by ${authorinfo(change.comment_history[comment_version].author)}187 </i18n:msg>188 <py:if test="comment_version > 0">189 (<a href="${href.ticket(ticket.id, cnum_hist=change.cnum, cversion=comment_version - 1)190 }#comment:${change.cnum}">previous</a>)191 </py:if>192 <py:if test="comment_version < max_version">193 (<a href="${href.ticket(ticket.id, cnum_hist=change.cnum, cversion=comment_version + 1)194 }#comment:${change.cnum}">next</a>)195 </py:if>196 <py:if test="comment_version > 0">197 (<a href="${href.ticket(ticket.id, action='comment-diff', cnum=change.cnum,198 version=comment_version)}">diff</a>)199 </py:if>200 </div>201 149 </div> 202 150 </py:for> … … 210 158 else href.newticket() + '#ticket'}"> 211 159 <!--! Add comment --> 212 <div py:if="ticket.exists and can_append" class="field">160 <div py:if="ticket.exists and can_append" id="trac-add-comment" class="field"> 213 161 <div class="trac-nav"> 214 162 <a href="#content" title="View ticket fields and description">View</a> ↑ 215 163 </div> 216 <h2 id="trac-add-comment">164 <h2> 217 165 <a id="edit" onfocus="$('#comment').get(0).focus()">Add a comment</a> 218 166 </h2> 167 <div id="trac-edit-warning" class="warning system-message" 168 style="${'display: none' if start_time == ticket['changetime'] else None}"> 169 This ticket has been modified since you started editing. You should review the 170 <em class="trac-new">other modifications</em> which have been appended above. 171 You can nevertheless proceed and submit your changes if you wish so. 172 </div> 219 173 <!--! Comment field --> 220 174 <fieldset class="iefix"> … … 226 180 ${comment}</textarea> 227 181 </fieldset> 228 <div py:if="preview_mode and chrome.warnings" i18n:msg="" class="warning system-message">229 Warnings are shown at the <a href="#warning">top of the page</a>. The ticket validation230 may have failed.231 </div>232 182 </div> 233 183 … … 364 314 </div> 365 315 316 <!--! Preview of ticket changes --> 317 <div py:if="ticket.exists and can_append" id="ticketchange" class="ticketdraft" 318 style="${'display: none' if not (change_preview.fields or change_preview.comment) 319 or cnum_edit is not None else None}"> 320 <xi:include href="ticket_change.html" py:with="change = change_preview"/> 321 </div> 322 366 323 <!--! Author or Reporter --> 367 324 <div py:if="authname == 'anonymous'" class="field"> … … 386 343 </div> 387 344 388 <!--! Preview of ticket changes -->389 <div py:if="ticket.exists and can_append" id="ticketchange" class="ticketdraft"390 style="${'display: none' if not (change_preview.fields or change_preview.comment)391 or cnum_edit is not None else None}">392 <h3 class="change" id="${'comment:%d' % change_preview.cnum if 'cnum' in change_preview else None}">393 <span class="threading" py:if="'replyto' in change_preview">394 in reply to: ${commentref('↑ ', change_preview.replyto)}395 </span>396 <i18n:msg params="author">Changed by ${authorinfo(change_preview.author)}</i18n:msg>397 </h3>398 <div id="ticketchange-content"><xi:include href="ticket_change.html" py:with="change = change_preview"/></div>399 </div>400 401 345 <!--! Attachment on creation checkbox --> 402 346 <p py:if="not ticket.exists and 'ATTACHMENT_CREATE' in perm(ticket.resource.child('attachment'))"> … … 412 356 <div class="buttons"> 413 357 <py:if test="ticket.exists"> 414 <input type="hidden" name="ts" value="${timestamp}" /> 358 <input type="hidden" name="start_time" value="${to_utimestamp(start_time)}" /> 359 <input type="hidden" name="view_time" value="${to_utimestamp(ticket['changetime'])}" /> 415 360 <input type="hidden" name="replyto" value="${replyto}" /> 416 <input type="hidden" name="cnum" value="${cnum}" />417 361 </py:if> 418 362 <input type="submit" name="preview" value="${_('Preview')}" accesskey="r" /> -
trunk/trac/ticket/templates/ticket_change.html
r10634 r10687 4 4 Arguments: 5 5 - change: the change data 6 - show_editor=False: if True, show a comment editor 7 - edited_comment: the current value of the comment edito 8 - cnum_edit: the comment number being edited 6 - hide_buttons=False: hide all buttons (Edit, Reply) 7 - cnum_edit=None: the comment number being edited 8 - edited_comment: the current value of the comment editor 9 - cnum_hist=None: the comment number for which to show a historical content 10 - can_append=False: True if the user is allowed to append to tickets 11 - has_edit_comment=False: True if the user is allowed to edit all comments 9 12 --> 10 13 <html xmlns="http://www.w3.org/1999/xhtml" … … 12 15 xmlns:xi="http://www.w3.org/2001/XInclude" 13 16 xmlns:i18n="http://genshi.edgewall.org/i18n" 14 py:with="show_editor = value_of('show_editor', False)" py:strip=""> 17 py:with="cnum = change.get('cnum'); hide_buttons = value_of('hide_buttons', False); 18 cnum_edit = value_of('cnum_edit'); cnum_hist = value_of('cnum_hist'); 19 can_append = value_of('can_append', False); has_edit_comment = value_of('has_edit_comment', False); 20 can_edit_comment = has_edit_comment or (authname and authname != 'anonymous' 21 and authname == change.author); 22 show_editor = can_edit_comment and str(cnum) == cnum_edit; 23 show_history = str(cnum) == cnum_hist; 24 max_version = max(change.comment_history) if change.comment_history else 0; 25 comment_version = int(cversion or 0) if show_history else max_version; 26 show_buttons = not hide_buttons and not show_editor and comment_version == max_version" 27 py:strip=""> 28 <py:def function="commentref(prefix, cnum, cls=None)"> 29 <a href="#comment:$cnum" class="$cls">$prefix$cnum</a> 30 </py:def> 31 <h3 class="change"> 32 <span class="threading" 33 py:with="change_replies = replies.get(str(cnum), []) if 'cnum' in change else []"> 34 <span py:if="'cnum' in change" id="comment:$cnum" class="cnum">${commentref('comment:', cnum)}</span> 35 <py:if test="'replyto' in change"> 36 in reply to: ${commentref('↑ ', change.replyto)} 37 <py:if test="change_replies">; </py:if> 38 </py:if> 39 <py:if test="change_replies"> 40 <i18n:choose numeral="len(change_replies)"> 41 <span i18n:singular="">follow-up:</span> 42 <span i18n:plural="">follow-ups:</span> 43 </i18n:choose> 44 <py:for each="reply in change_replies"> 45 ${commentref('↓ ', reply, 'follow-up')} 46 </py:for> 47 </py:if> 48 </span> 49 <py:choose> 50 <py:when test="'date' in change"> 51 <i18n:msg params="date, author">Changed ${pretty_dateinfo(change.date)} by ${authorinfo(change.author)}</i18n:msg> 52 </py:when> 53 <py:otherwise> 54 <i18n:msg params="author">Changed by ${authorinfo(change.author)}</i18n:msg> 55 </py:otherwise> 56 </py:choose> 57 </h3> 58 <div py:if="show_buttons" class="trac-ticket-buttons"> 59 <form py:if="'cnum' in change and can_edit_comment" method="get" action="#comment:${cnum}"> 60 <div class="inlinebuttons"> 61 <input type="hidden" name="cnum_edit" value="${cnum}"/> 62 <input type="submit" value="${_('Edit')}" title="${_('Edit comment %(cnum)s', cnum=cnum)}"/> 63 </div> 64 </form> 65 <form py:if="'cnum' in change and can_append" id="reply-to-comment-${cnum}" 66 method="get" action="#comment"> 67 <div class="inlinebuttons"> 68 <input type="hidden" name="replyto" value="${cnum}"/> 69 <input type="submit" value="${_('Reply')}" title="${_('Reply to comment %(cnum)s', cnum=cnum)}"/> 70 </div> 71 </form> 72 </div> 15 73 <ul py:if="change.fields" class="changes"> 16 74 <li py:for="field_name, field in sorted(change.fields.iteritems(), key=lambda item: item[1].label.lower())"> … … 37 95 </ul> 38 96 <form py:if="show_editor" id="trac-comment-editor" method="post" 39 action="${href.ticket(ticket.id) + '#comment:%d' % c hange.cnum}">97 action="${href.ticket(ticket.id) + '#comment:%d' % cnum}"> 40 98 <div> 41 99 <textarea name="edited_comment" class="wikitext trac-resizable" rows="10" cols="78"> 42 100 ${edited_comment if edited_comment is not None else change.comment}</textarea> 43 <input type="hidden" name="cnum_edit" value="${c hange.cnum}"/>101 <input type="hidden" name="cnum_edit" value="${cnum}"/> 44 102 </div> 45 103 <div class="buttons"> 46 104 <input type="submit" name="preview_comment" value="${_('Preview')}" 47 title="${_('Preview changes to comment %(cnum)s', cnum=c hange.cnum)}"/>105 title="${_('Preview changes to comment %(cnum)s', cnum=cnum)}"/> 48 106 <input type="submit" name="edit_comment" value="${_('Submit changes')}" 49 title="${_('Submit changes to comment %(cnum)s', cnum=c hange.cnum)}"/>107 title="${_('Submit changes to comment %(cnum)s', cnum=cnum)}"/> 50 108 <input type="submit" name="cancel_comment" value="${_('Cancel')}" 51 109 title="Cancel comment edit"/> … … 53 111 </form> 54 112 <py:choose> 55 <div py:when="str(c hange.cnum) == cnum_edit"113 <div py:when="str(cnum) == cnum_edit" 56 114 py:with="text = edited_comment if edited_comment is not None else change.comment" 57 115 class="comment searchable ticketdraft" style="${'display: none' if not text else None}" xml:space="preserve"> … … 65 123 </div> 66 124 </py:choose> 125 <div py:if="not show_editor and len(change.comment_history) > 1" py:choose="" 126 class="trac-lastedit ${'trac-shade' if comment_version != max_version else None}"> 127 <i18n:msg params="version, date, author" py:when="comment_version != max_version"> 128 Version ${comment_version}, edited ${pretty_dateinfo(change.comment_history[comment_version].date)} 129 by ${authorinfo(change.comment_history[comment_version].author)} 130 </i18n:msg> 131 <i18n:msg params="date, author" py:otherwise=""> 132 Last edited ${pretty_dateinfo(change.comment_history[comment_version].date)} 133 by ${authorinfo(change.comment_history[comment_version].author)} 134 </i18n:msg> 135 <py:if test="comment_version > 0"> 136 (<a href="${href.ticket(ticket.id, cnum_hist=cnum, cversion=comment_version - 1) 137 }#comment:${cnum}">previous</a>) 138 </py:if> 139 <py:if test="comment_version < max_version"> 140 (<a href="${href.ticket(ticket.id, cnum_hist=cnum, cversion=comment_version + 1) 141 }#comment:${cnum}">next</a>) 142 </py:if> 143 <py:if test="comment_version > 0"> 144 (<a href="${href.ticket(ticket.id, action='comment-diff', cnum=cnum, 145 version=comment_version)}">diff</a>) 146 </py:if> 147 </div> 67 148 </html> -
trunk/trac/ticket/templates/ticket_preview.html
r10637 r10687 1 1 <!--! 2 Render a ticket box preview and a ticket change preview, to be sent as a reply 3 to a ticket change auto-preview. 2 Render data relevant to automatic ticket preview. 4 3 --> 5 4 <html xmlns="http://www.w3.org/1999/xhtml" … … 7 6 xmlns:xi="http://www.w3.org/2001/XInclude" 8 7 xmlns:i18n="http://genshi.edgewall.org/i18n" 8 py:with="can_append = 'TICKET_APPEND' in perm(ticket.resource); 9 has_edit_comment = 'TICKET_EDIT_COMMENT' in perm(ticket.resource);" 9 10 py:strip=""> 10 <xi:include href="ticket_box.html" py:with="can_append = 'TICKET_APPEND' in perm(ticket.resource)"/> 11 <xi:include href="ticket_change.html" py:with="change = change_preview"/> 11 <xi:include href="ticket_box.html"/> 12 <div id="changelog"> 13 <div py:for="change in changes" 14 class="change${' trac-new' if change.date > start_time and 'attachment' not in change.fields else None}" 15 id="${'trac-change-%d-%d' % (change.cnum, to_utimestamp(change.date)) if 'cnum' in change else None}"> 16 <xi:include href="ticket_change.html" py:with="edited_comment = None; cnum_edit = 0"/> 17 </div> 18 </div> 19 <input type="hidden" name="view_time" value="${to_utimestamp(ticket['changetime'])}"/> 20 <div id="preview"><xi:include py:if="change_preview.fields or change_preview.comment" 21 href="ticket_change.html" py:with="change = change_preview"/></div> 12 22 </html> -
trunk/trac/ticket/tests/model.py
r10681 r10687 412 412 413 413 def assertChange(self, ticket, cnum, date, author, **fields): 414 change = ticket.get_change(cnum )414 change = ticket.get_change(cnum=cnum) 415 415 self.assertEqual(dict(date=date, author=author, fields=fields), change) 416 416 … … 568 568 'joe (%d)' % i, 569 569 'Comment 1 (%d)' % i, t[-1]) 570 history = ticket.get_comment_history(1) 570 history = ticket.get_comment_history(cnum=1) 571 self.assertEqual((0, t[0], 'jack', 'Comment 1'), history[0]) 572 for i in range(1, len(history)): 573 self.assertEqual((i, t[i], 'joe (%d)' % i, 574 'Comment 1 (%d)' % i), history[i]) 575 history = ticket.get_comment_history(cdate=self.t1) 571 576 self.assertEqual((0, t[0], 'jack', 'Comment 1'), history[0]) 572 577 for i in range(1, len(history)): … … 603 608 self.assertEqual('a', ticket['keywords']) 604 609 self.assertEqual('change4', ticket['foo']) 605 ticket.delete_change( 4)610 ticket.delete_change(cnum=4) 606 611 self.assertEqual('a, b', ticket['keywords']) 607 612 self.assertEqual('change3', ticket['foo']) 608 self.assertEqual(None, ticket.get_change(4)) 609 self.assertNotEqual(None, ticket.get_change(3)) 613 self.assertEqual(None, ticket.get_change(cnum=4)) 614 self.assertNotEqual(None, ticket.get_change(cnum=3)) 615 self.assertEqual(self.t3, ticket.time_changed) 616 617 def test_delete_last_comment_by_date(self): 618 ticket = Ticket(self.env, self.id) 619 self.assertEqual('a', ticket['keywords']) 620 self.assertEqual('change4', ticket['foo']) 621 ticket.delete_change(cdate=self.t4) 622 self.assertEqual('a, b', ticket['keywords']) 623 self.assertEqual('change3', ticket['foo']) 624 self.assertEqual(None, ticket.get_change(cdate=self.t4)) 625 self.assertNotEqual(None, ticket.get_change(cdate=self.t3)) 610 626 self.assertEqual(self.t3, ticket.time_changed) 611 627 … … 616 632 keywords=dict(author='joe', old='a, b', new='a'), 617 633 foo=dict(author='joe', old='change3', new='change4')) 618 ticket.delete_change(3) 619 self.assertEqual(None, ticket.get_change(3)) 634 ticket.delete_change(cnum=3) 635 self.assertEqual(None, ticket.get_change(cnum=3)) 636 self.assertEqual('a', ticket['keywords']) 637 self.assertChange(ticket, 4, self.t4, 'joe', 638 comment=dict(author='joe', old='4', new='Comment 4'), 639 keywords=dict(author='joe', old='a, b, c', new='a'), 640 foo=dict(author='joe', old='change2', new='change4')) 641 self.assertEqual(self.t4, ticket.time_changed) 642 643 def test_delete_mid_comment_by_date(self): 644 ticket = Ticket(self.env, self.id) 645 self.assertChange(ticket, 4, self.t4, 'joe', 646 comment=dict(author='joe', old='4', new='Comment 4'), 647 keywords=dict(author='joe', old='a, b', new='a'), 648 foo=dict(author='joe', old='change3', new='change4')) 649 ticket.delete_change(cdate=self.t3) 650 self.assertEqual(None, ticket.get_change(cdate=self.t3)) 620 651 self.assertEqual('a', ticket['keywords']) 621 652 self.assertChange(ticket, 4, self.t4, 'joe', -
trunk/trac/ticket/web_ui.py
r10644 r10687 494 494 'reassign_owner': req.authname, 495 495 'resolve_resolution': None, 496 ' timestamp': str(ticket['changetime'])})496 'start_time': ticket['changetime']}) 497 497 elif req.method == 'POST': # 'Preview' or 'Submit' 498 498 if 'cancel_comment' in req.args: … … 557 557 558 558 # Preview an existing ticket (after a Preview or a failed Save) 559 start_time = from_utimestamp(long(req.args.get('start_time', 0))) 559 560 data.update({ 560 'action': action, 561 'timestamp': req.args.get('ts'), 561 'action': action, 'start_time': start_time, 562 562 'reassign_owner': (req.args.get('reassign_choice') 563 563 or req.authname), … … 571 571 'resolve_resolution': None, 572 572 # Store a timestamp for detecting "mid air collisions" 573 ' timestamp': str(ticket['changetime'])})573 'start_time': ticket['changetime']}) 574 574 575 575 data.update({'comment': req.args.get('comment'), … … 656 656 657 657 def _prepare_data(self, req, ticket, absurls=False): 658 return {'ticket': ticket, 658 return {'ticket': ticket, 'to_utimestamp': to_utimestamp, 659 659 'context': web_context(req, ticket.resource, 660 660 absurls=absurls), … … 1142 1142 # Mid air collision? 1143 1143 if ticket.exists and (ticket._old or comment or force_collision_check): 1144 if req.args.get('ts') != str(ticket['changetime']): 1144 changetime = ticket['changetime'] 1145 if req.args.get('view_time') != str(to_utimestamp(changetime)): 1145 1146 add_warning(req, _("Sorry, can not save your changes. " 1146 1147 "This ticket has been modified by someone else " … … 1188 1189 # Validate comment numbering 1189 1190 try: 1190 # comment index must be a number1191 int(req.args.get('cnum') or 0)1192 1191 # replyto must be 'description' or a number 1193 1192 replyto = req.args.get('replyto') … … 1240 1239 1241 1240 def _do_save(self, req, ticket, action): 1242 cnum = req.args.get('cnum')1243 replyto = req.args.get('replyto')1244 internal_cnum = cnum1245 if cnum and replyto: # record parent.child relationship1246 internal_cnum = '%s.%s' % (replyto, cnum)1247 1248 1241 # Save the action controllers we need to call side-effects for before 1249 1242 # we save the changes to the ticket. … … 1254 1247 fragment = '' 1255 1248 now = datetime.now(utc) 1256 if ticket.save_changes(get_reporter_id(req, 'author'), 1257 req.args.get('comment'), when=now, 1258 cnum=internal_cnum): 1259 fragment = '#comment:' + cnum if cnum else '' 1249 cnum = ticket.save_changes(get_reporter_id(req, 'author'), 1250 req.args.get('comment'), when=now, 1251 replyto=req.args.get('replyto')) 1252 if cnum: 1253 fragment = '#comment:%d' % cnum 1260 1254 try: 1261 1255 tn = TicketNotifyEmail(self.env) … … 1550 1544 # Insert change preview 1551 1545 change_preview = { 1552 'date': datetime.now(utc), 1553 'author': author_id, 1554 'fields': field_changes, 1555 'preview': True, 1546 'author': author_id, 'fields': field_changes, 'preview': True, 1556 1547 'comment': req.args.get('comment', data.get('comment')), 1548 'comment_history': {}, 1557 1549 } 1558 1550 replyto = req.args.get('replyto') … … 1576 1568 data.update({ 1577 1569 'context': context, 1578 'fields': fields, 'changes': changes, 1579 'replies': replies, 'cnum': cnum + 1, 1570 'fields': fields, 'changes': changes, 'replies': replies, 1580 1571 'attachments': AttachmentModule(self.env).attachment_data(context), 1581 'action_controls': action_controls, 1582 'action': selected_action, 1572 'action_controls': action_controls, 'action': selected_action, 1583 1573 'change_preview': change_preview, 1584 1574 }) -
trunk/tracopt/ticket/deleter.py
r10315 r10687 20 20 from trac.ticket.web_ui import TicketModule 21 21 from trac.util import get_reporter_id 22 from trac.util.datefmt import from_utimestamp 22 23 from trac.util.translation import _ 23 24 from trac.web.api import IRequestFilter, IRequestHandler, ITemplateStreamFilter … … 55 56 56 57 def filter_stream(self, req, method, filename, stream, data): 57 if filename != 'ticket.html':58 if filename not in ('ticket.html', 'ticket_preview.html'): 58 59 return stream 59 60 ticket = data.get('ticket') … … 74 75 def delete_comment(): 75 76 for event in buffer: 76 cnum = event[1][1].get('id')[12:]77 cnum, cdate = event[1][1].get('id')[12:].split('-', 1) 77 78 return tag.form( 78 79 tag.div( … … 80 81 value='delete-comment'), 81 82 tag.input(type='hidden', name='cnum', value=cnum), 83 tag.input(type='hidden', name='cdate', value=cdate), 82 84 tag.input(type='submit', value=_('Delete'), 83 85 title=_('Delete comment %(num)s', … … 90 92 '/h3[@id="comment:description"]') \ 91 93 .after(delete_ticket).end() \ 92 .select('//div[ @class="change"]/@id') \94 .select('//div[starts-with(@class, "change")]/@id') \ 93 95 .copy(buffer).end() \ 94 .select('//div[@class="change" and @id]/h3[@class="change"]') \ 95 .after(delete_comment) 96 .select('//div[starts-with(@class, "change") and @id]' 97 '/div[@class="trac-ticket-buttons"]') \ 98 .prepend(delete_comment) 96 99 97 100 # IRequestFilter methods … … 119 122 ticket = Ticket(self.env, id) 120 123 action = req.args['action'] 124 cnum = req.args.get('cnum') 121 125 if req.method == 'POST': 122 126 if 'cancel' in req.args: 123 127 href = req.href.ticket(id) 124 128 if action == 'delete-comment': 125 href += '#comment:%s' % req.args.get('cnum')129 href += '#comment:%s' % cnum 126 130 req.redirect(href) 127 131 … … 133 137 134 138 elif action == 'delete-comment': 135 c num = int(req.args.get('cnum'))136 ticket.delete_change(c num)139 cdate = from_utimestamp(long(req.args.get('cdate'))) 140 ticket.delete_change(cdate=cdate) 137 141 add_notice(req, _('The ticket comment %(num)s on ticket ' 138 142 '#%(id)s has been deleted.', … … 144 148 tm._insert_ticket_data(req, ticket, data, 145 149 get_reporter_id(req, 'author'), {}) 146 data.update(action=action, del_cnum=None)150 data.update(action=action, cdate=None) 147 151 148 152 if action == 'delete-comment': 149 cnum = int(req.args.get('cnum'))150 data['del_cnum'] = cnum153 data['cdate'] = req.args.get('cdate') 154 cdate = from_utimestamp(long(data['cdate'])) 151 155 for change in data['changes']: 152 if change.get(' cnum') == cnum:156 if change.get('date') == cdate: 153 157 data['change'] = change 158 data['cnum'] = change.get('cnum') 154 159 break 155 160 else: -
trunk/tracopt/ticket/templates/ticket_delete.html
r10629 r10687 10 10 <title py:choose="action"> 11 11 <py:when test="'delete'"><i18n:msg params="id">Delete Ticket #$ticket.id</i18n:msg></py:when> 12 <py:otherwise><i18n:msg params="num, id">Delete comment $ del_cnum on Ticket #$ticket.id</i18n:msg></py:otherwise>12 <py:otherwise><i18n:msg params="num, id">Delete comment $cnum on Ticket #$ticket.id</i18n:msg></py:otherwise> 13 13 </title> 14 14 </head> … … 47 47 48 48 <py:otherwise> 49 <h1 i18n:msg="num, id">Delete comment $ del_cnum on Ticket #$ticket.id</h1>49 <h1 i18n:msg="num, id">Delete comment $cnum on Ticket #$ticket.id</h1> 50 50 51 51 <div id="changelog"> 52 52 <div class="change"> 53 <h3 class="change"> 54 <span class="threading" py:if="'cnum' in change" 55 py:with="change_replies = replies.get(str(change.cnum), [])"> 56 <span class="cnum">comment:$change.cnum</span> 57 </span> 58 <i18n:msg params="date, author">Changed ${pretty_dateinfo(change.date)} by ${authorinfo(change.author)}</i18n:msg> 59 </h3> 60 <xi:include href="ticket_change.html"/> 53 <xi:include href="ticket_change.html" py:with="hide_buttons = True"/> 61 54 </div> 62 55 </div> … … 65 58 <div> 66 59 <input type="hidden" name="action" value="delete-comment"/> 67 <input type="hidden" name="cnum" value="$del_cnum"/> 60 <input type="hidden" name="cnum" value="$cnum"/> 61 <input type="hidden" name="cdate" value="$cdate"/> 68 62 <p><strong>Are you sure you want to delete this ticket comment?</strong><br/> 69 63 This is an irreversible operation.</p>
Note:
See TracChangeset
for help on using the changeset viewer.