Edgewall Software

Ticket #2703: comment_threading-r3323.patch

File comment_threading-r3323.patch, 16.8 KB (added by cboos, 3 years ago)

Adding Reply to ticket comments feature on top of r3323, with comment threading within the chronological view (in reply to: ... - follow-ups: ... ... ). To apply, go in your trunk extraction and do patch -p1 < comment_threading-r3323.patch.

  • htdocs/css/ticket.css

    diff -r f8e1ea02978c -r 3081679abd00 htdocs/css/ticket.css
    a b  
    4646 font-size: 100%; 
    4747 font-weight: normal; 
    4848} 
     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} 
    4957#changelog .changes { list-style: square; margin-left: 2em; padding: 0 } 
    5058#changelog .comment { margin-left: 2em } 
    5159 
  • htdocs/css/trac.css

    diff -r f8e1ea02978c -r 3081679abd00 htdocs/css/trac.css
    a b  
    4040h1 :link, h1 :visited ,h2 :link, h2 :visited, h3 :link, h3 :visited, 
    4141h4 :link, h4 :visited, h5 :link, h5 :visited, h6 :link, h6 :visited { 
    4242 color: inherit; 
     43} 
     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} 
     53h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, 
     54h4:hover .anchor, h5:hover .anchor, h6:hover .anchor { 
     55 visibility: visible; 
    4356} 
    4457 
    4558@media screen { 
  • htdocs/css/wiki.css

    diff -r f8e1ea02978c -r 3081679abd00 htdocs/css/wiki.css
    a b  
    2121#overview .multi { color: #999 } 
    2222#overview .ipnr { color: #999; font-size: 80% } 
    2323#overview .comment { padding: 1em 0 0 } 
    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 } 
    3724 
    3825/* Styles for the page history table 
    3926   (extends the styles for "table.listing") */ 
  • templates/macros.cs

    diff -r f8e1ea02978c -r 3081679abd00 templates/macros.cs
    a b  
    190190   <input type="submit" value="Attach File" /> 
    191191  </div></form><?cs 
    192192 /if ?><?cs if:len(attachments) ?></div><?cs /if ?><?cs 
     193/def ?><?cs 
     194 
     195def:plural(base, count) ?><?cs 
     196 var:base ?><?cs if:count > 1 ?>s<?cs /if ?><?cs 
    193197/def ?> 
  • templates/ticket.cs

    diff -r f8e1ea02978c -r 3081679abd00 templates/ticket.cs
    a b  
    8080<?cs call:list_of_attachments(ticket.attachments, ticket.attach_href) ?> 
    8181<?cs /if ?> 
    8282 
     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 
    8387<?cs if:len(ticket.changes) ?><h2>Change History</h2> 
    8488<div id="changelog"><?cs 
    8589 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  <h3 <?cs if:change.cnum ?>id="comment:<?cs var:change.cnum ?>"<?cs /if ?>  
     91      class="change"><?cs 
     92   var:change.date ?>:  
     93    Modification <a href="#comment:<?cs var:change.cnum ?>" 
     94                    title="Permalink to comment:<?cs var:change.cnum ?>"> 
     95     <?cs var:change.cnum ?></a> 
     96    by <?cs var:change.author ?> <?cs 
     97   if:change.cnum ?><?cs 
     98    set:nreplies = len(ticket.replies[change.cnum]) ?><?cs 
     99    if:nreplies || change.replyto ?><span class="threading"> &mdash; <?cs 
     100     if:change.replyto ?>in reply to: <?cs  
     101      call:commentref('&uArr;', change.replyto) ?><?cs if nreplies ?> &ndash; <?cs /if ?><?cs 
     102     /if ?><?cs 
     103     if nreplies ?><?cs 
     104      call:plural('follow-up', nreplies) ?>: <?cs  
     105      each:reply = ticket.replies[change.cnum] ?><?cs  
     106       call:commentref('&dArr;', reply) ?><?cs  
     107      /each ?><?cs  
     108     /if ?></span><?cs 
     109    /if ?><?cs 
     110   /if ?></h3><?cs 
     111  if:change.cnum ?> 
     112   <form method="get" action="<?cs var:ticket.href ?>#comment"><div class="inlinebuttons"> 
     113    <input type="hidden" name="replyto" value="<?cs var:change.cnum ?>" /> 
     114    <input type="submit" value="Reply" title="Reply to comment <?cs var:change.cnum ?>" /></div> 
     115   </form><?cs  
     116   /if ?><?cs 
    88117  if:len(change.fields) ?> 
    89118   <ul class="changes"><?cs 
    90119   each:field = change.fields ?> 
     
    277306 
    278307 <div class="buttons"> 
    279308  <input type="hidden" name="ts" value="<?cs var:ticket.ts ?>" /> 
     309  <input type="hidden" name="replyto" value="<?cs var:ticket.replyto ?>" /> 
     310  <input type="hidden" name="cnum" value="<?cs var:ticket.cnum ?>" /> 
    280311  <input type="submit" name="preview" value="Preview" accesskey="r" />&nbsp; 
    281312  <input type="submit" value="Submit changes" /> 
    282313 </div> 
  • trac/ticket/api.py

    diff -r f8e1ea02978c -r 3081679abd00 trac/ticket/api.py
    a b  
    186186 
    187187    def get_link_resolvers(self): 
    188188        return [('bug', self._format_link), 
    189                 ('ticket', self._format_link)] 
     189                ('ticket', self._format_link), 
     190                ('comment', self._format_comment_link)] 
    190191 
    191192    def get_wiki_syntax(self): 
    192193        yield ( 
     
    215216        return html.A(class_='missing ticket', rel='nofollow', 
    216217                      href=formatter.href.ticket(target))[label] 
    217218 
     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  
    218242    # ISearchSource methods 
    219243 
    220244    def get_search_filters(self, req): 
  • trac/ticket/model.py

    diff -r f8e1ea02978c -r 3081679abd00 trac/ticket/model.py
    a b  
    180180        self._old = {} 
    181181        return self.id 
    182182 
    183     def save_changes(self, author, comment, when=0, db=None): 
     183    def save_changes(self, author, comment, when=0, db=None, cnum=''): 
    184184        """ 
    185185        Store ticket changes in the database. The ticket must already exist in 
    186186        the database. 
     
    244244        if comment: 
    245245            cursor.execute("INSERT INTO ticket_change " 
    246246                           "(ticket,time,author,field,oldvalue,newvalue) " 
    247                            "VALUES (%s,%s,%s,'comment','',%s)", 
    248                            (self.id, when, author, comment)) 
     247                           "VALUES (%s,%s,%s,'comment',%s,%s)", 
     248                           (self.id, when, author, cnum, comment)) 
    249249 
    250250        cursor.execute("UPDATE ticket SET changetime=%s WHERE id=%s", 
    251251                       (when, self.id)) 
     
    265265        db = self._get_db(db) 
    266266        cursor = db.cursor() 
    267267        if when: 
    268             cursor.execute("SELECT time,author,field,oldvalue,newvalue " 
     268            cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 " 
    269269                           "FROM ticket_change WHERE ticket=%s AND time=%s " 
    270270                           "UNION " 
    271                            "SELECT time,author,'attachment',null,filename " 
     271                           "SELECT time,author,'attachment',null,filename,0 " 
    272272                           "FROM attachment WHERE id=%s AND time=%s " 
    273273                           "UNION " 
    274                            "SELECT time,author,'comment',null,description " 
     274                           "SELECT time,author,'comment',null,description,0 " 
    275275                           "FROM attachment WHERE id=%s AND time=%s " 
    276276                           "ORDER BY time", 
    277277                           (self.id, when, str(self.id), when, self.id, when)) 
    278278        else: 
    279             cursor.execute("SELECT time,author,field,oldvalue,newvalue " 
     279            cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 " 
    280280                           "FROM ticket_change WHERE ticket=%s " 
    281281                           "UNION " 
    282                            "SELECT time,author,'attachment',null,filename " 
     282                           "SELECT time,author,'attachment',null,filename,0 " 
    283283                           "FROM attachment WHERE id=%s " 
    284284                           "UNION " 
    285                            "SELECT time,author,'comment',null,description " 
     285                           "SELECT time,author,'comment',null,description,0 " 
    286286                           "FROM attachment WHERE id=%s " 
    287287                           "ORDER BY time", (self.id,  str(self.id), self.id)) 
    288288        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)) 
    291292        return log 
    292293 
    293294    def delete(self, db=None): 
  • trac/ticket/notification.py

    diff -r f8e1ea02978c -r 3081679abd00 trac/ticket/notification.py
    a b  
    7575        changes = '' 
    7676        if not self.newticket and modtime:  # Ticket change 
    7777            changelog = ticket.get_changelog(modtime) 
    78             for date, author, field, old, new in changelog: 
     78            for date, author, field, old, new, permanent in changelog: 
    7979                self.hdf.set_unescaped('ticket.change.author', author) 
    8080                pfx = 'ticket.change.%s' % field 
    8181                newv = '' 
  • trac/ticket/web_ui.py

    diff -r f8e1ea02978c -r 3081679abd00 trac/ticket/web_ui.py
    a b  
    268268                if comment: 
    269269                    req.hdf['ticket.comment'] = comment 
    270270                    # Wiki format a preview of comment 
    271                     req.hdf['ticket.comment_preview'] = wiki_to_html(comment, 
    272                                                                      self.env, 
    273                                                                      req, db) 
     271                    req.hdf['ticket.comment_preview'] = wiki_to_html( 
     272                        comment, self.env, req, db) 
    274273        else: 
    275274            req.hdf['ticket.reassign_owner'] = req.authname 
    276275            # Store a timestamp in order to detect "mid air collisions" 
     
    511510            ticket['resolution'] = '' 
    512511 
    513512        now = int(time.time()) 
     513        cnum = req.args.get('cnum')         
    514514        ticket.save_changes(req.args.get('author', req.authname), 
    515                             req.args.get('comment'), when=now, db=db) 
     515                            req.args.get('comment'), when=now, db=db, 
     516                            cnum=cnum) 
    516517        db.commit() 
    517518 
    518519        try: 
     
    522523            self.log.exception("Failure sending notification on change to " 
    523524                               "ticket #%s: %s" % (ticket.id, e)) 
    524525 
    525         req.redirect(req.href.ticket(ticket.id)) 
     526        fragment = '' 
     527        if cnum: 
     528            fragment = '#comment:'+cnum 
     529        req.redirect(req.href.ticket(ticket.id) + fragment) 
    526530 
    527531    def _insert_ticket_data(self, req, db, ticket, reporter_id): 
    528532        """Insert ticket data into the hdf""" 
     533        replyto = req.args.get('replyto') 
     534        req.hdf['title'] = '#%d (%s)' % (ticket.id, ticket['summary']) 
    529535        req.hdf['ticket'] = ticket.values 
    530         req.hdf['ticket.id'] = ticket.id 
    531         req.hdf['ticket.href'] = req.href.ticket(ticket.id) 
    532  
     536        req.hdf['ticket'] = { 
     537            'id': ticket.id, 
     538            'href': req.href.ticket(ticket.id), 
     539            'replyto': replyto 
     540            } 
     541 
     542        # -- Ticket fields 
     543         
    533544        for field in TicketSystem(self.env).get_ticket_fields(): 
    534545            if field['type'] in ('radio', 'select'): 
    535546                value = ticket.values.get(field['name']) 
     
    547558            req.hdf['ticket.fields.' + name] = field 
    548559 
    549560        req.hdf['ticket.reporter_id'] = reporter_id 
    550         req.hdf['title'] = '#%d (%s)' % (ticket.id, ticket['summary']) 
    551         req.hdf['ticket.description.formatted'] = wiki_to_html(ticket['description'], 
    552                                                                self.env, req, db) 
     561        req.hdf['ticket.description.formatted'] = wiki_to_html( 
     562            ticket['description'], self.env, req, db) 
    553563 
    554564        req.hdf['ticket.opened'] = format_datetime(ticket.time_created) 
    555565        req.hdf['ticket.opened_delta'] = pretty_timedelta(ticket.time_created) 
    556566        if ticket.time_changed != ticket.time_created: 
    557             req.hdf['ticket.lastmod'] = format_datetime(ticket.time_changed) 
    558             req.hdf['ticket.lastmod_delta'] = pretty_timedelta(ticket.time_changed) 
     567            req.hdf['ticket'] = { 
     568                'lastmod': format_datetime(ticket.time_changed), 
     569                'lastmod_delta': pretty_timedelta(ticket.time_changed) 
     570                } 
     571 
     572        # -- Ticket Change History 
    559573 
    560574        changelog = ticket.get_changelog(db=db) 
    561575        curr_author = None 
    562         curr_date   = 0 
     576        curr_date = 0 
     577        autonum = 0 
     578        replies = {} 
    563579        changes = [] 
    564         for date, author, field, old, new in changelog: 
     580        current = None 
     581        for date, author, field, old, new, permanent in changelog: 
    565582            if date != curr_date or author != curr_author: 
    566                 changes.append({ 
     583                current = { 
    567584                    'date': format_datetime(date), 
    568585                    'author': author, 
    569586                    'fields': {} 
    570                 }) 
     587                } 
     588                changes.append(current) 
    571589                curr_date = date 
    572590                curr_author = author 
     591                if permanent: 
     592                    autonum += 1 
     593                    current['cnum'] = autonum 
    573594            if field == 'comment': 
    574                 changes[-1]['comment'] = wiki_to_html(new, self.env, req, db) 
     595                # numbered comments threading 
     596                if permanent: 
     597                    if old: 
     598                        current['cnum'] = old 
     599                        this_num = old 
     600                        parentsep = this_num.rfind('.')  
     601                        if parentsep > -1: # reply 1.2.3 -> parent 1.2 
     602                            parent = this_num[:parentsep] 
     603                            current['replyto'] = parent 
     604                            replies.setdefault(parent, []).append(this_num) 
     605                            autonum -= 1 # autonum skip replies 
     606                    else: 
     607                        this_num = str(autonum) 
     608                # prefill comment textarea with the cited parent, when replying 
     609                if permanent and replyto == this_num and \ 
     610                       not 'comment' in req.args: 
     611                    comment_num = this_num 
     612                    req.hdf['ticket.comment'] = '\n'.join( 
     613                        ['Replying to [comment:%s %s:]' % (replyto, author)] + 
     614                        ['> %s' % line for line in new.splitlines()] + ['']) 
     615                current['comment'] = wiki_to_html(new, self.env, req, db) 
    575616            elif field == 'description': 
    576                 changes[-1]['fields'][field] = '' 
     617                current['fields'][field] = '' 
    577618            else: 
    578                 changes[-1]['fields'][field] = {'old': old, 
    579                                                 'new': new} 
    580         req.hdf['ticket.changes'] = changes 
    581  
    582         # List attached files 
     619                current['fields'][field] = {'old': old, 'new': new} 
     620        if replyto: 
     621            cnum = '%s.%d' % (replyto, len(replies.get(replyto, [])) + 1) 
     622        else: 
     623            cnum = autonum + 1 
     624        req.hdf['ticket'] = { 
     625            'changes': changes, 
     626            'replies': replies, 
     627            'cnum': cnum 
     628           } 
     629 
     630        # -- Ticket Attachments 
     631 
    583632        req.hdf['ticket.attachments'] = attachments_to_hdf(self.env, req, db, 
    584633                                                           'ticket', ticket.id) 
    585634        if req.perm.has_permission('TICKET_APPEND'):