Edgewall Software

Ticket #1106: rename_with_page_save.diff

File rename_with_page_save.diff, 29.2 KB (added by shookie@…, 2 years ago)

contains full rename, and implements urlmapping to avoid problems with renaming or deleting standard pages, on top of trunk

  • trac/admin/tests/console-tests.txt

    diff --git a/trac/admin/tests/console-tests.txt b/trac/admin/tests/console-tests.txt
    index e05f6f5..a3dcc00 100644
    a b Available actions: 
    115115 TICKET_EDIT_CC, TICKET_EDIT_COMMENT, TICKET_EDIT_DESCRIPTION, 
    116116 TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, 
    117117 VERSIONCONTROL_ADMIN, WIKI_ADMIN, WIKI_CREATE, WIKI_DELETE, WIKI_MODIFY, 
    118  WIKI_VIEW 
     118 WIKI_RENAME, WIKI_VIEW 
    119119 
    120120===== test_permission_add_one_action_ok ===== 
    121121 
    Available actions: 
    150150 TICKET_EDIT_CC, TICKET_EDIT_COMMENT, TICKET_EDIT_DESCRIPTION, 
    151151 TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, 
    152152 VERSIONCONTROL_ADMIN, WIKI_ADMIN, WIKI_CREATE, WIKI_DELETE, WIKI_MODIFY, 
    153  WIKI_VIEW 
     153 WIKI_RENAME, WIKI_VIEW 
    154154 
    155155===== test_permission_add_multiple_actions_ok ===== 
    156156 
    Available actions: 
    186186 TICKET_EDIT_CC, TICKET_EDIT_COMMENT, TICKET_EDIT_DESCRIPTION, 
    187187 TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, 
    188188 VERSIONCONTROL_ADMIN, WIKI_ADMIN, WIKI_CREATE, WIKI_DELETE, WIKI_MODIFY, 
    189  WIKI_VIEW 
     189 WIKI_RENAME, WIKI_VIEW 
    190190 
    191191===== test_permission_remove_one_action_ok ===== 
    192192 
    Available actions: 
    220220 TICKET_EDIT_CC, TICKET_EDIT_COMMENT, TICKET_EDIT_DESCRIPTION, 
    221221 TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, 
    222222 VERSIONCONTROL_ADMIN, WIKI_ADMIN, WIKI_CREATE, WIKI_DELETE, WIKI_MODIFY, 
    223  WIKI_VIEW 
     223 WIKI_RENAME, WIKI_VIEW 
    224224 
    225225===== test_permission_remove_multiple_actions_ok ===== 
    226226 
    Available actions: 
    254254 TICKET_EDIT_CC, TICKET_EDIT_COMMENT, TICKET_EDIT_DESCRIPTION, 
    255255 TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, 
    256256 VERSIONCONTROL_ADMIN, WIKI_ADMIN, WIKI_CREATE, WIKI_DELETE, WIKI_MODIFY, 
    257  WIKI_VIEW 
     257 WIKI_RENAME, WIKI_VIEW 
    258258 
    259259===== test_component_list_ok ===== 
    260260 
  • trac/attachment.py

    diff --git a/trac/attachment.py b/trac/attachment.py
    index 77bd09f..3de5389 100644
    a b class Attachment(object): 
    274274            except OSError, e: 
    275275                env.log.error("Can't delete attachment directory %s: %s", 
    276276                    attachment_dir, exception_to_unicode(e, traceback=True)) 
     277 
     278    @classmethod 
     279    def reparent_all(cls, env, old_realm, old_id, new_realm, new_id, db=None): 
     280        """Reparent all attachments of a given resource to another resource. 
     281         
     282        :param old_realm: old parent's realm 
     283        :param old_id: old parent's id 
     284        :param new_realm: new parent's realm 
     285        :param new_id: new parent's id 
     286        :param env: the environent 
     287        :param db: the database connection 
     288        """ 
     289        old_dir = os.path.join(os.path.normpath(env.path), 'attachments', 
     290                               old_realm, old_id) 
     291        if not os.path.exists(old_dir): 
     292            return 
     293 
     294        new_dir = os.path.join(os.path.normpath(env.path), 'attachments', 
     295                               new_realm, new_id) 
     296         
     297        @with_transaction(env, db) 
     298        def do_reparent(db): 
     299            cursor = db.cursor() 
     300            cursor.execute(""" 
     301                SELECT filename FROM attachment 
     302                WHERE type=%s AND id=%s 
     303                """, (old_realm, old_id)) 
     304             
     305            renames = [] 
     306            for filename, in cursor: 
     307                old_filename = os.path.join(old_dir, filename) 
     308                new_filename = os.path.join(new_dir, filename) 
     309                if os.path.exists(new_filename): 
     310                    raise TracError(_("Can't reparent attachment '%(att)s' as " 
     311                                      "it already exists in %(realm)s:%(id)s",  
     312                                      att=new_filename, 
     313                                      realm=new_realm, id=new_id)) 
     314                renames.append((old_filename, new_filename)) 
     315             
     316            try: 
     317                if not os.path.exists(new_dir): 
     318                    os.makedirs(new_dir) 
     319            except OSError, e: 
     320                env.log.error("Can't create attachment directory '%s'", 
     321                              new_dir, exc_info=True) 
     322                raise TracError(_("Can't create attachment folder for " 
     323                                  "%(realm)s:%(id)s: %(err)", 
     324                                  realm=new_realm, id=new_id, err=e)) 
     325             
     326            for old_filename, new_filename in renames: 
     327                try: 
     328                    os.rename(old_filename, new_filename) 
     329                except OSError, e: 
     330                    env.log.error("Can't move attachment from '%s' to '%s'", 
     331                                  old_filename, new_filename, exc_info=True) 
     332                    raise TracError(_("Can't move attachment '%(att)s'", 
     333                                      att=os.path.basename(new_filename))) 
     334            cursor.execute(""" 
     335                UPDATE attachment SET type=%s, id=%s 
     336                WHERE type=%s AND id=%s 
     337                """, (new_realm, new_id, old_realm, old_id)) 
     338             
     339        env.log.info('Attachments reparented to: %s:%s', new_realm, new_id) 
     340 
     341         
     342        return 
    277343             
    278344    def open(self): 
    279345        self.env.log.debug('Trying to open attachment at %s', self.path) 
  • trac/db_default.py

    diff --git a/trac/db_default.py b/trac/db_default.py
    index 2c9e1a6..7d57acb 100644
    a b schema = [ 
    3838    Table('system', key='name')[ 
    3939        Column('name'), 
    4040        Column('value')], 
     41    Table('urlmap', key='key')[ 
     42        Column('key'), 
     43        Column('value')], 
    4144    Table('permission', key=('username', 'action'))[ 
    4245        Column('username'), 
    4346        Column('action')], 
    def get_data(db): 
    394397              ('name', 'value'), 
    395398                (('database_version', str(db_version)), 
    396399                 ('initial_database_version', str(db_version)))), 
     400            ('urlmap', 
     401              ('key', 'value'), 
     402                (('WikiStart', 'WikiStart'), 
     403                 ('TitleIndex', 'TitleIndex'), 
     404                 ('TracGuide', 'TracGuide'), 
     405                 ('TracInstall', 'TracInstall'), 
     406                 ('InterMapTxt', 'InterMapTxt'))), 
    397407            ('report', 
    398408              ('author', 'title', 'query', 'description'), 
    399409                __mkreports(get_reports(db)))) 
  • trac/env.py

    diff --git a/trac/env.py b/trac/env.py
    index 50babb3..0ec5b47 100644
    a b class Environment(Component, ComponentManager): 
    384384        row = cursor.fetchone() 
    385385        return row and int(row[0]) 
    386386 
     387    def get_urlmap(self, key, db=None, reverse=False, keycheck=False): 
     388        """Return the real path of some fixed pages trac expects, 
     389        but cannot rely on existing because 
     390        renaming and deleting pages is possible. 
     391        """ 
     392        if not db: 
     393            db = self.get_db_cnx() 
     394        cursor = db.cursor() 
     395        k, v = ('key', 'value') 
     396        if keycheck: 
     397            k, v = k, k 
     398        if reverse: 
     399            k, v = v, k 
     400        q = "SELECT %s FROM urlmap WHERE %s=" % (v, k) 
     401        cursor.execute(q + "%s", (key,)) 
     402        row = cursor.fetchone() 
     403        return row and row[0] 
     404 
    387405    def setup_config(self, load_defaults=False): 
    388406        """Load the configuration file.""" 
    389407        self.config = Configuration(os.path.join(self.path, 'conf', 
  • trac/htdocs/css/wiki.css

    diff --git a/trac/htdocs/css/wiki.css b/trac/htdocs/css/wiki.css
    index 091f2f6..f109783 100644
    a b  
    4242#changeinfo br { clear: left } 
    4343#changeinfo .options { padding: 0 0 1em 1em } 
    4444#changeinfo .options, #changeinfo .buttons { clear: left } 
    45 #delete, #save { margin-left: 6em } 
     45#delete, #rename, #save { margin-left: 6em } 
    4646#preview { 
    4747 background: #f4f4f4 url(../draft.png); 
    4848 margin: 1em 0 2em; 
  • trac/tests/functional/tester.py

    diff --git a/trac/tests/functional/tester.py b/trac/tests/functional/tester.py
    index b983e1b..24a06e5 100755
    a b class FunctionalTester(object): 
    176176            tc.formvalue('attachment', 'replace', True) 
    177177        tc.submit() 
    178178        tc.url(self.url + '/attachment/ticket/%s/$' % ticketid) 
     179        return tempfilename 
    179180 
    180181    def clone_ticket(self, ticketid): 
    181182        """Create a clone of the given ticket id using the clone button.""" 
    class FunctionalTester(object): 
    232233        tc.formvalue('attachment', 'description', random_sentence()) 
    233234        tc.submit() 
    234235        tc.url(self.url + '/attachment/wiki/%s/$' % name) 
     236        return tempfilename 
    235237 
    236238    def create_milestone(self, name=None, due=None): 
    237239        """Creates the specified milestone, with a random name if none is 
  • trac/wiki/api.py

    diff --git a/trac/wiki/api.py b/trac/wiki/api.py
    index 69dbed9..eaba52d 100644
    a b class IWikiChangeListener(Interface): 
    4242    def wiki_page_deleted(page): 
    4343        """Called when a page has been deleted.""" 
    4444 
     45    def wiki_page_renamed(page,old_page_name):  
     46        """Called when a page has been renamed in-place."""  
     47 
    4548    def wiki_page_version_deleted(page): 
    4649        """Called when a version of a page has been deleted.""" 
    4750 
    class WikiSystem(Component): 
    290293            pagename, version = pagename.split('@', 1) 
    291294        if version and query: 
    292295            query = '&' + query[1:] 
    293         pagename = pagename.rstrip('/') or 'WikiStart' 
     296        pagename = pagename.rstrip('/') or self.env.get_urlmap('WikiStart') 
    294297        referrer = '' 
    295298        if formatter.resource and formatter.resource.realm == 'wiki': 
    296299            referrer = formatter.resource.id 
  • trac/wiki/model.py

    diff --git a/trac/wiki/model.py b/trac/wiki/model.py
    index 6dc95b5..d302dc8 100644
    a b class WikiPage(object): 
    9191            if version is None: 
    9292                # Delete a wiki page completely 
    9393                cursor.execute("DELETE FROM wiki WHERE name=%s", (self.name,)) 
     94                mapped = self.env.get_urlmap(self.name, db=db, reverse=True) 
     95                if mapped: 
     96                    cursor.execute("""UPDATE urlmap SET value=NULL 
     97                                   WHERE key=%s""", (mapped,)) 
     98                 
    9499                self.env.log.info('Deleted page %s' % self.name) 
    95100            else: 
    96101                # Delete only a specific page version 
    class WikiPage(object): 
    135140                    """, (self.name, self.version + 1, to_utimestamp(t), 
    136141                          author, remote_addr, self.text, comment, 
    137142                          self.readonly)) 
     143                mappable = self.env.get_urlmap(self.name, db=db, keycheck=True) 
     144                if mappable: 
     145                    cursor.execute("""UPDATE urlmap SET value=%s 
     146                                   WHERE key=%s""", (self.name, self.name)) 
    138147                self.version += 1 
    139148                self.resource = self.resource(version=self.version) 
    140149            else: 
    class WikiPage(object): 
    158167        self.old_readonly = self.readonly 
    159168        self.old_text = self.text 
    160169 
     170    def rename(self, new_name, db=None): 
     171        """Rename wiki page in-place, keeping the history intact. 
     172        Renaming a page this way will eventually leave dangling references 
     173        to the old page - which litterally doesn't exist anymore. 
     174        """ 
     175        assert self.exists, 'Cannot rename non-existent page' 
     176 
     177        old_name = self.name 
     178         
     179        @with_transaction(self.env, db) 
     180        def do_rename(db): 
     181            cursor = db.cursor() 
     182            new_page = WikiPage(self.env, new_name, version=None, db=db) 
     183            if new_page.exists: 
     184                raise TracError(_("Can't rename to existing %(nn)s page.", 
     185                                  nn=new_name)) 
     186 
     187            cursor.execute("UPDATE wiki SET name=%s WHERE name=%s", 
     188                           (new_name, old_name)) 
     189            mapped = self.env.get_urlmap(old_name, db=db, reverse=True) 
     190            mappable = self.env.get_urlmap(new_name, db=db, keycheck=True) 
     191             
     192            if mapped or mappable: 
     193                cursor.execute("""UPDATE urlmap SET value=%s 
     194                               WHERE key=%s""", (new_name, mapped or mappable)) 
     195            self.name = new_name 
     196            self._fetch(self.name, None, db) 
     197 
     198            from trac.attachment import Attachment 
     199            Attachment.reparent_all(self.env, 'wiki', old_name, 
     200                                    'wiki', self.name, db) 
     201 
     202        self.env.log.info('Renamed page %s in-place to %s', old_name, new_name) 
     203         
     204        for listener in WikiSystem(self.env).change_listeners: 
     205            if hasattr(listener, 'wiki_page_renamed'): 
     206                listener.wiki_page_renamed(self, old_name) 
     207 
     208 
    161209    def get_history(self, db=None): 
    162210        if not db: 
    163211            db = self.env.get_db_cnx() 
  • new file trac/wiki/templates/wiki_rename.html

    diff --git a/trac/wiki/templates/wiki_rename.html b/trac/wiki/templates/wiki_rename.html
    new file mode 100644
    index 0000000..f00b807
    - +  
     1<!DOCTYPE html 
     2    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
     3    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
     4<html xmlns="http://www.w3.org/1999/xhtml" 
     5      xmlns:py="http://genshi.edgewall.org/" 
     6      xmlns:xi="http://www.w3.org/2001/XInclude"> 
     7  <xi:include href="layout.html" /> 
     8  <head> 
     9    <title>$title</title> 
     10  </head> 
     11 
     12  <body> 
     13    <div id="content" class="wiki" 
     14      py:with=" 
     15      current_href = href.wiki(page.name); 
     16     "> 
     17      <h1>${_('Rename')} <a href="$current_href">$page.name</a></h1> 
     18      <form action="$current_href" method="post"> 
     19        <p> 
     20          <input type="hidden" name="action" value="rename" /> 
     21          <strong>Renaming the page will rename all existing versions 
     22          of the page in place. <br /> 
     23          The history of the renamed page will be exactly the same as the one 
     24          of the original page.</strong> 
     25          <br /> 
     26          <p> 
     27            <label for="new_name">New name:</label> 
     28            <input type="text" id="new_name" name="new_name" size="40" value="$page.name" /> 
     29          </p> 
     30          <br /> 
     31          <fieldset id="rename_options" class="group"> 
     32            <legend>${_('Rename')} ${_('Options')}</legend> 
     33            <input type="checkbox" id="leave_redirection" name="leave_redirection"/> 
     34            <label for="leave_redirection">Leave a redirection to the new page.</label> 
     35            <br /> 
     36            If you choose to leave a redirection, this will  
     37            re-create the $page.name page with a link pointing to the new 
     38            page. 
     39          </fieldset> 
     40       </p> 
     41        <div class="buttons"> 
     42          <input type="submit" name="cancel" value="${_('Cancel')}" /> 
     43          <input type="submit" value="${_('Rename')} $page.name" /> 
     44        </div> 
     45      </form> 
     46    </div> 
     47  </body> 
     48</html> 
  • trac/wiki/templates/wiki_view.html

    diff --git a/trac/wiki/templates/wiki_view.html b/trac/wiki/templates/wiki_view.html
    index 0715fbe..17880bd 100644
    a b  
    7171 
    7272      <py:with vars="modify_perm = 'WIKI_MODIFY' in perm(page.resource); 
    7373                     delete_perm = 'WIKI_DELETE' in perm(page.resource); 
    74                      admin_perm = 'WIKI_ADMIN' in perm(page.resource)"> 
     74                     admin_perm = 'WIKI_ADMIN' in perm(page.resource); 
     75                           rename_perm = 'WIKI_RENAME' in perm(page.resource)"> 
    7576        <py:if test="admin_perm or (not page.readonly and (modify_perm or delete_perm))"> 
    7677          <div class="buttons"> 
    7778            <py:if test="modify_perm"> 
     
    101102                ${attach_file_form(attachments)} 
    102103              </py:if> 
    103104            </py:if> 
     105            <py:if test="page.exists and rename_perm">  
     106                     <form method="get" action="${href.wiki(page.name)}" id="rename">  
     107                        <div>  
     108                          <input type="hidden" name="action" value="rename" />  
     109                          <input type="submit" value="${_('Rename page')}" />  
     110                        </div>  
     111                     </form>  
     112                  </py:if>  
    104113            <py:if test="page.exists and delete_perm"> 
    105114              <form method="get" action="${href.wiki(page.name)}"> 
    106115                <div id="delete"> 
  • trac/wiki/tests/functional.py

    diff --git a/trac/wiki/tests/functional.py b/trac/wiki/tests/functional.py
    index bd85ec8..07a290e 100755
    a b class TestWiki(FunctionalTwillTestCaseSetup): 
    1212        self._tester.attach_file_to_wiki(pagename) 
    1313 
    1414 
     15class TestWikiRename(FunctionalTwillTestCaseSetup): 
     16    def runTest(self): 
     17        """Test for simple wiki rename""" 
     18        pagename = random_unique_camel() 
     19        self._tester.create_wiki_page(pagename) 
     20        self._tester.rename_wiki_page(pagename) 
     21        attachment = self._tester.attach_file_to_wiki(pagename) 
     22        base_url = self._tester.url 
     23        page_url = base_url + "/wiki/" + pagename 
     24         
     25        def click_rename(): 
     26            tc.formvalue('rename', 'action', 'rename') 
     27            tc.submit() 
     28            tc.url(page_url+r'\?action=rename') 
     29            tc.find("New name:") 
     30         
     31        tc.go(page_url) 
     32        tc.find("Rename page") 
     33        click_rename() 
     34        # attempt to rename the page to the current page name ...         
     35        tc.formvalue(page_url, 'new_name', 'rename') 
     36        tc.submit('rename') 
     37        tc.url(page_url) 
     38        tc.find("New name must be different from old name.") 
     39        # attempt to rename the page to an existing page name ... 
     40        tc.formvalue(page_url, 'new_name', 'WikiStart') 
     41        tc.submit('rename') 
     42        tc.url(page_url) 
     43        tc.find("Trac Error") 
     44        tc.find("Can't rename to existing WikiStart page") 
     45        # correct rename to new page name (old page replaced by a redirection) 
     46        tc.go(page_url) 
     47        click_rename() 
     48        newpagename = pagename+'Renamed' 
     49        tc.formvalue(page_url, 'new_name', newpagename) 
     50        tc.formvalue(page_url, 'leave_redirection', True) # the default 
     51        tc.submit('rename') 
     52        # check redirection page 
     53        tc.url(page_url) 
     54        tc.find("See.*/wiki/"+newpagename) 
     55        # check whether attachment exists on the new page but not on old page 
     56        tc.go(base_url+'/attachment/wiki/'+newpagename+'/'+attachment) 
     57        tc.notfind("Error: Invalid Attachment") 
     58        tc.go(base_url+'/attachment/wiki/'+pagename+'/'+attachment) 
     59        tc.find("Error: Invalid Attachment") 
     60        # rename again to another new page name (this time, no redirection) 
     61        tc.go(page_url) 
     62        click_rename() 
     63        newpagename = pagename+'RenamedAgain' 
     64        tc.formvalue(page_url, 'new_name', newpagename) 
     65        tc.formvalue(page_url, 'leave_redirection', False) 
     66        tc.submit('rename') 
     67        tc.url(base_url + "/wiki/" + newpagename) 
     68        # this time, the original page is gone 
     69        tc.go(page_url) 
     70        tc.url(page_url) 
     71        tc.find("Describe %s here" % pagename) 
     72         
     73 
     74 
    1575class RegressionTestTicket4812(FunctionalTwillTestCaseSetup): 
    1676    def runTest(self): 
    1777        """Test for regression of http://trac.edgewall.org/ticket/4812""" 
  • trac/wiki/tests/model.py

    diff --git a/trac/wiki/tests/model.py b/trac/wiki/tests/model.py
    index e4fde47..13f84b7 100644
    a b  
    11from datetime import datetime 
     2import tempfile 
    23import unittest 
    34 
     5from trac.attachment import Attachment 
    46from trac.core import * 
    57from trac.test import EnvironmentStub 
    68from trac.util.datefmt import utc, to_utimestamp 
    class TestWikiChangeListener(Component): 
    1416        self.changed = [] 
    1517        self.deleted = [] 
    1618        self.deleted_version = [] 
     19        self.renamed = {} 
    1720 
    1821    def wiki_page_added(self, page): 
    1922        self.added.append(page) 
    class TestWikiChangeListener(Component): 
    2730    def wiki_page_version_deleted(self, page): 
    2831        self.deleted_version.append(page) 
    2932 
     33    def wiki_page_renamed(self, page, old_page_name): 
     34        self.renamed[old_page_name] = page 
     35 
    3036 
    3137class WikiPageTestCase(unittest.TestCase): 
    3238 
    class WikiPageTestCase(unittest.TestCase): 
    194200        listener = TestWikiChangeListener(self.env) 
    195201        self.assertEqual(page, listener.deleted[0]) 
    196202 
     203    def test_rename_page(self): 
     204        cursor = self.db.cursor() 
     205        data = (1, 42, 'joe', '::1', 'Bla bla', 'Testing', 0) 
     206        cursor.execute("INSERT INTO wiki VALUES(%s,%s,%s,%s,%s,%s,%s,%s)", 
     207                       ('TestPage',) + data) 
     208         
     209        page = WikiPage(self.env, 'TestPage') 
     210 
     211        attachment = Attachment(self.env, 'wiki', 'TestPage') 
     212        attachment.insert('foo.txt', tempfile.TemporaryFile(), 0, 1) 
     213 
     214        page.rename('PageRenamed') 
     215         
     216        cursor.execute("SELECT version,time,author,ipnr,text,comment," 
     217                       "readonly FROM wiki WHERE name=%s", ('PageRenamed',)) 
     218        self.assertEqual(data, cursor.fetchone()) 
     219        self.assertEqual(None, cursor.fetchone()) 
     220         
     221        attachments = Attachment.select(self.env, 'wiki', 'PageRenamed') 
     222        self.assertEqual('foo.txt', attachments.next().filename) 
     223        self.assertRaises(StopIteration, attachments.next) 
     224        Attachment.delete_all(self.env, 'wiki', 'PageRenamed', self.db) 
     225 
     226 
     227        old_page = WikiPage(self.env, 'TestPage') 
     228         
     229        cursor.execute("SELECT version,time,author,ipnr,text,comment," 
     230                       "readonly FROM wiki WHERE name=%s", ('TestPage',)) 
     231        self.assertEqual(None, cursor.fetchone()) 
     232         
     233        listener = TestWikiChangeListener(self.env) 
     234        self.assertEqual({'TestPage': page}, listener.renamed) 
     235 
     236    def test_sys_pages(self): 
     237        cursor = self.db.cursor() 
     238        cursor.execute("INSERT INTO urlmap VALUES(%s, NULL)", 
     239                       ('WikiStart',)) 
     240 
     241        page = WikiPage(self.env) 
     242        page.name = 'WikiStart' 
     243        page.text = 'Bla bla' 
     244        t = datetime(2001, 1, 1, 1, 1, 1, 0, utc) 
     245        page.save('joe', 'Testing', '::1', t) 
     246         
     247        cursor.execute("SELECT key, value FROM urlmap WHERE key=%s", ('WikiStart',)) 
     248        self.assertEqual(('WikiStart', 'WikiStart'), cursor.fetchone()) 
     249         
     250        page.rename('Home') 
     251        cursor.execute("SELECT key, value FROM urlmap WHERE key=%s", ('WikiStart',)) 
     252        self.assertEqual(('WikiStart', 'Home'), cursor.fetchone()) 
     253         
     254        page.delete() 
     255        cursor.execute("SELECT key, value FROM urlmap WHERE key=%s", ('WikiStart',)) 
     256        self.assertEqual(('WikiStart', None), cursor.fetchone()) 
    197257 
    198258def suite(): 
    199259    return unittest.makeSuite(WikiPageTestCase, 'test') 
  • trac/wiki/web_ui.py

    diff --git a/trac/wiki/web_ui.py b/trac/wiki/web_ui.py
    index 6e3bcde..7bcda70 100644
    a b class WikiModule(Component): 
    9393    # IPermissionRequestor methods 
    9494 
    9595    def get_permission_actions(self): 
    96         actions = ['WIKI_CREATE', 'WIKI_DELETE', 'WIKI_MODIFY', 'WIKI_VIEW'] 
     96        actions = ['WIKI_CREATE', 'WIKI_DELETE', 'WIKI_MODIFY', 'WIKI_RENAME', 'WIKI_VIEW'] 
    9797        return actions + [('WIKI_ADMIN', actions)] 
    9898 
    9999    # IRequestHandler methods 
    class WikiModule(Component): 
    107107 
    108108    def process_request(self, req): 
    109109        action = req.args.get('action', 'view') 
    110         pagename = req.args.get('page', 'WikiStart') 
     110        pagename = req.args.get('page', self.env.get_urlmap('WikiStart')) 
     111        pagename = pagename or 'WikiStart' 
    111112        version = req.args.get('version') 
    112113        old_version = req.args.get('old_version') 
    113114 
    class WikiModule(Component): 
    145146                    return self._render_editor(req, page, action, has_collision) 
    146147            elif action == 'delete': 
    147148                self._do_delete(req, versioned_page) 
     149            elif action == 'rename': 
     150                return self._do_rename(req, page) 
    148151            elif action == 'diff': 
    149152                get_diff_options(req) 
    150153                req.redirect(req.href.wiki(versioned_page.name, action='diff', 
    151154                                           old_version=old_version)) 
    152155        elif action == 'delete': 
    153             return self._render_confirm(req, versioned_page) 
     156            return self._render_confirm_delete(req, versioned_page) 
     157        elif action == 'rename': 
     158            return self._render_confirm_rename(req, page) 
    154159        elif action == 'edit': 
    155160            return self._render_editor(req, versioned_page) 
    156161        elif action == 'diff': 
    class WikiModule(Component): 
    245250        old_version = int(req.args.get('old_version', 0)) or version 
    246251 
    247252        @with_transaction(self.env) 
    248         def do_transaction(db): 
     253        def do_delete(db): 
    249254            if version and old_version and version > old_version: 
    250255                # delete from `old_version` exclusive to `version` inclusive: 
    251256                for v in range(old_version, version): 
    class WikiModule(Component): 
    269274                                  version=version, name=page.name)) 
    270275            req.redirect(req.href.wiki(page.name)) 
    271276 
     277    def _do_rename(self, req, page): 
     278        if page.readonly: 
     279            req.perm(page.resource).require('WIKI_ADMIN') 
     280        else: 
     281            req.perm(page.resource).require('WIKI_RENAME') 
     282          
     283        if 'cancel' in req.args: 
     284            req.redirect(get_resource_url(self.env, page.resource, req.href)) 
     285          
     286        new_name = req.args.get('new_name', '').rstrip('/') 
     287        old_name = page.name 
     288        old_version = page.version 
     289        leave_redirection = req.args.get('leave_redirection') 
     290          
     291        # verify input parameters 
     292        warn = None 
     293        if not new_name: 
     294            warn = _("New name is mandatory for a rename.") 
     295        elif new_name == old_name: 
     296            warn = _("New name must be different from old name.") 
     297        if warn: 
     298            add_warning(req, warn) 
     299            return self._render_confirm_rename(req, page) 
     300 
     301        @with_transaction(self.env) 
     302        def do_rename(db): 
     303            page.rename(new_name, db) 
     304            if leave_redirection: 
     305                redirection_page = WikiPage(self.env, old_name) 
     306                redirection_page.text = _("See [wiki:%(page)s].", 
     307                                          page=new_name) 
     308                author = get_reporter_id(req) 
     309                comment = _("[wiki:'%(new_name)s@%(old_version)d' " 
     310                            "%(old_name)s] was renamed to %(new_name)s", 
     311                            old_name=old_name, old_version=old_version, 
     312                            new_name=new_name) 
     313                redirection_page.save(author, comment, req.remote_addr, 
     314                                      None, db) 
     315         
     316        req.redirect(req.href.wiki(leave_redirection and old_name or new_name)) 
     317 
     318         
     319 
    272320    def _do_save(self, req, page): 
    273321        if page.readonly: 
    274322            req.perm(page.resource).require('WIKI_ADMIN') 
    class WikiModule(Component): 
    293341            add_warning(req, _("Page not modified, showing latest version.")) 
    294342            return self._render_view(req, page) 
    295343 
    296     def _render_confirm(self, req, page): 
     344    def _render_confirm_delete(self, req, page): 
    297345        if page.readonly: 
    298346            req.perm(page.resource).require('WIKI_ADMIN') 
    299347        else: 
    class WikiModule(Component): 
    315363                    break 
    316364            data.update({'new_version': version, 'old_version': old_version, 
    317365                         'num_versions': num_versions}) 
     366        mapped = self.env.get_urlmap(page.name, reverse=True) 
     367        if mapped: 
     368            add_warning(req, _("""'%s' is the '%s' trac system page. 
     369                               Really delete?""") % (page.name, mapped)) 
    318370        self._wiki_ctxtnav(req, page) 
    319371        return 'wiki_delete.html', data, None 
    320372 
     373    def _render_confirm_rename(self, req, page): 
     374        if page.readonly: 
     375            req.perm(page.resource).require('WIKI_ADMIN') 
     376        else: 
     377            req.perm(page.resource).require('WIKI_RENAME') 
     378            
     379            data = self._page_data(req, page, 'rename') 
     380            mapped = self.env.get_urlmap(page.name, reverse=True) 
     381            if mapped: 
     382                add_warning(req, _("""'%s' is the '%s' trac system page. 
     383                                   Really rename?""") % (page.name, mapped)) 
     384            self._wiki_ctxtnav(req, page) 
     385            return 'wiki_rename.html', data, None 
     386         
     387 
    321388    def _render_diff(self, req, page): 
    322389        if not page.exists: 
    323390            raise TracError(_('Version %(num)s of page "%(name)s" does not ' 
    class WikiModule(Component): 
    512579                         conversion[3]) 
    513580 
    514581        data = self._page_data(req, page) 
    515         if page.name == 'WikiStart': 
     582        if page.name == self.env.get_urlmap('WikiStart'): 
    516583            data['title'] = '' 
    517584 
    518585        ws = WikiSystem(self.env) 
    class WikiModule(Component): 
    606673     
    607674    def _wiki_ctxtnav(self, req, page): 
    608675        """Add the normal wiki ctxtnav entries.""" 
    609         add_ctxtnav(req, _('Start Page'), req.href.wiki('WikiStart')) 
    610         add_ctxtnav(req, _('Index'), req.href.wiki('TitleIndex')) 
     676        wikistart = self.env.get_urlmap('WikiStart') 
     677        titleindex = self.env.get_urlmap('TitleIndex')         
     678        if wikistart: 
     679            add_ctxtnav(req, _('Start Page'), req.href.wiki(wikistart)) 
     680        if titleindex: 
     681            add_ctxtnav(req, _('Index'), req.href.wiki(titleindex)) 
    611682        if page.exists: 
    612683            add_ctxtnav(req, _('History'), req.href.wiki(page.name,  
    613684                                                         action='history'))