Edgewall Software

Ticket #1242: xref_cs1343.diff

File xref_cs1343.diff, 59.6 KB (added by cboos@…, 7 years ago)

The xref patch (on [1343])

  • scripts/trac-admin

     
    3030import sqlite 
    3131import StringIO 
    3232 
     33import trac.Environment # has to be imported before Xref 
    3334from trac import perm 
    3435from trac import util 
    3536from trac import sync 
     37from trac import Xref 
     38from trac import Href 
    3639import trac.siteconfig 
    37 import trac.Environment 
    3840 
    3941def my_sum(list): 
    4042    """Python2.1 doesn't have sum()""" 
     
    272274            docs = (self._help_about + self._help_help + 
    273275                    self._help_initenv + self._help_hotcopy + 
    274276                    self._help_resync + self._help_upgrade + 
     277                    self._help_xref + 
    275278                    self._help_wiki + 
    276279#                    self._help_config + self._help_wiki + 
    277280                    self._help_permission + self._help_component + 
     
    542545            print ' Installing wiki pages' 
    543546            cursor = cnx.cursor() 
    544547            self._do_wiki_load(trac.siteconfig.__default_wiki_dir__,cursor) 
     548            print ' Cross-referencing' 
     549            self.__env.href = Href('trac-admin::initenv') 
     550            self.__env._wiki_pages = {} 
     551            Xref.rebuild_cross_references(self.__env, cnx) 
    545552 
    546553            print ' Indexing repository' 
    547             sync.sync(cnx, rep, fs_ptr, pool) 
     554            sync.sync(self.__env, cnx, rep, fs_ptr, pool) 
    548555        except Exception, e: 
    549556            print 'Failed to initialize database.', e 
    550557            sys.exit(2) 
     
    592599        pool = core.svn_pool_create(None) 
    593600 
    594601        self.db_open() # We need to call this function to open the env, really stupid 
     602        self.__env.href = Href.Href('trac-admin::resync') # We need this for the WikiFormatter 
     603        self.__env._wiki_pages = {} 
    595604 
    596605        # Remove any trailing slash or else subversion might abort 
    597606        repository_dir = self.__env.get_config('trac', 'repository_dir') 
     
    605614        print 'resyncing...' 
    606615        self.db_execsql("DELETE FROM revision") 
    607616        self.db_execsql("DELETE FROM node_change") 
    608         sync.sync(cnx, rep, fs_ptr, pool) 
     617        sync.sync(self.__env, cnx, rep, fs_ptr, pool) 
    609618        print 'done.' 
    610619         
     620    _help_xref = [('xref', 'Regenerate all the cross-reference information between trac objects')] 
     621     
     622    ## XRef 
     623    def do_xref(self, line): 
     624        self.db_open() # We need to call this function to open the env, really stupid 
     625        cnx = self.__env.get_db_cnx() 
     626        print 'cross-referencing... (except changesets: use \'resync\' for that)' 
     627        self.__env.href = Href.Href('trac-admin::xref') 
     628        self.__env._wiki_pages = {} 
     629        Xref.rebuild_cross_references(self.__env, cnx, False) 
     630        print 'done.' 
     631         
    611632    ## Wiki 
    612633    _help_wiki = [('wiki list', 'List wiki pages'), 
    613634                  ('wiki remove <name>', 'Remove wiki page'), 
  • trac/core.py

     
    4040    'search'      : ('Search', 'Search', 0), 
    4141    'report'      : ('Report', 'Report', 0), 
    4242    'ticket'      : ('Ticket', 'TicketModule', 0), 
     43    'bug'         : ('Ticket', 'TicketModule', 0), 
     44    'issue'       : ('Ticket', 'TicketModule', 0), 
     45    'source'      : ('Browser', 'Browser', 1), 
     46    'repos'       : ('Browser', 'Browser', 1), 
    4347    'browser'     : ('Browser', 'Browser', 1), 
    4448    'timeline'    : ('Timeline', 'Timeline', 1), 
    4549    'changeset'   : ('Changeset', 'Changeset', 1), 
     
    4852    'attachment'  : ('File', 'Attachment', 0), 
    4953    'roadmap'     : ('Roadmap', 'Roadmap', 0), 
    5054    'settings'    : ('Settings', 'Settings', 0), 
     55    'xref'        : ('Xref', 'XrefModule', 0), 
     56    'orphans'     : ('Xref', 'XrefModule', 0), 
    5157    'milestone'   : ('Milestone', 'Milestone', 0) 
    5258    } 
    5359 
     
    7480        pool, rep, fs_ptr = open_svn_repos(repos_dir) 
    7581        module.repos = rep 
    7682        module.fs_ptr = fs_ptr 
    77         sync.sync(db, rep, fs_ptr, pool) 
     83        sync.sync(env, db, rep, fs_ptr, pool) 
    7884        module.pool = pool 
    7985 
    8086    return module 
  • trac/db_default.py

     
    2121 
    2222 
    2323# Database version identifier. Used for automatic upgrades. 
    24 db_version = 9 
     24db_version = 10 
    2525 
    2626def __mkreports(reports): 
    2727    """Utility function used to create report data in same syntax as the 
     
    159159         UNIQUE(sid,var_name) 
    160160); 
    161161 
     162CREATE TABLE xref ( 
     163         src_type        text, 
     164         src_id          text, 
     165         relation        text, 
     166         dest_type       text, 
     167         dest_id         text, 
     168         facet           text, 
     169         context         text 
     170); 
     171 
     172 
    162173CREATE INDEX node_change_idx    ON node_change(rev); 
    163174CREATE INDEX ticket_change_idx  ON ticket_change(ticket, time); 
    164175CREATE INDEX wiki_idx           ON wiki(name,version); 
    165176CREATE INDEX session_idx        ON session(sid,var_name); 
     177CREATE INDEX xref_src_idx       ON xref(src_id,src_type); 
     178CREATE INDEX xref_dest_idx      ON xref(dest_id,dest_type); 
    166179""" 
    167180 
    168181## 
  • trac/Milestone.py

     
    2424from trac.Ticket import get_custom_fields, Ticket 
    2525from trac.WikiFormatter import wiki_to_html 
    2626from trac.util import * 
     27from trac.Xref import TracRef 
    2728 
    2829import time 
    2930 
     
    159160        cursor.execute("INSERT INTO milestone (name,due,completed,description) " 
    160161                       "VALUES (%s,%s,%s,%s)", 
    161162                       (name, due, completed, description)) 
     163        TracRef('milestone', name).replace_xrefs_from_wiki(self.env, self.db, 'description', description) 
    162164        self.db.commit() 
    163165        req.redirect(self.env.href.milestone(name)) 
    164166 
     
    182184                                   "WHERE milestone=%s", (id,)) 
    183185            self.log.info('Deleting milestone %s' % id) 
    184186            cursor.execute("DELETE FROM milestone WHERE name=%s", (id,)) 
     187            TracRef('milestone', id).delete_xrefs(self.db, 'description') 
    185188            self.db.commit() 
    186189            req.redirect(self.env.href.roadmap()) 
    187190        else: 
     
    266269        action = req.args.get('action', 'view') 
    267270        id = req.args.get('id') 
    268271 
     272        TracRef('milestone', id).add_backlinks(self.db, req) 
     273 
    269274        if action == 'new': 
    270275            self.perm.assert_permission(perm.MILESTONE_CREATE) 
    271276            self.render_editor(req) 
  • trac/tests/href.py

     
    9494        self.assertEqual('/attachment/ticket/42/foo.txt?format=raw', 
    9595                         self.href.attachment('ticket', '42', 'foo.txt', 'raw')) 
    9696 
     97    def test_xref(self): 
     98        """Testing Href.xref""" 
     99        self.assertEqual('/xref', self.href.xref()) 
     100        self.assertEqual('/xref/wiki', self.href.xref('wiki')) 
     101        self.assertEqual('/xref/wiki/WikiStart', self.href.xref('wiki', 'WikiStart')) 
     102 
     103    def test_orphans(self): 
     104        """Testing Href.orphans""" 
     105        self.assertEqual('/orphans', self.href.orphans()) 
     106 
    97107def suite(): 
    98108    return unittest.makeSuite(HrefTestCase,'test') 
    99109 
  • trac/Report.py

     
    2222from trac import perm, util 
    2323from trac.Module import Module 
    2424from trac.WikiFormatter import wiki_to_html 
     25from trac.Xref import TracRef 
    2526 
    2627import re 
    2728import time 
     
    113114        cursor.execute("INSERT INTO report (title,sql,description) " 
    114115                       "VALUES (%s,%s,%s)", (title, sql, description)) 
    115116        id = self.db.get_last_id() 
     117        TracRef('report', id).replace_xrefs_from_wiki(self.env, self.db, 'description', description) 
    116118        self.db.commit() 
    117119        req.redirect(self.env.href.report(id)) 
    118120 
     
    122124        if not req.args.has_key('cancel'): 
    123125            cursor = self.db.cursor () 
    124126            cursor.execute("DELETE FROM report WHERE id=%s", (id,)) 
     127            TracRef('report', id).delete_xrefs(self.db, 'description') 
    125128            self.db.commit() 
    126129            req.redirect(self.env.href.report()) 
    127130        else: 
     
    401404                                   req.args.get('description', ''), 
    402405                                   req.args.get('sql', '')) 
    403406 
     407        if id != -1: 
     408            TracRef('report', id).add_backlinks(self.db, req) 
     409 
    404410        if id != -1 or action == 'new': 
    405411            self.add_link('up', self.env.href.report(), 'Available Reports') 
    406412 
  • trac/sync.py

     
    2020# Author: Jonas Borgström <jonas@edgewall.com> 
    2121 
    2222from svn import fs, util, delta, repos, core 
     23from trac.Xref import TracRef 
    2324 
    2425import posixpath 
    2526 
    2627 
    27 def sync(db, repos, fs_ptr, pool): 
     28def sync(env, db, repos, fs_ptr, pool): 
    2829    """ 
    2930    Update the revision and node_change tables to be in sync with 
    3031    the repository. 
     
    5657        cursor.execute ('INSERT INTO revision (rev, time, author, message) ' 
    5758                        'VALUES (%s, %s, %s, %s)', rev + offset, date, 
    5859                        author, message) 
     60        TracRef('changeset', rev + offset).replace_xrefs_from_wiki(env, db, 'content', message) 
    5961        insert_change (subpool, fs_ptr, rev + offset, cursor) 
    6062        core.svn_pool_clear(subpool) 
    6163 
     
    125127                old_path = posixpath.join(old_path, posixpath.split(path)[1]) 
    126128                action = 'A' 
    127129            else: 
    128                 self._save_change(core.svn_node_file, 'A', path)  
     130                self._save_change(core.svn_node_dir, 'A', path)  
    129131                action = None 
    130132 
    131133            if action: 
  • trac/File.py

     
    2525from trac import perm, util 
    2626from trac.Module import Module 
    2727from trac.WikiFormatter import wiki_to_html 
     28from trac.Xref import TracRef 
    2829 
    2930import svn.core 
    3031import svn.fs 
     
    261262 
    262263        self.rev = req.args.get('rev', None) 
    263264        self.path = req.args.get('path', '/') 
     265 
     266        TracRef('source', self.path).add_backlinks(self.db, req) 
     267         
    264268        if not self.rev: 
    265269            rev_specified = 0 
    266270            self.rev = svn.fs.youngest_rev(self.fs_ptr, self.pool) 
  • trac/Log.py

     
    2222from trac import perm, util 
    2323from trac.Module import Module 
    2424from trac.WikiFormatter import wiki_to_oneliner 
     25from trac.Xref import TracRef 
    2526 
    2627import svn.core 
    2728import svn.fs 
     
    121122 
    122123        self.path = req.args.get('path', '/') 
    123124        self.authzperm.assert_permission(self.path) 
     125 
     126        TracRef('source', self.path).add_backlinks(self.db, req) 
     127         
    124128        if req.args.has_key('rev'): 
    125129            try: 
    126130                rev = int(req.args.get('rev')) 
  • trac/upgrades/db10.py

     
     1import time 
     2 
     3sql = """ 
     4-- Initial creation of the general cross-reference table 
     5CREATE TABLE xref ( 
     6         src_type        text, 
     7         src_id          text, 
     8         relation        text, 
     9         dest_type       text, 
     10         dest_id         text, 
     11         facet           text, 
     12         context         text 
     13); 
     14 
     15CREATE INDEX xref_src_idx       ON xref(src_id,src_type); 
     16CREATE INDEX xref_dest_idx      ON xref(dest_id,dest_type); 
     17""" 
     18 
     19def do_upgrade(env, ver, cursor): 
     20    cursor.execute(sql) 
     21 
     22def do_db_upgrade(env, ver, db): 
     23    """Renumbering of ticket comments (using the spare 'oldvalue' field)""" 
     24    cursor = db.cursor() 
     25    update_cursor = db.cursor() 
     26    cursor.execute("SELECT ticket, time, author FROM ticket_change " 
     27                   "WHERE field = 'comment' " 
     28                   "ORDER BY ticket, time, author ") 
     29    previous_ticket = None 
     30    for ticket, time, author in cursor: 
     31        if ticket != previous_ticket: 
     32            previous_ticket = ticket 
     33            n = 1 
     34        update_cursor.execute("UPDATE ticket_change SET " 
     35                              "oldvalue = %s ", 
     36                              (n)) 
     37        n += 1 
     38 
     39         
     40 
     41 
     42 
     43 
     44 
     45 
     46 
     47 
     48 
     49 
     50 
     51 
     52 
     53     
  • trac/Href.py

     
    5656        else: 
    5757            return href_join(self.base, 'browser', path) 
    5858 
     59    def source(self, path, rev=None): 
     60        return self.browser(path, rev) 
     61 
    5962    def login(self): 
    6063        return href_join(self.base, 'login') 
    6164 
     
    176179        if format: 
    177180            href += '?format=%s' % format 
    178181        return href 
     182 
     183    def xref(self, module=None, id=None): 
     184        if module and id: 
     185            href = href_join(self.base, 'xref', str(module), str(id)) 
     186        else: 
     187            href = href_join(self.base, 'xref') 
     188        return href 
     189 
     190    def orphans(self, module=None): 
     191        if module: 
     192            href = href_join(self.base, 'orphans', str(module)) 
     193        else: 
     194            href = href_join(self.base, 'orphans') 
     195        return href 
     196 
  • trac/web/main.py

     
    153153    if match: 
    154154        set_if_missing(args, 'mode', match.group(1)) 
    155155        return args 
    156     match = re.search('^/(ticket|report)(?:/([0-9]+)/*)?', path_info) 
     156    match = re.search('^/(ticket|bug|issue|report)(?:/([0-9]+)/*)?', path_info) 
    157157    if match: 
    158158        set_if_missing(args, 'mode', match.group(1)) 
    159159        if match.group(2): 
    160160            set_if_missing(args, 'id', match.group(2)) 
    161161        return args 
    162     match = re.search('^/(browser|log|file)(?:(/.*))?', path_info) 
     162    match = re.search('^/(browser|source|repos|log|file)(?:(/.*))?', path_info) 
    163163    if match: 
    164164        set_if_missing(args, 'mode', match.group(1)) 
    165165        if match.group(2): 
     
    183183        if match.group(1): 
    184184            set_if_missing(args, 'id', urllib.unquote(match.group(1))) 
    185185        return args 
     186    match = re.search('^/(xref|orphans)(?:/([^/]+))?(?:/(.*)/?)?', path_info) 
     187    if match: 
     188        set_if_missing(args, 'mode', match.group(1)) 
     189        set_if_missing(args, 'type', match.group(2)) 
     190        id = match.group(3) 
     191        if id: 
     192            set_if_missing(args, 'id', urllib.unquote(id)) 
     193        return args 
    186194    return args 
    187195 
    188196def populate_hdf(hdf, env, req=None): 
     
    208216        'login': env.href.login(), 
    209217        'logout': env.href.logout(), 
    210218        'settings': env.href.settings(), 
     219        'xref': env.href.xref(), 
     220        'orphans': env.href.orphans(), 
    211221        'homepage': 'http://trac.edgewall.com/' 
    212222    } 
    213223 
  • trac/Changeset.py

     
    2424from trac.Diff import get_diff_options, hdf_diff, unified_diff 
    2525from trac.Module import Module 
    2626from trac.WikiFormatter import wiki_to_html 
     27from trac.Xref import TracRef 
    2728from trac import authzperm, perm 
    2829 
    2930import svn.core 
     
    435436        else: 
    436437            self.rev = youngest_rev 
    437438 
     439        TracRef('changeset', self.rev).add_backlinks(self.db, req) 
     440 
    438441        self.diff_options = get_diff_options(req) 
    439442        if req.args.has_key('update'): 
    440443            req.redirect(self.env.href.changeset(self.rev)) 
     
    462465        req.hdf['changeset.revision'] = self.rev 
    463466        req.hdf['changeset.changes'] = change_info 
    464467        req.hdf['changeset.href'] = self.env.href.changeset(self.rev) 
    465          
     468 
    466469        if len(change_info) == 0: 
    467470            raise authzperm.AuthzPermissionError() 
    468471         
  • trac/Wiki.py

     
    2424from trac.Module import Module 
    2525from trac.util import escape, TracError, get_reporter_id 
    2626from trac.WikiFormatter import * 
     27from trac.Xref import TracRef 
    2728 
    2829import os 
    2930import time 
     
    5152    Represents a wiki page (new or existing). 
    5253    """ 
    5354 
    54     def __init__(self, name, version, perm_, db): 
    55         self.db = db 
     55    def __init__(self, name, version, perm_, env, db): 
     56        self.env = env 
     57        self.db  = db 
    5658        self.name = name 
    5759        self.perm = perm_ 
    5860        cursor = self.db.cursor () 
     
    108110                            "%s,%s)", (self.name, self.version + 1, 
    109111                            int(time.time()), author, remote_addr, self.text, 
    110112                            comment, self.readonly)) 
     113            TracRef('wiki', self.name).replace_xrefs_from_wiki(self.env, self.db, 'content', self.text) 
    111114            self.db.commit() 
    112115            self.version += 1 
    113116            self.old_readonly = self.readonly 
     
    126129        req.hdf['wiki.page_name'] = escape(pagename) 
    127130        req.hdf['wiki.current_href'] = escape(self.env.href.wiki(pagename)) 
    128131 
     132        self.ref = TracRef('wiki', pagename) 
     133        self.ref.add_backlinks(self.db, req) 
     134 
    129135        if action == 'diff': 
    130136            version = int(req.args.get('version', 0)) 
    131137            self._render_diff(req, pagename, version) 
     
    136142        elif action == 'delete': 
    137143            version = None 
    138144            if req.args.has_key('delete_version'): 
    139                 version = int(req.args['version']) 
     145                version = int(req.args.get('version')) 
    140146            self._delete_page(req, pagename, version) 
    141147        elif action == 'save': 
    142148            if req.args.has_key('cancel'): 
     
    164170            cursor = self.db.cursor() 
    165171            cursor.execute("DELETE FROM wiki WHERE name=%s and version=%s", 
    166172                           (pagename, version)) 
     173            self.ref.delete_xrefs(self.db, 'comment:%s' % version) 
    167174            self.log.info('Deleted version %d of page %s' % (version, pagename)) 
    168175            cursor.execute("SELECT COUNT(*) FROM wiki WHERE name=%s", (pagename,)) 
    169             if not cursor.fetchone(): 
     176            last_version = cursor.fetchone()[0] 
     177            if last_version == 0: 
    170178                page_deleted = 1 
     179            elif version > last_version: # resurrect the previous 'content'  
     180                cursor.execute("SELECT text FROM wiki WHERE name=%s ORDER BY version DESC", 
     181                               (pagename)) 
     182                text = cursor.fetchone()[0] 
     183                self.ref.replace_xrefs_from_wiki(self.env, self.db, 'content', text) 
    171184        else: # Delete a wiki page completely 
    172185            cursor.execute("DELETE FROM wiki WHERE name=%s", (pagename,)) 
    173186            page_deleted = 1 
     
    175188        self.db.commit() 
    176189 
    177190        if page_deleted: 
     191            self.ref.delete_xrefs(self.db) 
    178192            # Delete orphaned attachments 
    179193            for attachment in self.env.get_attachments(self.db, 'wiki', pagename): 
    180194                self.env.delete_attachment(self.db, 'wiki', pagename, 
     
    233247    def _render_editor(self, req, pagename, preview=0): 
    234248        self.perm.assert_permission(perm.WIKI_MODIFY) 
    235249 
    236         page = WikiPage(pagename, None, self.perm, self.db) 
     250        page = WikiPage(pagename, None, self.perm, self.env, self.db) 
    237251        if req.args.has_key('text'): 
    238252            page.set_content(req.args.get('text')) 
    239253        if preview: 
     
    311325            self.add_link('alternate', '?format=txt', 'Plain Text', 
    312326                          'text/plain') 
    313327 
    314         page = WikiPage(pagename, version, self.perm, self.db) 
     328        page = WikiPage(pagename, version, self.perm, self.env, self.db) 
    315329 
    316330        info = { 
    317331            'version': page.version, 
     
    331345    def _save_page(self, req, pagename): 
    332346        self.perm.assert_permission(perm.WIKI_MODIFY) 
    333347 
    334         page = WikiPage(pagename, None, self.perm, self.db) 
     348        page = WikiPage(pagename, None, self.perm, self.env, self.db) 
    335349        if req.args.has_key('text'): 
    336350            page.set_content(req.args.get('text')) 
    337351 
  • trac/Environment.py

     
    2323# 
    2424 
    2525from trac import db, db_default, Logging, Mimeview, util 
     26from trac.Xref import TracRef 
    2627 
     28 
    2729import ConfigParser 
    2830import os 
    2931import shutil 
     
    253255        cursor.execute('INSERT INTO attachment VALUES(%s,%s,%s,%s,%s,%s,%s,%s)', 
    254256                       (type, id, filename, length, int(time.time()), 
    255257                       description, author, ipnr)) 
     258        TracRef(type, id).replace_xrefs_from_wiki(self, cnx, 'attachment:%s' % filename, description) 
    256259        shutil.copyfileobj(attachment.file, fd) 
    257260        self.log.info('New attachment: %s/%s/%s by %s', type, id, filename, author) 
    258261        cnx.commit() 
     
    265268        cursor = cnx.cursor() 
    266269        cursor.execute('DELETE FROM attachment WHERE type=%s AND id=%s AND ' 
    267270                       'filename=%s', (type, id, filename)) 
     271        TracRef(type, id).delete_xrefs(cnx, 'attachment:%s' % filename) 
    268272        os.unlink(path) 
    269273        self.log.info('Attachment removed: %s/%s/%s', type, id, filename) 
    270274        cnx.commit() 
     
    324328                    err = 'No upgrade module for version %i (%s.py)' % (i, upg) 
    325329                    raise EnvironmentError, err 
    326330                d.do_upgrade(self, i, cursor) 
     331                if hasattr(d, 'do_db_upgrade'): # for more complex upgrades, as in 'db9.py' 
     332                    d.do_db_upgrade(self, i, cnx) 
    327333            cursor.execute("UPDATE system SET value=%s WHERE " 
    328334                           "name='database_version'", (db_default.db_version)) 
    329335            self.log.info('Upgraded db version from %d to %d', 
  • trac/Xref.py

     
     1# -*- coding: iso8859-1 -*- 
     2# 
     3# Copyright (C) 2005 Edgewall Software 
     4# Copyright (C) 2005 Christian Boos <cboos@wanadoo.fr> 
     5# 
     6# Trac is free software; you can redistribute it and/or 
     7# modify it under the terms of the GNU General Public License as 
     8# published by the Free Software Foundation; either version 2 of the 
     9# License, or (at your option) any later version. 
     10# 
     11# Trac is distributed in the hope that it will be useful, 
     12# but WITHOUT ANY WARRANTY; without even the implied warranty of 
     13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
     14# General Public License for more details. 
     15# 
     16# You should have received a copy of the GNU General Public License 
     17# along with this program; if not, write to the Free Software 
     18# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
     19# 
     20# Author: Christian Boos <cboos@wanadoo.fr> 
     21 
     22""" 
     23 
     24  This module implements a general cross-reference facility for Trac, 
     25  as suggested in #1242. 
     26 
     27  Currently, cross-references between the following Trac objects are supported: 
     28   * Wiki page 
     29   * Ticket 
     30   * Changeset 
     31   * Report 
     32   * Milestone 
     33   * Source (only as targets for now) 
     34 
     35  Basically, two kinds of references are supported: 
     36   * ''implicit references between objects'' 
     37     Implicit references are created for every TracLinks that can be found 
     38     in (any of) the wiki text of a Trac object. 
     39     Indeed, some objects may have separately editable wiki texts, 
     40     each of them being a ''facet'' of this object. 
     41     (TODO: generic fine grained anchoring: <object href>#<facet> should go to the facet) 
     42   * ''explicit relation between objects'' 
     43     An explicit relation must be created explicitely as such, 
     44     by some programmatic mean. 
     45     Currently, some of the ticket fields are setting up explicit relationships. 
     46 
     47   Note that implicit references always have an empty 'relation', 
     48   whereas explicit references may use the 'facet' for informative purpose. 
     49 
     50""" 
     51 
     52 
     53from trac.Module import Module 
     54from trac.util import escape 
     55from trac.WikiFormatter import XRefFormatter 
     56 
     57__all__ = ['TracRef', 'rebuild_cross_references'] 
     58 
     59 
     60how_much_context = 40 
     61 
     62 
     63class TracRef: 
     64    """ 
     65    A TracRef encapsulate the identity of a Trac Object (changeset, 
     66    wiki page, ticket, ...) and can be used to manage the relationships 
     67    to other Trac Objects. 
     68 
     69    The cross-reference information is stored in the XREF table 
     70    (see trac/db_default.py). 
     71     
     72    Besides the obvious 'type' and 'id' information for the source and the 
     73    destination objects, there is also: 
     74     * facet: the location of the cross-reference within the source object. 
     75     * context: the wikitext surrounding the cross-reference, if any 
     76     * relation: the explicit nature of the relationship. 
     77    """ 
     78 
     79    def __init__(self, type, id): 
     80        self.type = type 
     81        self.id   = id 
     82        if type == 'source':            # TODO: oo-ify 
     83            self.id = id.strip('/') 
     84 
     85    def name(self): 
     86        if self.type == 'wiki':         # TODO: oo-ify 
     87            # TODO: use canonical name if it doesn't follow the WikiPageNames conventions 
     88            return escape(self.id) 
     89        elif self.type == 'ticket': 
     90            return 'Ticket #%s' % self.id 
     91        elif self.type == 'changeset': 
     92            return 'Changeset [%s]' % self.id 
     93        elif self.type == 'report': 
     94            return 'Report {%s}' % self.id 
     95        else: 
     96            return self.type + ':' + escape(self.id) 
     97 
     98    def icon(self): 
     99        if self.type == 'ticket':       # TODO: oo-ify 
     100            return 'newticket' 
     101        else: 
     102            return self.type 
     103 
     104    def href(self, env): 
     105        m = getattr(env.href, self.type) 
     106        if m: 
     107            return m(self.id) 
     108        else: 
     109            return env.href.wiki() 
     110 
     111    # -- used by other Modules 
     112 
     113    def add_backlinks(self, db, req): 
     114        req.hdf['xref_count'] = self.count_sources(db) 
     115 
     116    def replace_relation(self, db, relation, dest, facet='', context=''): 
     117        """ 
     118        Replace the related object for the given 'relation'. 
     119        The 'facet' and 'context' are only used to recreate the new relation. 
     120        Best suited for 1 to 1 relationships. 
     121        """ 
     122        print "- %s:%s --[%s]--> *:*" % (self.type, self.id, relation) 
     123        cursor = db.cursor() 
     124        cursor.execute("DELETE FROM xref " 
     125                       "WHERE src_id = %s AND src_type = %s " 
     126                       "AND relation = %s ", 
     127                       (self.id, self.type, relation)) 
     128        self.insert_xref(db, relation, dest, facet, context) 
     129 
     130    def replace_xrefs_from_wiki(self, env, db, facet, wikitext): 
     131        """ 
     132        Remove then re-create the cross-references for the given facet. 
     133        """ 
     134        self.delete_xrefs(db, facet) 
     135        self._create_xrefs_from_wiki(env, db, facet, wikitext) 
     136 
     137 
     138    def _create_xrefs_from_wiki(self, env, db, facet, wikitext): 
     139        """ 
     140        Parse the given 'wikitext' in order to generate all the cross-reference. 
     141        It is assumed that self is the source object for these references. 
     142        """ 
     143        XRefFormatter(env, db, False).format(wikitext, self, facet) 
     144 
     145    def delete_xrefs(self, db, facet=None): 
     146        """ 
     147        Remove all the cross-references having this reference as a source. 
     148        Only delete the cross-references for the 'facet', if given. 
     149        """ 
     150        cursor = db.cursor() 
     151        if facet: 
     152            facet_clause = "AND facet = %s" 
     153            tuple = (self.id, self.type, facet) 
     154            print "- %s:%s --[*]--> *:* (in %s)" % (self.type, self.id, facet) 
     155        else: 
     156            facet_clause = "" 
     157            tuple = (self.id, self.type) 
     158            print "- %s:%s --[*]--> *:*" % (self.type, self.id) 
     159        cursor.execute("DELETE FROM xref " 
     160                       "WHERE src_id = %s AND src_type = %s " + facet_clause, 
     161                       tuple) 
     162 
     163 
     164    def count_sources(self, db): 
     165        return self._count(db, 'dest') 
     166 
     167    def count_destinations(self, db): 
     168        return self._count(db, 'src') 
     169 
     170    def _count(self, db, base): 
     171        cursor = db.cursor() 
     172        cursor.execute("SELECT count(*) FROM xref " 
     173                       "WHERE <base>_id = %s AND <base>_type = %s ".replace('<base>', base), 
     174                       (self.id, self.type)) 
     175        return cursor.fetchone()[0] 
     176 
     177 
     178    def find_sources(self, db, relation=None): 
     179        """ 
     180        Retrieve all the incoming relationships for this object. 
     181        If 'relation' is given, only the sources for the given relation 
     182        are retrieved, otherwise all relations are searched, even implicit ones. 
     183        """ 
     184        return self._find(db, 'dest', 'src', relation) 
     185 
     186    def find_destinations(self, db, relation=None): 
     187        """ 
     188        Retrieve all the outgoing relationships for this object. 
     189        If 'relation' is given, only the targets for the given relation 
     190        are retrieved, otherwise all relations are searched, even implicit ones. 
     191        """ 
     192        return self._find(db, 'src', 'dest', relation) 
     193 
     194    def _find(self, db, base, other, relation): 
     195        cursor = db.cursor() 
     196        if relation: 
     197            relation_clause = "AND relation = %s" 
     198            tuple = (self.id, self.type, relation) 
     199        else: 
     200            relation_clause = "" 
     201            tuple = (self.id, self.type) 
     202        cursor.execute(("SELECT <other>_type, <other>_id, relation, facet, context " 
     203                        "FROM xref WHERE <base>_id = %s AND <base>_type = %s " 
     204                        ).replace('<base>', base).replace('<other>', other) + relation_clause, 
     205                       tuple) 
     206        return cursor 
     207 
     208    # -- used by the WikiFormatter.XRefFormatter: 
     209     
     210    def insert_xref(self, db, relation, dest, facet, context): 
     211        print "+ %s:%s --[%s]--> %s:%s (in %s %s)" % (self.type, self.id,  
     212                                                      relation, 
     213                                                      dest.type, dest.id, 
     214                                                      facet, context) 
     215        cursor = db.cursor() 
     216        cursor.execute("INSERT INTO xref VALUES (%s,%s,%s,%s,%s,%s,%s)", 
     217                       (self.type, self.id, relation, dest.type, dest.id, facet, context)) 
     218 
     219    def extract_context(self, text, start, end): 
     220        start_ellipsis = end_ellipsis = '...' 
     221        start = start - how_much_context 
     222        if start < 0: 
     223            start = 0 
     224            start_ellipsis = '' 
     225        end = end + how_much_context 
     226        if end > len(text): 
     227            end = len(text) 
     228            end_ellipsis = '' 
     229        return start_ellipsis + text[start:end] + end_ellipsis 
     230 
     231 
     232 
     233 
     234 
     235def rebuild_cross_references(env, db, do_changesets=True): 
     236    """ 
     237    Rebuild all cross-references in the given environment. 
     238 
     239    As an option, the rebuilding of the references found in 
     240    changesets can be skipped, as this is done by a 'resync' 
     241    operation, which is what is advised to do in trac-admin. 
     242    """ 
     243    cursor  = db.cursor() 
     244    xreffmt = XRefFormatter(env, db, False) 
     245 
     246    def add_xrefs(src, facet, text): 
     247        xreffmt.format(text, src, facet) 
     248 
     249    # -- wiki objects 
     250    cursor.execute("SELECT name, text, comment, version FROM wiki " 
     251                   "ORDER BY name, version DESC") 
     252    previous_page = None 
     253    src = None 
     254    for name, text, comment, version in cursor: 
     255        if name != previous_page: 
     256            previous_page = name 
     257            src = TracRef('wiki', name) 
     258            src.delete_xrefs(db) 
     259            add_xrefs(src, 'content', text) 
     260        add_xrefs(src, 'comment:%d' % version, comment) 
     261 
     262    # -- ticket objects 
     263    # -- -- description 
     264    cursor.execute("SELECT id, description FROM ticket") 
     265    for id, description in cursor: 
     266        src = TracRef('ticket', id) 
     267        src.delete_xrefs(db) 
     268        add_xrefs(src, 'description', description) 
     269        # -- -- comments 
     270        comment_cursor = db.cursor() 
     271        comment_cursor.execute("SELECT oldvalue, newvalue FROM ticket_change " 
     272                               "WHERE ticket = %s AND field = 'comment'", 
     273                               (id)) 
     274        for n, value in comment_cursor: 
     275            add_xrefs(src, 'comment:%s' % n, value) 
     276        # -- -- custom fields 
     277        # (uncomment when the custom fields will support wiki formatting) 
     278        # ... "SELECT name, value FROM ticket_custom" 
     279 
     280    # -- changeset objects 
     281    if do_changesets: 
     282        cursor.execute("SELECT rev, message FROM revision") 
     283        for rev, message in cursor: 
     284            src = TracRef('changeset', rev) 
     285            src.delete_xrefs(db) 
     286            add_xrefs(src, 'content', message) 
     287 
     288    # -- report objects 
     289    cursor.execute("SELECT id, description FROM report") 
     290    for id, description in cursor: 
     291        src = TracRef('report', id) 
     292        src.delete_xrefs(db) 
     293        add_xrefs(src, 'description', description) 
     294 
     295    # -- milestone objects 
     296    cursor.execute("SELECT name, description FROM milestone") 
     297    for name, description in cursor: 
     298        src = TracRef('milestone', name) 
     299        src.delete_xrefs(db) 
     300        add_xrefs(src, 'description', description) 
     301 
     302    # -- attachment object 
     303    cursor.execute("SELECT type, id, description, filename FROM attachment") 
     304    for type, id, description, filename in cursor: 
     305        add_xrefs(TracRef(type, id), 'attachment:%s' % filename, description) 
     306 
     307    db.commit() 
     308 
     309 
     310 
     311 
     312def find_orphaned_objects(db): 
     313    # Most interesting 'orphans' order: wikis, tickets, milestones then reports and changesets 
     314    queries = [ 
     315        "SELECT DISTINCT(name), 'wiki' FROM wiki " 
     316        "WHERE name NOT IN (SELECT DISTINCT(dest_id) FROM xref WHERE dest_type = 'wiki') " 
     317        , 
     318        "SELECT id, 'ticket' FROM ticket " 
     319        "WHERE id NOT IN (SELECT DISTINCT(dest_id) FROM xref WHERE dest_type = 'ticket') " 
     320        , 
     321        "SELECT name, 'milestone' FROM milestone " 
     322        "WHERE name NOT IN (SELECT DISTINCT(dest_id) FROM xref WHERE dest_type = 'milestone') " 
     323        , 
     324        "SELECT id, 'report' FROM report " 
     325        "WHERE id NOT IN (SELECT DISTINCT(dest_id) FROM xref WHERE dest_type = 'report') " 
     326        ,  
     327        "SELECT rev, 'changeset' FROM revision " 
     328        "WHERE rev NOT IN (SELECT DISTINCT(dest_id) FROM xref WHERE dest_type = 'changeset') " 
     329        , 
     330        # Not sure about this one: it works, but produces a huge list (good for testing, though :) 
     331        # "SELECT DISTINCT(name), 'source' FROM node_change " 
     332        # "WHERE name NOT IN (SELECT DISTINCT(dest_id) FROM xref WHERE dest_type = 'source') " 
     333        ] 
     334    cursor = db.cursor() 
     335    cursor.execute(" UNION ALL ".join(queries)) 
     336    return cursor 
     337 
     338 
     339 
     340 
     341class XrefModule(Module): 
     342    template_name = 'xref.cs' 
     343 
     344    def render(self, req): 
     345        mode = req.args.get('mode', 'xref') 
     346        req.hdf['xref.mode'] = mode 
     347        if mode == 'orphans': 
     348            self._orphans(req) 
     349            self.template_name = 'orphans.cs' 
     350        else: 
     351            direction = req.args.get('direction','back') 
     352            if direction == 'forward': 
     353                req.hdf['xref.direction.name'] = 'Forward Link' 
     354                base = self._base(req) 
     355                self._references(req, base.find_destinations(self.db)) 
     356            else: # direction == back 
     357                req.hdf['xref.direction.back'] = 1 
     358                req.hdf['xref.direction.name'] = 'Backlink' 
     359                base = self._base(req) 
     360                self._references(req, base.find_sources(self.db)) 
     361 
     362    def _base(self, req): 
     363        type = req.args.get('type', 'wiki') 
     364        id = req.args.get('id', 'WikiStart') 
     365        base = TracRef(type, id) 
     366        req.hdf['title'] = req.hdf['xref.direction.name'] + ' for ' + base.name() 
     367        req.hdf['xref.base.type'] = type 
     368        req.hdf['xref.base.id'] = escape(id) 
     369        req.hdf['xref.base.name'] = base.name() 
     370        req.hdf['xref.base.icon'] = base.icon() 
     371        req.hdf['xref.base.href'] = base.href(self.env) 
     372        req.hdf['xref.current_href'] = escape(self.env.href.xref(type, id)) 
     373        return base 
     374 
     375    def _references(self, req, refs): 
     376        links = [] 
     377        relations = [] 
     378        for type, id, relation, facet, context in refs: 
     379            other_ref = TracRef(type, id) 
     380            dict = {'type' : type, 
     381                    'id' : id, 
     382                    'name' : other_ref.name(), 
     383                    'icon' : other_ref.icon(), 
     384                    'href' : other_ref.href(self.env), 
     385                    'relation' : relation, 
     386                    'facet' : facet, 
     387                    'context' : context} 
     388            if relation: 
     389                relations.append(dict) 
     390            else: 
     391                links.append(dict) 
     392        req.hdf['xref.links'] = links 
     393        req.hdf['xref.relations'] = relations 
     394 
     395    def _orphans(self, req): 
     396        req.hdf['title'] = 'Orphaned objects' 
     397        orphans = [] 
     398        for id, type in find_orphaned_objects(self.db): 
     399            ref = TracRef(type, id) 
     400            obj = {'type' : type, 
     401                    'id' : id, 
     402                    'name' : ref.name(), 
     403                    'icon' : ref.icon(), 
     404                    'href' : ref.href(self.env)} 
     405            orphans.append(obj) 
     406        req.hdf['orphans'] = orphans 
     407         
  • trac/Ticket.py

     
    2323from trac.Module import Module 
    2424from trac.WikiFormatter import wiki_to_html 
    2525from trac.Notify import TicketNotifyEmail 
     26from trac.Xref import TracRef 
    2627 
    2728import time 
    2829import string 
     
    3738                  'reporter', 'owner', 'cc', 'url', 'version', 'status', 
    3839                  'resolution', 'keywords', 'summary', 'description', 
    3940                  'changetime'] 
     41    field_xrefs = { 'description' : ('implicit',), 
     42                    'summary'     : ('implicit',), 
     43                    'milestone'   : ('1 to 1', 'milestone'), 
     44                    'component'   : ('1 to 1', 'wiki') } 
    4045 
    4146    def __init__(self, *args): 
    4247        UserDict.__init__(self) 
    4348        self._old = {} 
     49        self.ref = TracRef('ticket', -1) 
    4450        if len(args) == 2: 
    4551            self._fetch_ticket(*args) 
    4652 
     
    6571            raise util.TracError('Ticket %d does not exist.' % id, 
    6672                                 'Invalid Ticket Number') 
    6773 
    68         self['id'] = id 
     74        self['id'] = self.ref.id = id 
    6975        for i in range(len(Ticket.std_fields)): 
    7076            self[Ticket.std_fields[i]] = row[i] or '' 
    7177 
     
    9197            if not dict.has_key(name): 
    9298                self[name] = '0' 
    9399 
    94     def insert(self, db): 
     100    def insert(self, env, db): 
    95101        """Add ticket to database""" 
    96102        assert not self.has_key('id') 
     103        assert self.ref.id == -1 
    97104 
    98105        # Add a timestamp 
    99106        now = int(time.time()) 
     
    107114                       % (','.join(std_fields), 
    108115                          ','.join(['%s'] * len(std_fields))), 
    109116                       map(lambda n, self=self: self[n], std_fields)) 
    110         id = db.get_last_id() 
     117        self['id'] = self.ref.id = db.get_last_id() 
    111118 
     119        for name in Ticket.field_xrefs.keys(): 
     120            self.xref_field(env, db, name, self[name], 
     121                            time.strftime('%c', time.localtime(now))) 
     122         
    112123        custom_fields = filter(lambda n: n[:7] == 'custom_', self.keys()) 
    113124        for name in custom_fields: 
    114125            cursor.execute("INSERT INTO ticket_custom(ticket,name,value) " 
    115                            "VALUES(%s,%s,%s)", (id, name[7:], self[name])) 
     126                           "VALUES(%s,%s,%s)", (self.ref.id, name[7:], self[name])) 
     127            # TODO: support xrefs in custom fields too... 
    116128        db.commit() 
    117         self['id'] = id 
    118129        self._forget_changes() 
    119         return id 
     130        return self.ref.id 
    120131 
    121     def save_changes(self, db, author, comment, when = 0): 
     132    def save_changes(self, env, db, author, comment, when = 0): 
    122133        """Store ticket changes in the database. 
    123134        The ticket must already exist in the database.""" 
    124135        assert self.has_key('id') 
     
    161172                fname = name 
    162173                cursor.execute("UPDATE ticket SET %s=%s WHERE id=%s", 
    163174                               (fname, self[name], id)) 
     175            self.xref_field(env, db, fname, self[name], 
     176                            time.strftime('%c', time.localtime(when))) 
    164177            cursor.execute("INSERT INTO ticket_change " 
    165178                           "(ticket,time,author,field,oldvalue,newvalue) " 
    166179                           "VALUES (%s, %s, %s, %s, %s, %s)", 
    167180                           (id, when, author, fname, self._old[name], 
    168181                            self[name])) 
    169182        if comment: 
     183            cursor.execute("SELECT count(*) FROM ticket_change " 
     184                               "WHERE ticket = %s", # AND oldvalue LIKE '%%%s.' parent (threads) 
     185                               (id)) 
     186            n = cursor.fetchone()[0] + 1 
    170187            cursor.execute("INSERT INTO ticket_change " 
    171188                           "(ticket,time,author,field,oldvalue,newvalue) " 
    172                            "VALUES (%s,%s,%s,'comment','',%s)", 
    173                            (id, when, author, comment)) 
     189                           "VALUES (%s,%s,%s,'comment',%s,%s)", 
     190                           (id, when, author, n, comment)) 
     191            self.ref.replace_xrefs_from_wiki(env, db, 'comment:%d' % n, comment) 
    174192 
    175193        cursor.execute("UPDATE ticket SET changetime=%s WHERE id=%s", 
    176194                       (when, id)) 
     
    208226            log.append((int(row[0]), row[1], row[2], row[3] or '', row[4] or '')) 
    209227        return log 
    210228 
     229    def xref_field(self, env, db, name, value, context=''): 
     230        """ 
     231        Create cross-references and relationships for this ticket. 
     232        """ 
     233        assert self.ref.id != -1 
     234        xref_kind = Ticket.field_xrefs[name] 
     235        if xref_kind: 
     236            if xref_kind[0] == 'implicit': 
     237                self.ref.replace_xrefs_from_wiki(env, db, name, self[name]) 
     238            elif xref_kind[0] == '1 to 1': 
     239                self.ref.replace_relation(db, 'has-%s' % name, TracRef(xref_kind[1], value),  
     240                                          'field', context) 
    211241 
     242 
     243 
    212244def get_custom_fields(env): 
    213245    cfg = env.get_config_items('ticket-custom') 
    214246    if not cfg: 
     
    296328            owner = cursor.fetchone()[0] 
    297329            ticket['owner'] = owner 
    298330 
    299         tktid = ticket.insert(self.db) 
     331        tktid = ticket.insert(self.env, self.db) 
    300332 
    301333        # Notify 
    302334        try: 
     
    399431        ticket.populate(req.args) 
    400432 
    401433        now = int(time.time()) 
    402         ticket.save_changes(self.db, req.args.get('author', req.authname), 
     434        ticket.save_changes(self.env, self.db, 
     435                            req.args.get('author', req.authname), 
    403436                            req.args.get('comment'), when=now) 
    404437 
    405438        try: 
     
    511544        ticket = Ticket(self.db, id) 
    512545        reporter_id = util.get_reporter_id(req) 
    513546 
     547        TracRef('ticket', id).add_backlinks(self.db, req) 
     548 
    514549        if preview: 
    515550            # Use user supplied values 
    516551            for field in Ticket.std_fields: 
  • trac/Browser.py

     
    2222from trac import perm, util 
    2323from trac.Module import Module 
    2424from trac.WikiFormatter import wiki_to_oneliner 
     25from trac.Xref import TracRef 
    2526 
    2627import svn.core 
    2728import svn.fs 
     
    165166        desc = req.args.has_key('desc') 
    166167         
    167168        self.authzperm.assert_permission (path) 
    168          
     169 
     170        TracRef('source', path).add_backlinks(self.db, req) 
     171 
    169172        if not rev: 
    170173            rev_specified = 0 
    171174            rev = svn.fs.youngest_rev(self.fs_ptr, self.pool) 
  • trac/WikiFormatter.py

     
    2727 
    2828import util 
    2929 
     30 
    3031__all__ = ['Formatter', 'OneLinerFormatter', 'wiki_to_html', 'wiki_to_oneliner'] 
    3132 
    3233 
     
    5152 
    5253    _open_tags = [] 
    5354    env = None 
     55    xref = None 
    5456    absurls = 0 
    5557 
    5658    def __init__(self, env, db, absurls=0): 
     
    6567                # Check for preceding escape character '!' 
    6668                if match[0] == '!': 
    6769                    return match[1:] 
     70                if self.xref: # remember the context 
     71                    self.context = self.xref.extract_context(self.text, fullmatch.start(), fullmatch.end()) 
    6872                return getattr(self, '_' + itype + '_formatter')(match, fullmatch) 
    6973 
    7074    def tag_open_p(self, tag): 
     
    162166        else: 
    163167            return '<a href="%s">%s</a>' % (url, text) 
    164168 
     169    def make_xref(self,type,id): 
     170        """ 
     171        Create a new cross-reference for the given destination type and id. 
     172        All the source information (TracRef, facet and context) is 
     173        already known at this point. 
     174        """ 
     175        if self.xref: 
     176            from trac.Xref import TracRef 
     177            self.xref.insert_xref(self.db, '', TracRef(type, id), self.facet, self.context) 
     178 
    165179    def _make_wiki_link(self, page, text): 
    166180        anchor = '' 
    167181        if page.find('#') != -1: 
    168182            anchor = page[page.find('#'):] 
    169183            page = page[:page.find('#')] 
     184        self.make_xref('wiki', page) 
    170185        if not self.env._wiki_pages.has_key(page): 
    171186            return '<a class="missing wiki" href="%s" rel="nofollow">%s?</a>' \ 
    172187                   % (self._href.wiki(page) + anchor, text) 
     
    175190                   % (self._href.wiki(page) + anchor, text) 
    176191 
    177192    def _make_changeset_link(self, rev, text): 
     193        self.make_xref('changeset', rev) 
    178194        cursor = self.db.cursor() 
    179195        cursor.execute('SELECT message FROM revision WHERE rev=%s', (rev,)) 
    180196        row = cursor.fetchone() 
     
    187203                   % (self._href.changeset(rev), text) 
    188204 
    189205    def _make_ticket_link(self, id, text): 
     206        self.make_xref('ticket', id) 
    190207        cursor = self.db.cursor() 
    191208        cursor.execute("SELECT summary,status FROM ticket WHERE id=%s", (id,)) 
    192209        row = cursor.fetchone() 
     
    202219            return '<a class="missing ticket" href="%s" rel="nofollow">%s</a>' \ 
    203220                   % (self._href.ticket(id), text) 
    204221    _make_bug_link = _make_ticket_link # alias 
     222    _make_issue_link = _make_ticket_link # alias 
    205223 
    206224    def _make_milestone_link(self, name, text): 
     225        self.make_xref('milestone', name) 
    207226        return '<a class="milestone" href="%s">%s</a>' \ 
    208227               % (self._href.milestone(name), text) 
    209228 
    210229    def _make_report_link(self, id, text): 
     230        self.make_xref('report', id) 
    211231        return '<a class="report" href="%s">%s</a>' \ 
    212232               % (self._href.report(id), text) 
    213233 
     
    221241        if match: 
    222242            path = match.group(1) 
    223243            rev = match.group(2) 
     244        self.make_xref('source', path) 
    224245        if rev: 
    225246            return '<a class="source" href="%s">%s</a>' \ 
    226247                   % (self._href.browser(path, rev), text) 
     
    231252    _make_repos_link = _make_source_link # alias 
    232253 
    233254 
     255class XRefFormatter(CommonFormatter): 
     256    """ 
     257    A special version of the wiki formatter that only cares about Trac objects. 
     258    This version is used for generating cross-references (see Xref.py). 
     259    """ 
     260 
     261    _rules = CommonFormatter._rules  
     262 
     263    _compiled_rules = re.compile('(?:' + string.join(_rules, '|') + ')') 
     264 
     265    def format(self, text, xref, facet): 
     266        if not text: 
     267            return 
     268        self.xref = xref      # remember the source's TracRef 
     269        self.facet = facet    # remember the source's facet 
     270        self._open_tags = [] 
     271 
     272        self.in_code_block = 0 
     273 
     274        rules = self._compiled_rules 
     275 
     276        for line in text.splitlines(): 
     277            # Handle code block 
     278            if self.in_code_block or line.strip() == '{{{': 
     279                self.handle_code_block(line) 
     280                continue 
     281            # Handle Horizontal ruler 
     282            elif line[0:4] == '----': 
     283                continue 
     284            # Handle new paragraph 
     285            elif line == '': 
     286                continue 
     287 
     288            self.text = util.escape(line) 
     289            # Throw a bunch of regexps on the problem 
     290            result = re.sub(rules, self.replace, self.text) 
     291 
     292    def handle_code_block(self, line): 
     293        if line.strip() == '{{{': 
     294            self.in_code_block += 1 
     295        elif line.strip() == '}}}': 
     296            self.in_code_block -= 1 
     297 
     298 
    234299class OneLinerFormatter(CommonFormatter): 
    235300    """ 
    236301    A special version of the wiki formatter that only implement a 
     
    251316 
    252317        rules = self._compiled_rules 
    253318 
    254         result = re.sub(rules, self.replace, util.escape(text.strip())) 
     319        text = util.escape(text.strip()) 
     320        result = re.sub(rules, self.replace, text) 
    255321        # Close all open 'one line'-tags 
    256322        result += self.close_tag(None) 
    257323        out.write(result) 
     
    630696    return out.getvalue() 
    631697 
    632698 
    633 def wiki_to_oneliner(wikitext, env, db,absurls=0): 
     699def wiki_to_oneliner(wikitext, env, db, absurls=0): 
    634700    out = StringIO.StringIO() 
    635701    OneLinerFormatter(env, db, absurls).format(wikitext, out) 
    636702    return out.getvalue() 
  • templates/report.cs

     
    44 
    55<div id="ctxtnav" class="nav"> 
    66 <h2>Report Navigation</h2> 
    7  <ul> 
    8   <?cs if report.edit_href || report.copy_href || report.delete_href ?> 
     7 <ul><?cs  
     8  call:backlinks("report", report.id) ?><?cs 
     9  if report.edit_href || report.copy_href || report.delete_href ?> 
    910  <li><b>This report:</b> 
    10    <ul> 
    11     <?cs if report.edit_href 
    12       ?><li <?cs if !report.delete_href && !report.copy_href ?>class="last"<?cs /if 
    13         ?>><a href="<?cs var:report.edit_href ?>">Edit</a></li><?cs 
    14      /if ?><?cs 
    15      if report.copy_href ?><li <?cs if !report.delete_href ?>class="last"<?cs /if 
    16         ?>><a href="<?cs var:report.copy_href ?>">Copy</a></li><?cs /if ?><?cs 
    17     if report.delete_href ?><li class="last"><a href="<?cs var:report.delete_href ?>">Delete</a></li><?cs /if ?></ul></li> 
    18   <?cs /if ?> 
    19   <?cs if:report.create_href ?> 
    20    <li><a href="<?cs var:report.create_href ?>">New Report</a></li> 
    21   <?cs /if ?> 
     11   <ul><?cs 
     12    if report.edit_href ?> 
     13    <li <?cs if !report.delete_href && !report.copy_href ?>class="last"<?cs /if ?>> 
     14     <a href="<?cs var:report.edit_href ?>">Edit</a> 
     15    </li><?cs 
     16    /if ?><?cs 
     17    if report.copy_href ?><li <?cs if !report.delete_href ?>class="last"<?cs /if ?>> 
     18     <a href="<?cs var:report.copy_href ?>">Copy</a> 
     19    </li><?cs 
     20    /if ?><?cs 
     21    if report.delete_href ?> 
     22    <li class="last"><a href="<?cs var:report.delete_href ?>">Delete</a></li><?cs  
     23    /if ?> 
     24   </ul> 
     25  </li><?cs 
     26  /if ?><?cs 
     27  if:report.create_href ?> 
     28  <li><a href="<?cs var:report.create_href ?>">New Report</a></li><?cs 
     29  /if ?> 
    2230  <li class="last"><a href="<?cs var:$trac.href.query ?>">Custom Query</a></li> 
    2331 </ul> 
    2432</div> 
  • templates/file.cs

     
    33<?cs include "macros.cs"?> 
    44 
    55<div id="ctxtnav" class="nav"> 
    6  <?cs if:args.mode != 'attachment' && trac.acl.LOG_VIEW ?><ul> 
    7   <li class="last"><a href="<?cs var:file.logurl ?>">Revision Log</a></li> 
    8  </ul><?cs /if ?> 
     6 <ul><?cs  
     7  call:backlinks("source", file.path) ?><?cs 
     8  if:args.mode != 'attachment' && trac.acl.LOG_VIEW ?> 
     9  <li class="last"><a href="<?cs var:file.logurl ?>">Revision Log</a></li><?cs  
     10 /if ?> 
     11 </ul> 
    912</div> 
    1013 
    1114<div id="content" class="file"> 
  • templates/log.cs

     
    33<?cs include "macros.cs"?> 
    44 
    55<div id="ctxtnav" class="nav"> 
    6  <ul> 
     6 <ul><?cs  
     7  call:backlinks("source", log.path) ?> 
    78  <li class="last"><a href="<?cs 
    89    var:log.items.0.file_href ?>">View Latest Revision</a></li> 
    910 </ul> 
  • templates/ticket.cs

     
    55<div id="ctxtnav" class="nav"> 
    66 <h2>Ticket Navigation</h2> 
    77 <ul><?cs 
     8  call:backlinks("ticket", ticket.id) ?><?cs 
    89  if:len(links.prev) ?> 
    910   <li class="first<?cs if:!len(links.up) && !len(links.next) ?> last<?cs /if ?>"> 
    1011    &larr; <a href="<?cs var:links.prev.0.href ?>" title="<?cs 
  • templates/browser.cs

     
    33<?cs include "macros.cs"?> 
    44 
    55<div id="ctxtnav" class="nav"> 
    6  <ul> 
     6 <ul><?cs  
     7  call:backlinks("source", browser.path) ?> 
    78  <li class="last"><a href="<?cs var:browser.log_href ?>">Revision Log</a></li> 
    89 </ul> 
    910</div> 
  • templates/macros.cs

     
    176176  </div><?cs 
    177177 /each ?><?cs 
    178178/def ?> 
     179 
     180<?cs def:backlinks(type,id) ?><?cs  
     181 if:$xref_count == #0 ?><?cs 
     182 elif:$xref_count == #1 ?> 
     183 <li><a href="<?cs var:trac.href.xref ?>/<?cs var:type ?>/<?cs var:id ?>" 
     184        title="One backlink">Backlink</a></li><?cs 
     185 else ?> 
     186 <li><a href="<?cs var:trac.href.xref ?>/<?cs var:type ?>/<?cs var:id ?>" 
     187        title="<?cs var:xref_count ?> backlinks">Backlinks</a></li><?cs 
     188 /if ?><?cs 
     189/def ?> 
     190 
  • templates/milestone.cs

     
    22<?cs include:"header.cs"?> 
    33<?cs include:"macros.cs"?> 
    44 
    5 <div id="ctxtnav" class="nav"></div> 
     5<div id="ctxtnav" class="nav"> 
     6 <h2>Milestone Navigation</h2> 
     7 <ul><?cs  
     8  call:backlinks("milestone", milestone.name) ?> 
     9 </ul> 
     10</div> 
    611 
    712<div id="content" class="milestone"> 
    813 <?cs if:milestone.mode == "new" ?> 
  • templates/changeset.cs

     
    55 
    66<div id="ctxtnav" class="nav"> 
    77 <h2>Changeset Navigation</h2> 
    8  <ul><?cs 
     8 <ul><?cs  
     9  call:backlinks("changeset", changeset.revision) ?><?cs 
    910  if:len(links.prev) ?> 
    1011   <li class="first<?cs if:!len(links.next) ?> last<?cs /if ?>"> 
    1112    <a class="prev" href="<?cs var:links.prev.0.href ?>" title="<?cs 
  • templates/wiki.cs

     
    44 
    55<div id="ctxtnav" class="nav"> 
    66 <h2>Wiki Navigation</h2> 
    7  <ul> 
     7 <ul><?cs 
     8  call:backlinks("wiki", wiki.page_name) ?> 
    89  <li><a href="<?cs var:$trac.href.wiki ?>">Start Page</a></li> 
    910  <li><a href="<?cs var:$trac.href.wiki ?>/TitleIndex">Title Index</a></li> 
    10   <li><a href="<?cs var:$trac.href.wiki ?>/RecentChanges">Recent Changes</a></li> 
    11   <?cs if:wiki.history_href ?> 
    12    <li class="last"><a href="<?cs var:wiki.history_href ?>">Page History</a></li> 
    13   <?cs else ?> 
    14    <li class="last">Page History</li> 
    15   <?cs /if ?> 
     11  <li><a href="<?cs var:$trac.href.wiki ?>/RecentChanges">Recent Changes</a></li><?cs 
     12  if:wiki.history_href ?> 
     13   <li class="last"><a href="<?cs var:wiki.history_href ?>">Page History</a></li><?cs 
     14  else ?> 
     15   <li class="last">Page History</li><?cs  
     16  /if ?> 
    1617 </ul> 
    1718 <hr /> 
    1819</div> 
  • templates/xref.cs

     
     1<?cs set:html.stylesheet = 'css/timeline.css' ?> 
     2<?cs include "header.cs" ?> 
     3<?cs include "macros.cs" ?> 
     4 
     5<div id="ctxtnav" class="nav"> 
     6 <h2>Xref Navigation</h2> 
     7 <ul><?cs 
     8  if:xref.direction.back ?> 
     9   <li><a href="<?cs var:$trac.current_href ?>?direction=forward" 
     10          title="Show a summary of Trac Objects referenced by <?cs var:base.name ?>"> 
     11    Forward Links</a> 
     12   </li><?cs 
     13  else ?> 
     14   <li><a href="<?cs var:$trac.current_href ?>?direction=back" 
     15          title="Show the Trac Objects referencing <?cs var:base.name ?>" 
     16    Backward Links</a> 
     17   </li><?cs 
     18  /if ?> 
     19  <li><a href="<?cs var:$trac.href.orphans ?>" 
     20         title="Show Trac Objects which are not referenced"> 
     21    Orphaned Objects</a> 
     22  </li> 
     23 </ul> 
     24 <hr /> 
     25</div> 
     26<div id="content" class="wiki"> 
     27 
     28 <?cs def:anchor(xref) ?> 
     29  <a href="<?cs var:xref.href ?>"><em><?cs var:xref.name ?></em></a><?cs  
     30 /def ?> 
     31 
     32 <?cs set:nlinks = len(xref.links) ?> 
     33 <?cs set:nrelations = len(xref.relations) ?> 
     34 
     35 <?cs if:$nlinks + $nrelations == #0 ?> 
     36  <h1>No <?cs var:xref.direction.name ?>s for <?cs call:anchor(xref.base) ?></h1><?cs 
     37 elif: $nlinks + $nrelations == #1 ?> 
     38  <h1>One <?cs var:xref.direction.name ?> for <?cs call:anchor(xref.base) ?></h1><?cs 
     39 else ?> 
     40  <h1><?cs var:xref.count ?> <?cs var:xref.direction.name ?>s for <?cs call:anchor(xref.base) ?></h1><?cs 
     41 /if ?> 
     42 
     43 <?cs if:$nrelations > #0 ?> 
     44  <h2>Relationships of <?cs var:xref.base.name ?></h2> 
     45  <dl><?cs 
     46   each:item = xref.relations ?> 
     47    <dt class="<?cs var:item.icon ?>"> 
     48     <a href="<?cs var:item.href ?>"><?cs  
     49      if:xref.direction.back ?> 
     50       <em><?cs var:item.name ?></em> 
     51       <strong><?cs var:item.relation ?></strong> 
     52       <?cs var:xref.base.name ?><?cs 
     53      else ?> 
     54       <?cs var:xref.base.name ?> 
     55       <strong><?cs var:item.relation ?></strong> 
     56       <em><?cs var:item.name ?></em><?cs 
     57      /if ?> 
     58     </a> 
     59    </dt> 
     60    <dd><?cs var:item.context ?></dd><?cs 
     61   /each ?> 
     62  </dl> 
     63 <?cs /if ?> 
     64 
     65 <?cs if:$nlinks > #0 ?> 
     66  <?cs if:xref.direction.back ?> 
     67   <h2><?cs var:xref.base.name ?> is referenced in the following Trac Objects:</h2><?cs 
     68  else ?> 
     69   <h2><?cs var:xref.base.name ?> references the following Trac Objects:</h2><?cs 
     70  /if ?> 
     71  <dl><?cs  
     72   set:previous_name = "" ?><?cs 
     73   each:item = xref.links ?><?cs 
     74    if item.name != previous_name ?><?cs 
     75     set previous_name = item.name ?> 
     76     <dt class="<?cs var:item.icon ?>"><?cs call:anchor(item) ?></dt><?cs 
     77    /if ?> 
     78    <dd><?cs var:item.context ?>   <em>(in the <?cs var:item.facet?>)</em></dd><?cs 
     79   /each ?> 
     80  </dl> 
     81 <?cs /if ?> 
     82 
     83</div> 
     84 
     85<?cs include "footer.cs" ?> 
  • templates/orphans.cs

     
     1<?cs set:html.stylesheet = 'css/timeline.css' ?> 
     2<?cs include "header.cs" ?> 
     3<?cs include "macros.cs" ?> 
     4 
     5<div id="ctxtnav" class="nav"> 
     6 <h2>Xref Navigation</h2> 
     7 <ul> 
     8 </ul> 
     9 <hr /> 
     10</div> 
     11<div id="content" class="wiki"> 
     12 
     13 <?cs def:anchor(xref) ?> 
     14 <a href="<?cs var:xref.href ?>"><em><?cs var:xref.name ?></em></a><?cs  
     15 /def ?> 
     16 
     17 <h1><?cs var:len(orphans) ?> Orphaned Trac Objects</h1> 
     18  <p>The following objects are not referenced by any other objects</p> 
     19  <dl><?cs  
     20   set:previous_type = "" ?><?cs 
     21   each:item = orphans ?><?cs 
     22    if:item.type != previous_type ?><?cs 
     23     set:previous_type = item.type ?> 
     24     <h2>Orphaned <?cs var:item.type ?> objects:</h2><?cs 
     25    /if ?> 
     26    <dt class="<?cs var:item.icon ?>"><?cs call:anchor(item) ?></dt><?cs 
     27   /each ?> 
     28 </dl> 
     29 
     30</div> 
     31 
     32<?cs include "footer.cs" ?>