Edgewall Software

Ticket #2703: comment_threading-r3361.2.patch

File comment_threading-r3361.2.patch, 18.6 KB (added by cboos, 2 years ago)

second round, this time works with IE as well ;)

  • htdocs/css/trac.css

     
    4242 color: inherit; 
    4343} 
    4444 
     45/* Heading anchors */ 
     46.anchor:link, .anchor:visited { 
     47 border: none; 
     48 color: #d7d7d7; 
     49 font-size: .8em; 
     50 vertical-align: text-top; 
     51} 
     52* > .anchor:link, * > .anchor:visited { 
     53 visibility: hidden; 
     54} 
     55h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, 
     56h4:hover .anchor, h5:hover .anchor, h6:hover .anchor { 
     57 visibility: visible; 
     58} 
     59 
    4560@media screen { 
    4661 a.ext-link .icon { 
    4762  background: url(../extlink.gif) left center no-repeat; 
     
    277292} 
    278293 
    279294blockquote.citation {  
    280  margin: 0; 
     295 margin: -0.6em 0; 
    281296 border-style: solid;  
    282297 border-width: 0 0 0 2px;  
     298 padding-left: .5em; 
    283299 border-color: #b44;  
    284  padding-left: .5em; 
    285300} 
     301.citation blockquote.citation { border-color: #4b4; } 
     302.citation .citation blockquote.citation { border-color: #44b; } 
     303.citation .citation .citation blockquote.citation { border-color: #c55; } 
    286304 
    287305table.wiki { 
    288306 border: 2px solid #ccc; 
  • htdocs/css/ticket.css

     
    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} 
     57.inlinebuttons > input { /* rule ignored by IE */ 
     58 visibility: hidden; 
     59} 
     60div.change:hover .inlinebuttons input { 
     61 visibility: visible; 
     62} 
     63 
    4964#changelog .changes { list-style: square; margin-left: 2em; padding: 0 } 
    5065#changelog .comment { margin-left: 2em } 
    5166 
  • htdocs/css/wiki.css

     
    2222#overview .ipnr { color: #999; font-size: 80% } 
    2323#overview .comment { padding: 1em 0 0 } 
    2424 
    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  
    3825/* Styles for the page history table 
    3926   (extends the styles for "table.listing") */ 
    4027#wikihist td { padding: 0 .5em } 
  • trac/ticket/api.py

     
    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/web_ui.py

     
    269269                if comment: 
    270270                    req.hdf['ticket.comment'] = comment 
    271271                    # 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) 
    275274        else: 
    276275            req.hdf['ticket.reassign_owner'] = req.authname 
    277276            # Store a timestamp in order to detect "mid air collisions" 
     
    512511            ticket['resolution'] = '' 
    513512 
    514513        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) 
    515519        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) 
    517522        db.commit() 
    518523 
    519524        try: 
     
    523528            self.log.exception("Failure sending notification on change to " 
    524529                               "ticket #%s: %s" % (ticket.id, e)) 
    525530 
    526         req.redirect(req.href.ticket(ticket.id)) 
     531        fragment = cnum and '#comment:'+cnum or '' 
     532        req.redirect(req.href.ticket(ticket.id) + fragment) 
    527533 
    528534    def _insert_ticket_data(self, req, db, ticket, reporter_id): 
    529535        """Insert ticket data into the hdf""" 
     536        replyto = req.args.get('replyto') 
     537        req.hdf['title'] = '#%d (%s)' % (ticket.id, ticket['summary']) 
    530538        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            } 
    533544 
     545        # -- Ticket fields 
     546         
    534547        for field in TicketSystem(self.env).get_ticket_fields(): 
    535548            if field['type'] in ('radio', 'select'): 
    536549                value = ticket.values.get(field['name']) 
     
    548561            req.hdf['ticket.fields.' + name] = field 
    549562 
    550563        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) 
    554566 
    555567        req.hdf['ticket.opened'] = format_datetime(ticket.time_created) 
    556568        req.hdf['ticket.opened_delta'] = pretty_timedelta(ticket.time_created) 
    557569        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                } 
    560574 
     575        # -- Ticket Change History 
     576 
    561577        changelog = ticket.get_changelog(db=db) 
    562         curr_author = None 
    563         curr_date   = 0 
     578        autonum = 0 # used for "root" numbers 
     579        replies = {} 
    564580        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 = { 
    568587                    'date': format_datetime(date), 
    569588                    'author': author, 
    570589                    '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 
    574595            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()] + ['']) 
    576613            elif field == 'description': 
    577                 changes[-1]['fields'][field] = '' 
     614                current['fields'][field] = '' 
    578615            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           } 
    582622 
    583         # List attached files 
     623        # -- Ticket Attachments 
     624 
    584625        req.hdf['ticket.attachments'] = attachments_to_hdf(self.env, req, db, 
    585626                                                           'ticket', ticket.id) 
    586627        if req.perm.has_permission('TICKET_APPEND'): 
  • trac/ticket/tests/model.py

     
    196196        ticket['component'] = 'bar' 
    197197        ticket['milestone'] = 'foo' 
    198198        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)) 
    201201            if field == 'component': 
    202202                self.assertEqual(('foo', 'bar'), (old, new)) 
    203203            elif field == 'milestone': 
     
    214214        ticket['component'] = 'bar' 
    215215        ticket['component'] = 'foo' 
    216216        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)) 
    219219            if field == 'comment': 
    220220                self.assertEqual(('', 'Testing'), (old, new)) 
    221221            else: 
  • trac/ticket/model.py

     
    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

     
    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 = '' 
  • templates/ticket.cs

     
    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 <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"> &mdash; <?cs 
     96     if:change.replyto ?>in reply to: <?cs  
     97      call:commentref('&uarr;', change.replyto) ?><?cs if nreplies ?> &ndash; <?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('&darr;', reply) ?><?cs  
     103      /each ?><?cs  
     104     /if ?><?cs 
     105    /if ?></span>&nbsp; 
     106     <a href="#comment:<?cs var:change.cnum ?>" class="anchor" 
     107        title="Permalink to comment:<?cs var:change.cnum ?>">&para;</a><?cs 
     108   /if ?> 
     109  </h3><?cs 
     110  if:change.cnum ?> 
     111   <form method="get" action="<?cs var:ticket.href ?>#comment"><div class="inlinebuttons"> 
     112    <input type="hidden" name="replyto" value="<?cs var:change.cnum ?>" /> 
     113    <input type="submit" value="Reply" title="Reply to comment <?cs var:change.cnum ?>" /></div> 
     114   </form><?cs  
     115  /if ?><?cs 
    88116  if:len(change.fields) ?> 
    89117   <ul class="changes"><?cs 
    90118   each:field = change.fields ?> 
     
    100128   /each ?> 
    101129   </ul><?cs 
    102130  /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 
    105135/if ?> 
    106136 
    107137<?cs if:trac.acl.TICKET_CHGPROP || trac.acl.TICKET_APPEND ?> 
     
    277307 
    278308 <div class="buttons"> 
    279309  <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 ?>" /> 
    280312  <input type="submit" name="preview" value="Preview" accesskey="r" />&nbsp; 
    281313  <input type="submit" value="Submit changes" /> 
    282314 </div> 
  • templates/macros.cs

     
    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 ?>