Edgewall Software

Ticket #1106: 1106-wiki-rename-r9352.patch

File 1106-wiki-rename-r9352.patch, 27.1 KB (added by rblank, 2 years ago)

Refactored and cleaned-up rename_simple.diff.

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

    diff --git a/trac/admin/tests/console-tests.txt b/trac/admin/tests/console-tests.txt
    a b  
    120120 TICKET_EDIT_CC, TICKET_EDIT_COMMENT, TICKET_EDIT_DESCRIPTION, 
    121121 TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, 
    122122 VERSIONCONTROL_ADMIN, WIKI_ADMIN, WIKI_CREATE, WIKI_DELETE, WIKI_MODIFY, 
    123  WIKI_VIEW 
     123 WIKI_RENAME, WIKI_VIEW 
    124124 
    125125===== test_permission_add_one_action_ok ===== 
    126126 
     
    155155 TICKET_EDIT_CC, TICKET_EDIT_COMMENT, TICKET_EDIT_DESCRIPTION, 
    156156 TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, 
    157157 VERSIONCONTROL_ADMIN, WIKI_ADMIN, WIKI_CREATE, WIKI_DELETE, WIKI_MODIFY, 
    158  WIKI_VIEW 
     158 WIKI_RENAME, WIKI_VIEW 
    159159 
    160160===== test_permission_add_multiple_actions_ok ===== 
    161161 
     
    191191 TICKET_EDIT_CC, TICKET_EDIT_COMMENT, TICKET_EDIT_DESCRIPTION, 
    192192 TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, 
    193193 VERSIONCONTROL_ADMIN, WIKI_ADMIN, WIKI_CREATE, WIKI_DELETE, WIKI_MODIFY, 
    194  WIKI_VIEW 
     194 WIKI_RENAME, WIKI_VIEW 
    195195 
    196196===== test_permission_remove_one_action_ok ===== 
    197197 
     
    225225 TICKET_EDIT_CC, TICKET_EDIT_COMMENT, TICKET_EDIT_DESCRIPTION, 
    226226 TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, 
    227227 VERSIONCONTROL_ADMIN, WIKI_ADMIN, WIKI_CREATE, WIKI_DELETE, WIKI_MODIFY, 
    228  WIKI_VIEW 
     228 WIKI_RENAME, WIKI_VIEW 
    229229 
    230230===== test_permission_remove_multiple_actions_ok ===== 
    231231 
     
    259259 TICKET_EDIT_CC, TICKET_EDIT_COMMENT, TICKET_EDIT_DESCRIPTION, 
    260260 TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, 
    261261 VERSIONCONTROL_ADMIN, WIKI_ADMIN, WIKI_CREATE, WIKI_DELETE, WIKI_MODIFY, 
    262  WIKI_VIEW 
     262 WIKI_RENAME, WIKI_VIEW 
    263263 
    264264===== test_component_list_ok ===== 
    265265 
  • trac/attachment.py

    diff --git a/trac/attachment.py b/trac/attachment.py
    a b  
    6363    def attachment_deleted(attachment): 
    6464        """Called when an attachment is deleted.""" 
    6565 
     66    def attachment_reparented(attachment, old_parent_realm, old_parent_id): 
     67        """Called when an attachment is reparented.""" 
     68 
    6669 
    6770class IAttachmentManipulator(Interface): 
    6871    """Extension point interface for components that need to manipulate 
     
    155158        self.author = row[4] 
    156159        self.ipnr = row[5] 
    157160 
    158     def _get_path(self): 
    159         path = os.path.join(self.env.path, 'attachments', self.parent_realm, 
    160                             unicode_quote(self.parent_id)) 
    161         if self.filename: 
    162             path = os.path.join(path, unicode_quote(self.filename)) 
     161    def _get_path(self, parent_realm, parent_id, filename): 
     162        path = os.path.join(self.env.path, 'attachments', parent_realm, 
     163                            unicode_quote(parent_id)) 
     164        if filename: 
     165            path = os.path.join(path, unicode_quote(filename)) 
    163166        return os.path.normpath(path) 
    164     path = property(_get_path) 
     167     
     168    @property 
     169    def path(self): 
     170        return self._get_path(self.parent_realm, self.parent_id, self.filename) 
    165171 
    166     def _get_title(self): 
    167         return '%s:%s: %s' % (self.parent_realm,  
    168                               self.parent_id, self.filename) 
    169     title = property(_get_title) 
     172    @property 
     173    def title(self): 
     174        return '%s:%s: %s' % (self.parent_realm, self.parent_id, self.filename) 
    170175 
    171176    def delete(self, db=None): 
    172177        assert self.filename, 'Cannot delete non-existent attachment' 
     
    192197        for listener in AttachmentModule(self.env).change_listeners: 
    193198            listener.attachment_deleted(self) 
    194199 
     200    def reparent(self, new_realm, new_id, db=None): 
     201        assert self.filename, 'Cannot reparent non-existent attachment' 
     202        new_id = unicode(new_id) 
     203         
     204        @with_transaction(self.env, db) 
     205        def do_reparent(db): 
     206            cursor = db.cursor() 
     207            new_path = self._get_path(new_realm, new_id, self.filename) 
     208            if os.path.exists(new_path): 
     209                raise TracError(_('Cannot reparent attachment "%(att)s" as ' 
     210                                  'it already exists in %(realm)s:%(id)s',  
     211                                  att=self.filename, realm=new_realm, 
     212                                  id=new_id)) 
     213            cursor.execute(""" 
     214                UPDATE attachment SET type=%s, id=%s 
     215                WHERE type=%s AND id=%s AND filename=%s 
     216                """, (new_realm, new_id, self.parent_realm, self.parent_id, 
     217                      self.filename)) 
     218            dirname = os.path.dirname(new_path) 
     219            if not os.path.exists(dirname): 
     220                os.makedirs(dirname) 
     221            if os.path.isfile(self.path): 
     222                try: 
     223                    os.rename(self.path, new_path) 
     224                except OSError, e: 
     225                    self.env.log.error('Failed to move attachment file %s: %s', 
     226                                       self.path, 
     227                                       exception_to_unicode(e, traceback=True)) 
     228                    raise TracError(_('Could not reparent attachment %(name)s', 
     229                                      name=self.filename)) 
     230 
     231        old_realm, old_id = self.parent_realm, self.parent_id 
     232        self.parent_realm, self.parent_id = new_realm, new_id 
     233        self.resource = Resource(new_realm, new_id).child('attachment', 
     234                                                          self.filename) 
     235         
     236        self.env.log.info('Attachment reparented: %s' % self.title) 
     237 
     238        for listener in AttachmentModule(self.env).change_listeners: 
     239            if hasattr(listener, 'attachment_reparented'): 
     240                listener.attachment_deleted(self, old_realm, old_id) 
    195241 
    196242    def insert(self, filename, fileobj, size, t=None, db=None): 
    197243        self.size = size and int(size) or 0 
     
    274320            except OSError, e: 
    275321                env.log.error("Can't delete attachment directory %s: %s", 
    276322                    attachment_dir, exception_to_unicode(e, traceback=True)) 
     323 
     324    @classmethod 
     325    def reparent_all(cls, env, parent_realm, parent_id, new_realm, new_id, 
     326                     db=None): 
     327        """Reparent all attachments of a given resource to another resource. 
     328        """ 
     329        attachment_dir = None 
     330        for attachment in list(cls.select(env, parent_realm, parent_id, db)): 
     331            attachment_dir = os.path.dirname(attachment.path) 
     332            attachment.reparent(new_realm, new_id, db) 
     333        if attachment_dir: 
     334            try: 
     335                os.rmdir(attachment_dir) 
     336            except OSError, e: 
     337                env.log.error("Can't delete attachment directory %s: %s", 
     338                    attachment_dir, exception_to_unicode(e, traceback=True)) 
    277339             
    278340    def open(self): 
    279341        self.env.log.debug('Trying to open attachment at %s', self.path) 
  • trac/htdocs/css/wiki.css

    diff --git a/trac/htdocs/css/wiki.css b/trac/htdocs/css/wiki.css
    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: 3em } 
    4646#preview { 
    4747 background: #f4f4f4 url(../draft.png); 
    4848 margin: 1em 0 2em; 
  • trac/tests/attachment.py

    diff --git a/trac/tests/attachment.py b/trac/tests/attachment.py
    a b  
    11# -*- coding: utf-8 -*- 
    22 
    3 import os 
     3import os.path 
    44import shutil 
     5from StringIO import StringIO 
    56import tempfile 
    67import unittest 
    78 
     
    7071 
    7172    def test_insert(self): 
    7273        attachment = Attachment(self.env, 'ticket', 42) 
    73         attachment.insert('foo.txt', tempfile.TemporaryFile(), 0, 1) 
     74        attachment.insert('foo.txt', StringIO(''), 0, 1) 
    7475        attachment = Attachment(self.env, 'ticket', 42) 
    75         attachment.insert('bar.jpg', tempfile.TemporaryFile(), 0, 2) 
     76        attachment.insert('bar.jpg', StringIO(''), 0, 2) 
    7677 
    7778        attachments = Attachment.select(self.env, 'ticket', 42) 
    7879        self.assertEqual('foo.txt', attachments.next().filename) 
     
    8182 
    8283    def test_insert_unique(self): 
    8384        attachment = Attachment(self.env, 'ticket', 42) 
    84         attachment.insert('foo.txt', tempfile.TemporaryFile(), 0) 
     85        attachment.insert('foo.txt', StringIO(''), 0) 
    8586        self.assertEqual('foo.txt', attachment.filename) 
    8687        attachment = Attachment(self.env, 'ticket', 42) 
    87         attachment.insert('foo.txt', tempfile.TemporaryFile(), 0) 
     88        attachment.insert('foo.txt', StringIO(''), 0) 
    8889        self.assertEqual('foo.2.txt', attachment.filename) 
    8990 
    9091    def test_insert_outside_attachments_dir(self): 
    9192        attachment = Attachment(self.env, '../../../../../sth/private', 42) 
    9293        self.assertRaises(AssertionError, attachment.insert, 'foo.txt', 
    93                           tempfile.TemporaryFile(), 0) 
     94                          StringIO(''), 0) 
    9495 
    9596    def test_delete(self): 
    9697        attachment1 = Attachment(self.env, 'wiki', 'SomePage') 
    97         attachment1.insert('foo.txt', tempfile.TemporaryFile(), 0) 
     98        attachment1.insert('foo.txt', StringIO(''), 0) 
    9899        attachment2 = Attachment(self.env, 'wiki', 'SomePage') 
    99         attachment2.insert('bar.jpg', tempfile.TemporaryFile(), 0) 
     100        attachment2.insert('bar.jpg', StringIO(''), 0) 
    100101 
    101102        attachments = Attachment.select(self.env, 'wiki', 'SomePage') 
    102103        self.assertEqual(2, len(list(attachments))) 
     
    116117        doesn't exist for some reason. 
    117118        """ 
    118119        attachment = Attachment(self.env, 'wiki', 'SomePage') 
    119         attachment.insert('foo.txt', tempfile.TemporaryFile(), 0) 
     120        attachment.insert('foo.txt', StringIO(''), 0) 
    120121        os.unlink(attachment.path) 
    121122 
    122123        attachment.delete() 
    123124 
     125    def test_reparent(self): 
     126        attachment1 = Attachment(self.env, 'wiki', 'SomePage') 
     127        attachment1.insert('foo.txt', StringIO(''), 0) 
     128        path1 = attachment1.path 
     129        attachment2 = Attachment(self.env, 'wiki', 'SomePage') 
     130        attachment2.insert('bar.jpg', StringIO(''), 0) 
     131 
     132        attachments = Attachment.select(self.env, 'wiki', 'SomePage') 
     133        self.assertEqual(2, len(list(attachments))) 
     134        attachments = Attachment.select(self.env, 'ticket', 123) 
     135        self.assertEqual(0, len(list(attachments))) 
     136        assert os.path.exists(path1) and os.path.exists(attachment2.path) 
     137 
     138        attachment1.reparent('ticket', 123) 
     139        self.assertEqual('ticket', attachment1.parent_realm) 
     140        self.assertEqual('ticket', attachment1.resource.parent.realm) 
     141        self.assertEqual('123', attachment1.parent_id) 
     142        self.assertEqual('123', attachment1.resource.parent.id) 
     143         
     144        attachments = Attachment.select(self.env, 'wiki', 'SomePage') 
     145        self.assertEqual(1, len(list(attachments))) 
     146        attachments = Attachment.select(self.env, 'ticket', 123) 
     147        self.assertEqual(1, len(list(attachments))) 
     148        assert not os.path.exists(path1) and os.path.exists(attachment1.path) 
     149        assert os.path.exists(attachment2.path) 
     150 
    124151    def test_legacy_permission_on_parent(self): 
    125152        """Ensure that legacy action tests are done on parent.  As 
    126153        `ATTACHMENT_VIEW` maps to `TICKET_VIEW`, the `TICKET_VIEW` is tested 
  • trac/tests/functional/tester.py

    diff --git a/trac/tests/functional/tester.py b/trac/tests/functional/tester.py
    a b  
    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.""" 
     
    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
    a b  
    4545    def wiki_page_version_deleted(page): 
    4646        """Called when a version of a page has been deleted.""" 
    4747 
     48    def wiki_page_renamed(page, old_name):  
     49        """Called when a page has been renamed."""  
     50 
    4851 
    4952class IWikiPageManipulator(Interface): 
    5053    """Extension point interface for components that need to do specific 
  • trac/wiki/model.py

    diff --git a/trac/wiki/model.py b/trac/wiki/model.py
    a b  
    158158        self.old_readonly = self.readonly 
    159159        self.old_text = self.text 
    160160 
     161    def rename(self, new_name, db=None): 
     162        """Rename wiki page in-place, keeping the history intact. 
     163        Renaming a page this way will eventually leave dangling references 
     164        to the old page - which litterally doesn't exist anymore. 
     165        """ 
     166        assert self.exists, 'Cannot rename non-existent page' 
     167 
     168        old_name = self.name 
     169         
     170        @with_transaction(self.env, db) 
     171        def do_rename(db): 
     172            cursor = db.cursor() 
     173            new_page = WikiPage(self.env, new_name, db=db) 
     174            if new_page.exists: 
     175                raise TracError(_("Can't rename to existing %(name)s page.", 
     176                                  name=new_name)) 
     177 
     178            cursor.execute("UPDATE wiki SET name=%s WHERE name=%s", 
     179                           (new_name, old_name)) 
     180            WikiSystem(self.env).pages.invalidate(db) 
     181            from trac.attachment import Attachment 
     182            Attachment.reparent_all(self.env, 'wiki', old_name, 
     183                                    'wiki', new_name, db) 
     184 
     185        self.name = new_name 
     186        self.env.log.info('Renamed page %s to %s', old_name, new_name) 
     187         
     188        for listener in WikiSystem(self.env).change_listeners: 
     189            if hasattr(listener, 'wiki_page_renamed'): 
     190                listener.wiki_page_renamed(self, old_name) 
     191 
    161192    def get_history(self, db=None): 
    162193        if not db: 
    163194            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
    - +  
     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" py:with="current_href = href.wiki(page.name)"> 
     14      <h1>Rename <a href="$current_href">$page.name</a></h1> 
     15      <form id="rename" action="$current_href" method="post"> 
     16        <p> 
     17          <input type="hidden" name="action" value="rename" /> 
     18          <strong>Renaming the page will rename all existing versions of the page in place.</strong><br /> 
     19          The complete history of the page will be moved to the new location. 
     20        </p> 
     21        <div class="field"> 
     22          <label>New name: <input type="text" id="new_name" name="new_name" size="40" value="$page.name" /></label> 
     23        </div> 
     24        <div class="field"> 
     25          <label><input type="checkbox" id="redirect" name="redirect"/> 
     26                 Leave a redirection page at the old location</label> 
     27        </div> 
     28        <div class="buttons"> 
     29          <input type="submit" name="cancel" value="${_('Cancel')}" /> 
     30          <input type="submit" name="submit" value="${_('Rename')}" /> 
     31        </div> 
     32      </form> 
     33    </div> 
     34  </body> 
     35</html> 
  • trac/wiki/templates/wiki_view.html

    diff --git a/trac/wiki/templates/wiki_view.html b/trac/wiki/templates/wiki_view.html
    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                <xi:include href="attach_file_form.html" py:with="alist = attachments"/> 
    102103              </py:if> 
    103104            </py:if> 
     105            <form method="get" action="${href.wiki(page.name)}" id="rename" py:if="page.exists and rename_perm">  
     106              <div>  
     107                <input type="hidden" name="action" value="rename" />  
     108                <input type="submit" value="${_('Rename page')}" />  
     109              </div>  
     110            </form>  
    104111            <py:if test="page.exists and delete_perm"> 
    105112              <form method="get" action="${href.wiki(page.name)}"> 
    106113                <div id="delete"> 
  • trac/wiki/tests/functional.py

    diff --git a/trac/wiki/tests/functional.py b/trac/wiki/tests/functional.py
    a b  
    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        attachment = self._tester.attach_file_to_wiki(pagename) 
     21        base_url = self._tester.url 
     22        page_url = base_url + "/wiki/" + pagename 
     23         
     24        def click_rename(): 
     25            tc.formvalue('rename', 'action', 'rename') 
     26            tc.submit() 
     27            tc.url(page_url + r'\?action=rename') 
     28            tc.find("New name:") 
     29         
     30        tc.go(page_url) 
     31        tc.find("Rename page") 
     32        click_rename() 
     33        # attempt to rename the page to the current page name        
     34        tc.formvalue('rename', 'new_name', pagename) 
     35        tc.submit('submit') 
     36        tc.url(page_url) 
     37        tc.find("New name must be different from old name") 
     38        # attempt to rename the page to an existing page name 
     39        tc.formvalue('rename', 'new_name', 'WikiStart') 
     40        tc.submit('submit') 
     41        tc.url(page_url) 
     42        tc.find("Trac Error") 
     43        tc.find("Can't rename to existing WikiStart page") 
     44        # correct rename to new page name (old page replaced by a redirection) 
     45        tc.go(page_url) 
     46        click_rename() 
     47        newpagename = pagename + 'Renamed' 
     48        tc.formvalue('rename', 'new_name', newpagename) 
     49        tc.formvalue('rename', 'redirect', True) 
     50        tc.submit('submit') 
     51        # check redirection page 
     52        tc.url(page_url) 
     53        tc.find("See.*/wiki/" + newpagename) 
     54        # check whether attachment exists on the new page but not on old page 
     55        tc.go(base_url + '/attachment/wiki/' + newpagename + '/' + attachment) 
     56        tc.notfind("Error: Invalid Attachment") 
     57        tc.go(base_url + '/attachment/wiki/' + pagename + '/' + attachment) 
     58        tc.find("Error: Invalid Attachment") 
     59        # rename again to another new page name (this time, no redirection) 
     60        tc.go(page_url) 
     61        click_rename() 
     62        newpagename = pagename + 'RenamedAgain' 
     63        tc.formvalue('rename', 'new_name', newpagename) 
     64        tc.formvalue('rename', 'redirect', False) 
     65        tc.submit('submit') 
     66        tc.url(base_url + "/wiki/" + newpagename) 
     67        # this time, the original page is gone 
     68        tc.go(page_url) 
     69        tc.url(page_url) 
     70        tc.find("The page %s does not exist" % pagename) 
     71 
     72 
    1573class RegressionTestTicket4812(FunctionalTwillTestCaseSetup): 
    1674    def runTest(self): 
    1775        """Test for regression of http://trac.edgewall.org/ticket/4812""" 
     
    65123        import trac.tests.functional.testcases 
    66124        suite = trac.tests.functional.testcases.functionalSuite() 
    67125    suite.addTest(TestWiki()) 
     126    suite.addTest(TestWikiRename()) 
    68127    suite.addTest(RegressionTestTicket4812()) 
    69128    if has_docutils: 
    70129        import docutils 
  • trac/wiki/tests/model.py

    diff --git a/trac/wiki/tests/model.py b/trac/wiki/tests/model.py
    a b  
     1# -*- coding: utf-8 -*- 
     2 
    13from datetime import datetime 
     4import os.path 
     5import shutil 
     6from StringIO import StringIO 
     7import tempfile 
    28import unittest 
    39 
     10from trac.attachment import Attachment 
    411from trac.core import * 
    512from trac.test import EnvironmentStub 
    613from trac.util.datefmt import utc, to_utimestamp 
     
    1421        self.changed = [] 
    1522        self.deleted = [] 
    1623        self.deleted_version = [] 
     24        self.renamed = [] 
    1725 
    1826    def wiki_page_added(self, page): 
    1927        self.added.append(page) 
     
    2735    def wiki_page_version_deleted(self, page): 
    2836        self.deleted_version.append(page) 
    2937 
     38    def wiki_page_renamed(self, page, old_name): 
     39        self.renamed.append((page, old_name)) 
     40 
    3041 
    3142class WikiPageTestCase(unittest.TestCase): 
    3243 
    3344    def setUp(self): 
    3445        self.env = EnvironmentStub() 
     46        self.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv') 
     47        os.mkdir(self.env.path) 
    3548        self.db = self.env.get_db_cnx() 
    3649 
    3750    def tearDown(self): 
     51        shutil.rmtree(self.env.path) 
    3852        self.env.reset_db() 
    3953 
    4054    def test_new_page(self): 
     
    194208        listener = TestWikiChangeListener(self.env) 
    195209        self.assertEqual(page, listener.deleted[0]) 
    196210 
     211    def test_rename_page(self): 
     212        cursor = self.db.cursor() 
     213        data = (1, 42, 'joe', '::1', 'Bla bla', 'Testing', 0) 
     214        cursor.execute("INSERT INTO wiki VALUES(%s,%s,%s,%s,%s,%s,%s,%s)", 
     215                       ('TestPage',) + data) 
     216        attachment = Attachment(self.env, 'wiki', 'TestPage') 
     217        attachment.insert('foo.txt', StringIO(), 0, 1) 
     218         
     219        page = WikiPage(self.env, 'TestPage') 
     220        page.rename('PageRenamed') 
     221        self.assertEqual('PageRenamed', page.name) 
     222         
     223        cursor.execute("SELECT version,time,author,ipnr,text,comment," 
     224                       "readonly FROM wiki WHERE name=%s", ('PageRenamed',)) 
     225        self.assertEqual(data, cursor.fetchone()) 
     226        self.assertEqual(None, cursor.fetchone()) 
     227         
     228        attachments = Attachment.select(self.env, 'wiki', 'PageRenamed') 
     229        self.assertEqual('foo.txt', attachments.next().filename) 
     230        self.assertRaises(StopIteration, attachments.next) 
     231        Attachment.delete_all(self.env, 'wiki', 'PageRenamed', self.db) 
     232 
     233        old_page = WikiPage(self.env, 'TestPage') 
     234        self.assertEqual(False, old_page.exists) 
     235         
     236        cursor.execute("SELECT version,time,author,ipnr,text,comment," 
     237                       "readonly FROM wiki WHERE name=%s", ('TestPage',)) 
     238        self.assertEqual(None, cursor.fetchone()) 
     239         
     240        listener = TestWikiChangeListener(self.env) 
     241        self.assertEqual((page, 'TestPage'), listener.renamed[0]) 
     242 
    197243 
    198244def suite(): 
    199245    return unittest.makeSuite(WikiPageTestCase, 'test') 
  • trac/wiki/web_ui.py

    diff --git a/trac/wiki/web_ui.py b/trac/wiki/web_ui.py
    a b  
    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', 
     97                   'WIKI_VIEW'] 
    9798        return actions + [('WIKI_ADMIN', actions)] 
    9899 
    99100    # IRequestHandler methods 
     
    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                style, options, diff_data = get_diff_options(req) 
    150153                contextall = diff_data['options']['contextall'] 
     
    153156                                           version=version, 
    154157                                           contextall=contextall or None)) 
    155158        elif action == 'delete': 
    156             return self._render_confirm(req, versioned_page) 
     159            return self._render_confirm_delete(req, versioned_page) 
     160        elif action == 'rename': 
     161            return self._render_confirm_rename(req, page) 
    157162        elif action == 'edit': 
    158163            return self._render_editor(req, versioned_page) 
    159164        elif action == 'diff': 
     
    248253        old_version = int(req.args.get('old_version', 0)) or version 
    249254 
    250255        @with_transaction(self.env) 
    251         def do_transaction(db): 
     256        def do_delete(db): 
    252257            if version and old_version and version > old_version: 
    253258                # delete from `old_version` exclusive to `version` inclusive: 
    254259                for v in range(old_version, version): 
     
    272277                                  version=version, name=page.name)) 
    273278            req.redirect(req.href.wiki(page.name)) 
    274279 
     280    def _do_rename(self, req, page): 
     281        if page.readonly: 
     282            req.perm(page.resource).require('WIKI_ADMIN') 
     283        else: 
     284            req.perm(page.resource).require('WIKI_RENAME') 
     285          
     286        if 'cancel' in req.args: 
     287            req.redirect(get_resource_url(self.env, page.resource, req.href)) 
     288          
     289        old_name, old_version = page.name, page.version 
     290        new_name = req.args.get('new_name', '').rstrip('/') 
     291        redirect = req.args.get('redirect') 
     292          
     293        # verify input parameters 
     294        warn = None 
     295        if not new_name: 
     296            warn = _('New name is mandatory for a rename.') 
     297        elif new_name == old_name: 
     298            warn = _('New name must be different from old name.') 
     299        if warn: 
     300            add_warning(req, warn) 
     301            return self._render_confirm_rename(req, page) 
     302 
     303        @with_transaction(self.env) 
     304        def do_rename(db): 
     305            page.rename(new_name, db) 
     306            if redirect: 
     307                redirection = WikiPage(self.env, old_name) 
     308                redirection.text = 'See [wiki:"%s"].' % new_name 
     309                author = get_reporter_id(req) 
     310                comment = '[wiki:"%s@%d" %s] was renamed to [wiki:"%s"].' % ( 
     311                          new_name, old_version, old_name, new_name) 
     312                redirection.save(author, comment, req.remote_addr, db=db) 
     313         
     314        req.redirect(req.href.wiki(redirect and old_name or new_name)) 
     315 
    275316    def _do_save(self, req, page): 
    276317        if page.readonly: 
    277318            req.perm(page.resource).require('WIKI_ADMIN') 
     
    296337            add_warning(req, _("Page not modified, showing latest version.")) 
    297338            return self._render_view(req, page) 
    298339 
    299     def _render_confirm(self, req, page): 
     340    def _render_confirm_delete(self, req, page): 
    300341        if page.readonly: 
    301342            req.perm(page.resource).require('WIKI_ADMIN') 
    302343        else: 
     
    321362        self._wiki_ctxtnav(req, page) 
    322363        return 'wiki_delete.html', data, None 
    323364 
     365    def _render_confirm_rename(self, req, page): 
     366        if page.readonly: 
     367            req.perm(page.resource).require('WIKI_ADMIN') 
     368        else: 
     369            req.perm(page.resource).require('WIKI_RENAME') 
     370            
     371        data = self._page_data(req, page, 'rename') 
     372        self._wiki_ctxtnav(req, page) 
     373        return 'wiki_rename.html', data, None 
     374         
    324375    def _render_diff(self, req, page): 
    325376        if not page.exists: 
    326377            raise TracError(_('Version %(num)s of page "%(name)s" does not '