Edgewall Software

Ticket #7145: 7145-concurrent-editing-2-r10647.patch

File 7145-concurrent-editing-2-r10647.patch, 25.0 KB (added by rblank, 14 months ago)

Improved concurrent editing.

  • trac/htdocs/css/ticket.css

    diff --git a/trac/htdocs/css/ticket.css b/trac/htdocs/css/ticket.css
    a b div.comment ol { list-style: decimal } 
    110110} 
    111111#trac-comment-editor .wikitoolbar { margin-left: -1px } 
    112112#trac-add-comment :link, #trac-add-comment :visited { color: #b00 } 
     113#changelog .trac-new { border-left: 0.3em solid #c0f0c0; padding-left: 0.3em; } 
    113114#changelog h3, #ticketchange h3 { 
    114115 border-bottom: 1px solid #d7d7d7; 
    115116 color: #999; 
    form .field .wikitoolbar { margin-left:  
    137138form .field div.trac-resizable { width: 100% } 
    138139 
    139140#propertyform { margin-bottom: 2em; } 
     141#trac-edit-warning .trac-new { background-color: #c0f0c0; } 
    140142#properties { white-space: nowrap; line-height: 160%; padding: .5em } 
    141143#properties table { border-spacing: 0; width: 100%; padding: 0 .5em } 
    142144#properties table th { 
  • trac/htdocs/js/threaded_comments.js

    diff --git a/trac/htdocs/js/threaded_comments.js b/trac/htdocs/js/threaded_comments.js
    a b jQuery(document).ready(function($){ 
    33  var toggle = $('#trac-threaded-toggle'); 
    44  toggle.click(function() { 
    55    if ($(this).checked()) { 
    6       if (!comments) 
    7         comments = $("div.change"); 
     6      comments = $("div.change"); 
    87      comments.each(function() { 
    98        var children = $("a.follow-up", this).map(function() { 
    109          var cnum = $(this).attr("href").replace('#comment:', ''); 
  • trac/ticket/model.py

    diff --git a/trac/ticket/model.py b/trac/ticket/model.py
    a b class Ticket(object): 
    255255        return self.id 
    256256 
    257257    def save_changes(self, author=None, comment=None, when=None, db=None, 
    258                      cnum=''): 
     258                     cnum='', replyto=None): 
    259259        """ 
    260260        Store ticket changes in the database. The ticket must already exist in 
    261261        the database.  Returns False if there were no changes to save, True 
    class Ticket(object): 
    263263 
    264264        :since 0.13: the `db` parameter is no longer needed and will be removed 
    265265        in version 0.14 
     266        :since 0.13: the `cnum` parameter is deprecated, and threading should 
     267        be controled with the new `replyto` argument 
    266268        """ 
    267269        assert self.exists, "Cannot update a new ticket" 
    268270 
    class Ticket(object): 
    296298                    pass 
    297299 
    298300        with self.env.db_transaction as db: 
     301            db("UPDATE ticket SET changetime=%s WHERE id=%s", 
     302               (when_ts, self.id)) 
     303             
    299304            # find cnum if it isn't provided 
    300             comment_num = cnum 
    301             if not comment_num: 
     305            if not cnum: 
    302306                num = 0 
    303307                for ts, old in db(""" 
    304308                        SELECT DISTINCT tc1.time, COALESCE(tc2.oldvalue,'') 
    class Ticket(object): 
    314318                        break 
    315319                    except ValueError: 
    316320                        num += 1 
    317                 comment_num = str(num + 1) 
     321                cnum = str(num + 1) 
     322                if replyto: 
     323                    cnum = '%s.%s' % (replyto, cnum) 
    318324 
    319325            # store fields 
    320326            for name in self._old.keys(): 
    class Ticket(object): 
    344350            db("""INSERT INTO ticket_change 
    345351                    (ticket,time,author,field,oldvalue,newvalue) 
    346352                  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)) 
    351354 
    352355        old_values = self._old 
    353356        self._old = {} 
    class Ticket(object): 
    355358 
    356359        for listener in TicketSystem(self.env).change_listeners: 
    357360            listener.ticket_changed(self, comment, author, old_values) 
    358         return True 
     361        return int(cnum.rsplit('.', 1)[-1]) 
    359362 
    360363    def get_changelog(self, when=None, db=None): 
    361364        """Return the changelog as a list of tuples of the form 
  • trac/ticket/templates/ticket.html

    diff --git a/trac/ticket/templates/ticket.html b/trac/ticket/templates/ticket.html
    a b  
    3939                     .blur(function() { comment_focused = false; }); 
    4040        $("#propertyform").autoSubmit({preview: '1'}, function(data, reply) { 
    4141          var items = $(reply); 
     42          // Update ticket box 
    4243          $("#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.attr('checked', false).click().attr('checked', false); 
     48          $("#changelog .trac-new").remove(); 
     49          var new_changes = items.filter("#new-changes").children(); 
     50          $("#changelog").append(new_changes); 
     51          // Show warning 
     52          $("#trac-edit-warning").toggle(new_changes.length != 0); 
     53          // Update view time 
     54          $("#propertyform input[name='view_time']").replaceWith(items.filter("input[name='view_time']")); 
     55          // Update preview 
     56          var preview = $("#ticketchange-content").html(items.filter('#preview').children()); 
     57          var show_preview = preview.children().length != 0; 
    4558          $("#ticketchange").toggle(show_preview); 
     59          // Collapse property form if comment editor has focus 
    4660          if (show_preview && comment_focused) 
    4761            $("#modify").parent().addClass("collapsed"); 
    4862        }); 
     
    5266        }); 
    5367        /*]]>*/ 
    5468        <py:if test="preview_mode"> 
    55         $("#changelog").parent().toggleClass("collapsed"); 
    5669        $("#attachments").toggleClass("collapsed"); 
    5770        $("#trac-add-comment").scrollToTop(); 
    5871        </py:if> 
     
    128141          <h2 class="foldable">Change History</h2> 
    129142 
    130143          <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('&uarr;&nbsp;', 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('&darr;&nbsp;', 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> 
    177                 <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 &lt; 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               </div> 
    202             </py:for> 
     144            <xi:include href="ticket_changes.html"/> 
    203145          </div> 
    204146        </div> 
    205147      </py:if> 
     
    216158          <h2 id="trac-add-comment"> 
    217159            <a id="edit" onfocus="$('#comment').get(0).focus()">Add a comment</a> 
    218160          </h2> 
     161          <div id="trac-edit-warning" class="warning system-message" 
     162               style="${'display: none' if start_time == ticket['changetime'] else None}"> 
     163            This ticket has been modified since you started editing. The 
     164            <span class="trac-new">other modifications</span> are highlighted. 
     165          </div> 
    219166          <!--! Comment field --> 
    220167          <fieldset class="iefix"> 
    221168            <label for="comment" i18n:msg="">You may use 
     
    225172            <textarea id="comment" name="comment" class="wikitext trac-resizable" rows="10" cols="78"> 
    226173${comment}</textarea> 
    227174          </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 validation 
    230             may have failed. 
    231           </div> 
    232175        </div> 
    233176 
    234177        <div> 
     
    411354        </div> 
    412355        <div class="buttons"> 
    413356          <py:if test="ticket.exists"> 
    414             <input type="hidden" name="ts" value="${timestamp}" /> 
     357            <input type="hidden" name="start_time" value="${to_utimestamp(start_time)}" /> 
     358            <input type="hidden" name="view_time" value="${to_utimestamp(ticket['changetime'])}" /> 
    415359            <input type="hidden" name="replyto" value="${replyto}" /> 
    416             <input type="hidden" name="cnum" value="${cnum}" /> 
    417360          </py:if> 
    418361          <input type="submit" name="preview" value="${_('Preview')}" accesskey="r" />&nbsp; 
    419362          <input type="submit" name="submit" value="${_('Submit changes') if ticket.exists else _('Create ticket')}" /> 
  • trac/ticket/templates/ticket_change.html

    diff --git a/trac/ticket/templates/ticket_change.html b/trac/ticket/templates/ticket_change.html
    a b Render a ticket comment. 
    44Arguments: 
    55 - change: the change data 
    66 - show_editor=False: if True, show a comment editor 
    7  - edited_comment: the current value of the comment edito 
     7 - edited_comment: the current value of the comment editor 
    88 - cnum_edit: the comment number being edited 
    99--> 
    1010<html xmlns="http://www.w3.org/1999/xhtml" 
  • new file trac/ticket/templates/ticket_changes.html

    diff --git a/trac/ticket/templates/ticket_changes.html b/trac/ticket/templates/ticket_changes.html
    new file mode 100644
    - +  
     1<!--! 
     2Render a list of ticket changes. 
     3 
     4Arguments: 
     5 - changes: the list of changes 
     6 - edited_comment: the current value of the comment edito 
     7 - cnum_edit: the comment number being edited 
     8 - can_append: True if the user is allowed to append to tickets 
     9 - has_edit_comment: True if the user is allowed to edit comments 
     10--> 
     11<html xmlns="http://www.w3.org/1999/xhtml" 
     12      xmlns:py="http://genshi.edgewall.org/" 
     13      xmlns:xi="http://www.w3.org/2001/XInclude" 
     14      xmlns:i18n="http://genshi.edgewall.org/i18n" 
     15      py:strip=""> 
     16  <py:def function="commentref(prefix, cnum, cls=None)"> 
     17    <a href="#comment:$cnum" class="$cls">$prefix$cnum</a> 
     18  </py:def> 
     19  <py:for each="change in changes" 
     20          py:with="can_edit_comment = has_edit_comment or (authname and authname != 'anonymous' 
     21                                                           and authname == change.author); 
     22                   show_editor = can_edit_comment and str(change.cnum) == cnum_edit; 
     23                   show_history = str(change.cnum) == cnum_hist; 
     24                   max_version = max(change.comment_history); 
     25                   comment_version = int(cversion or 0) if show_history else max_version; 
     26                   show_buttons = not show_editor and comment_version == max_version"> 
     27    <div class="change${' trac-nobuttons' if not show_buttons else None}${ 
     28                        ' trac-new' if change.date > start_time else None}" 
     29         id="${'trac-change-%d' % change.cnum if 'cnum' in change else None}"> 
     30      <h3 class="change"> 
     31        <span class="threading" py:if="'cnum' in change" 
     32              py:with="change_replies = replies.get(str(change.cnum), [])"> 
     33          <span id="comment:$change.cnum" class="cnum">${commentref('comment:', change.cnum)}</span> 
     34          <py:if test="change_replies or 'replyto' in change"> 
     35            <py:if test="'replyto' in change"> 
     36              in reply to: ${commentref('&uarr;&nbsp;', 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('&darr;&nbsp;', reply, 'follow-up')} 
     46              </py:for></py:if> 
     47          </py:if> 
     48        </span> 
     49        <i18n:msg params="date, author">Changed ${pretty_dateinfo(change.date)} by ${authorinfo(change.author)}</i18n:msg> 
     50      </h3> 
     51      <py:if test="show_buttons"> 
     52        <form py:if="'cnum' in change and can_edit_comment" method="get" action="#comment:${change.cnum}"> 
     53          <div class="inlinebuttons"> 
     54            <input type="hidden" name="cnum_edit" value="${change.cnum}"/> 
     55            <input type="submit" value="${_('Edit')}" title="${_('Edit comment %(cnum)s', cnum=change.cnum)}"/> 
     56          </div> 
     57        </form> 
     58        <form py:if="'cnum' in change and can_append" id="reply-to-comment-${change.cnum}" 
     59              method="get" action="#comment"> 
     60          <div class="inlinebuttons"> 
     61            <input type="hidden" name="replyto" value="${change.cnum}"/> 
     62            <input type="submit" value="${_('Reply')}" title="${_('Reply to comment %(cnum)s', cnum=change.cnum)}"/> 
     63          </div> 
     64        </form> 
     65      </py:if> 
     66      <xi:include href="ticket_change.html"/> 
     67      <div py:if="not show_editor and len(change.comment_history) > 1" py:choose="" 
     68           class="trac-lastedit ${'trac-shade' if comment_version != max_version else None}"> 
     69        <i18n:msg params="version, date, author" py:when="comment_version != max_version"> 
     70            Version ${comment_version}, edited ${pretty_dateinfo(change.comment_history[comment_version].date)} 
     71            by ${authorinfo(change.comment_history[comment_version].author)} 
     72        </i18n:msg> 
     73        <i18n:msg params="date, author" py:otherwise=""> 
     74            Last edited ${pretty_dateinfo(change.comment_history[comment_version].date)} 
     75            by ${authorinfo(change.comment_history[comment_version].author)} 
     76        </i18n:msg> 
     77        <py:if test="comment_version > 0"> 
     78          (<a href="${href.ticket(ticket.id, cnum_hist=change.cnum, cversion=comment_version - 1) 
     79                     }#comment:${change.cnum}">previous</a>) 
     80        </py:if> 
     81        <py:if test="comment_version &lt; max_version"> 
     82          (<a href="${href.ticket(ticket.id, cnum_hist=change.cnum, cversion=comment_version + 1) 
     83                     }#comment:${change.cnum}">next</a>) 
     84        </py:if> 
     85        <py:if test="comment_version > 0"> 
     86          (<a href="${href.ticket(ticket.id, action='comment-diff', cnum=change.cnum, 
     87                                  version=comment_version)}">diff</a>) 
     88        </py:if> 
     89      </div> 
     90    </div> 
     91  </py:for> 
     92</html> 
  • trac/ticket/templates/ticket_preview.html

    diff --git a/trac/ticket/templates/ticket_preview.html b/trac/ticket/templates/ticket_preview.html
    a b to a ticket change auto-preview. 
    66      xmlns:py="http://genshi.edgewall.org/" 
    77      xmlns:xi="http://www.w3.org/2001/XInclude" 
    88      xmlns:i18n="http://genshi.edgewall.org/i18n" 
     9      py:with="can_append = 'TICKET_APPEND' in perm(ticket.resource); 
     10               has_edit_comment = 'TICKET_EDIT_COMMENT' in perm(ticket.resource);" 
    911      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"/> 
     12  <xi:include href="ticket_box.html"/> 
     13  <div id="new-changes"> 
     14    <xi:include href="ticket_changes.html" 
     15                py:with="changes = [c for c in changes if c.date > start_time]; 
     16                         edited_comment=None; cnum_edit=0"/> 
     17  </div> 
     18  <input type="hidden" name="view_time" value="${to_utimestamp(ticket['changetime'])}"/> 
     19  <div id="preview"><xi:include href="ticket_change.html" py:with="change = change_preview"/></div> 
    1220</html> 
  • trac/ticket/web_ui.py

    diff --git a/trac/ticket/web_ui.py b/trac/ticket/web_ui.py
    a b class TicketModule(Component): 
    493493            data.update({'action': None, 
    494494                         'reassign_owner': req.authname, 
    495495                         'resolve_resolution': None, 
    496                          'timestamp': str(ticket['changetime'])}) 
     496                         'start_time': ticket['changetime']}) 
    497497        elif req.method == 'POST': # 'Preview' or 'Submit' 
    498498            if 'cancel_comment' in req.args: 
    499499                req.redirect(req.href.ticket(ticket.id)) 
    class TicketModule(Component): 
    556556                req.args['preview'] = True 
    557557 
    558558            # Preview an existing ticket (after a Preview or a failed Save) 
     559            start_time = from_utimestamp(long(req.args.get('start_time', 0))) 
    559560            data.update({ 
    560                 'action': action, 
    561                 'timestamp': req.args.get('ts'), 
     561                'action': action, 'start_time': start_time, 
    562562                'reassign_owner': (req.args.get('reassign_choice')  
    563563                                   or req.authname), 
    564564                'resolve_resolution': req.args.get('resolve_choice'), 
    class TicketModule(Component): 
    570570                         'reassign_owner': req.authname, 
    571571                         'resolve_resolution': None, 
    572572                         # Store a timestamp for detecting "mid air collisions" 
    573                          'timestamp': str(ticket['changetime'])}) 
     573                         'start_time': ticket['changetime']}) 
    574574 
    575575        data.update({'comment': req.args.get('comment'), 
    576576                     'cnum_edit': req.args.get('cnum_edit'), 
    class TicketModule(Component): 
    655655        return 'ticket.html', data, None 
    656656 
    657657    def _prepare_data(self, req, ticket, absurls=False): 
    658         return {'ticket': ticket, 
     658        return {'ticket': ticket, 'to_utimestamp': to_utimestamp, 
    659659                'context': web_context(req, ticket.resource, 
    660660                                                absurls=absurls), 
    661661                'preserve_newlines': self.must_preserve_newlines} 
    class TicketModule(Component): 
    11411141 
    11421142        # Mid air collision? 
    11431143        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)): 
    11451146                add_warning(req, _("Sorry, can not save your changes. " 
    11461147                              "This ticket has been modified by someone else " 
    11471148                              "since you started")) 
    class TicketModule(Component): 
    11871188 
    11881189        # Validate comment numbering 
    11891190        try: 
    1190             # comment index must be a number 
    1191             int(req.args.get('cnum') or 0) 
    11921191            # replyto must be 'description' or a number 
    11931192            replyto = req.args.get('replyto') 
    11941193            if replyto != 'description': 
    class TicketModule(Component): 
    12391238        req.redirect(req.href.ticket(ticket.id)) 
    12401239 
    12411240    def _do_save(self, req, ticket, action): 
    1242         cnum = req.args.get('cnum') 
    1243         replyto = req.args.get('replyto') 
    1244         internal_cnum = cnum 
    1245         if cnum and replyto: # record parent.child relationship 
    1246             internal_cnum = '%s.%s' % (replyto, cnum) 
    1247  
    12481241        # Save the action controllers we need to call side-effects for before 
    12491242        # we save the changes to the ticket. 
    12501243        controllers = list(self._get_action_controllers(req, ticket, action)) 
    class TicketModule(Component): 
    12531246 
    12541247        fragment = '' 
    12551248        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 
    12601254            try: 
    12611255                tn = TicketNotifyEmail(self.env) 
    12621256                tn.notify(ticket, newticket=False, modtime=now) 
    class TicketModule(Component): 
    15751569                                                            ticket[user]) 
    15761570        data.update({ 
    15771571            'context': context, 
    1578             'fields': fields, 'changes': changes, 
    1579             'replies': replies, 'cnum': cnum + 1, 
     1572            'fields': fields, 'changes': changes, 'replies': replies, 
    15801573            'attachments': AttachmentModule(self.env).attachment_data(context), 
    15811574            'action_controls': action_controls, 
    15821575            'action': selected_action, 
  • tracopt/ticket/deleter.py

    diff --git a/tracopt/ticket/deleter.py b/tracopt/ticket/deleter.py
    a b class TicketDeleter(Component): 
    5454    # ITemplateStreamFilter methods 
    5555     
    5656    def filter_stream(self, req, method, filename, stream, data): 
     57        # TODO: Also handle ticket_preview.html 
    5758        if filename != 'ticket.html': 
    5859            return stream 
    5960        ticket = data.get('ticket') 
    class TicketDeleter(Component): 
    7475        def delete_comment(): 
    7576            for event in buffer: 
    7677                cnum = event[1][1].get('id')[12:] 
     78                # FIXME: Identify comment by timestamp instead of cnum 
    7779                return tag.form( 
    7880                    tag.div( 
    7981                        tag.input(type='hidden', name='action',