From b4d9ca8875898500ca198bff501dfc7d1c28bd71 Mon Sep 17 00:00:00 2001
From: Jean-Philippe Lang <jp_lang@yahoo.fr>
Date: Sun, 9 Sep 2007 17:05:38 +0000
Subject: Added the ability to rename wiki pages (specific permission
 required).

Existing links that point to the old page are preserved and automatically redirected to the new page (this behaviour can be disabled when renaming the page).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@720 e93f8b46-1217-0410-a6f0-8f06a7374b81
---
 app/controllers/wiki_controller.rb      | 12 ++++++
 app/models/wiki.rb                      | 15 +++++--
 app/models/wiki_page.rb                 | 30 +++++++++++++-
 app/models/wiki_redirect.rb             | 23 +++++++++++
 app/views/wiki/rename.rhtml             | 11 +++++
 app/views/wiki/show.rhtml               |  5 ++-
 db/migrate/067_create_wiki_redirects.rb | 15 +++++++
 lang/bg.yml                             |  2 +
 lang/de.yml                             |  2 +
 lang/en.yml                             |  2 +
 lang/es.yml                             |  2 +
 lang/fr.yml                             |  2 +
 lang/it.yml                             |  2 +
 lang/ja.yml                             |  2 +
 lang/nl.yml                             |  2 +
 lang/pt-br.yml                          |  2 +
 lang/pt.yml                             |  2 +
 lang/sv.yml                             |  2 +
 lang/zh.yml                             |  2 +
 lib/redmine.rb                          |  1 +
 test/unit/wiki_redirect_test.rb         | 73 +++++++++++++++++++++++++++++++++
 21 files changed, 201 insertions(+), 8 deletions(-)
 create mode 100644 app/models/wiki_redirect.rb
 create mode 100644 app/views/wiki/rename.rhtml
 create mode 100644 db/migrate/067_create_wiki_redirects.rb
 create mode 100644 test/unit/wiki_redirect_test.rb

diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb
index e9212a1c7..95f792c5e 100644
--- a/app/controllers/wiki_controller.rb
+++ b/app/controllers/wiki_controller.rb
@@ -75,6 +75,18 @@ class WikiController < ApplicationController
     flash[:error] = l(:notice_locking_conflict)
   end
   
+  # rename a page
+  def rename
+    @page = @wiki.find_page(params[:page])    
+    @page.redirect_existing_links = true
+    # used to display the *original* title if some AR validation errors occur
+    @original_title = @page.pretty_title
+    if request.post? && @page.update_attributes(params[:wiki_page])
+      flash[:notice] = l(:notice_successful_update)
+      redirect_to :action => 'index', :id => @project, :page => @page.title
+    end
+  end
+  
   # show page history
   def history
     @page = @wiki.find_page(params[:page])
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index ed473c7c0..b6cd661ff 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -18,6 +18,7 @@
 class Wiki < ActiveRecord::Base
   belongs_to :project
   has_many :pages, :class_name => 'WikiPage', :dependent => :destroy
+  has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
   
   validates_presence_of :start_page
   validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
@@ -25,14 +26,20 @@ class Wiki < ActiveRecord::Base
   # find the page with the given title
   # if page doesn't exist, return a new page
   def find_or_new_page(title)
-    title = Wiki.titleize(title || start_page)
-    find_page(title) || WikiPage.new(:wiki => self, :title => title)  
+    find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
   end
   
   # find the page with the given title
-  def find_page(title)
+  def find_page(title, options = {})
     title = start_page if title.blank?
-    pages.find_by_title(Wiki.titleize(title))
+    title = Wiki.titleize(title)
+    page = pages.find_by_title(title)
+    if !page && !(options[:with_redirect] == false)
+      # search for a redirect
+      redirect = redirects.find_by_title(title)
+      page = find_page(redirect.redirects_to, :with_redirect => false) if redirect
+    end
+    page
   end
   
   # turn a string into a valid page title
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 074d36daa..1ef8b7db4 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -22,13 +22,39 @@ class WikiPage < ActiveRecord::Base
   has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
   has_many :attachments, :as => :container, :dependent => :destroy
   
+  attr_accessor :redirect_existing_links
+  
   validates_presence_of :title
   validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
   validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
   validates_associated :content
-  
+
+  def title=(value)
+    value = Wiki.titleize(value)
+    @previous_title = read_attribute(:title) if @previous_title.blank?
+    write_attribute(:title, value)
+  end
+
   def before_save
-    self.title = Wiki.titleize(title)
+    self.title = Wiki.titleize(title)    
+    # Manage redirects if the title has changed
+    if !@previous_title.blank? && (@previous_title != title) && !new_record?
+      # Update redirects that point to the old title
+      wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
+        r.redirects_to = title
+        r.title == r.redirects_to ? r.destroy : r.save
+      end
+      # Remove redirects for the new title
+      wiki.redirects.find_all_by_title(title).each(&:destroy)
+      # Create a redirect to the new title
+      wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
+      @previous_title = nil
+    end
+  end
+  
+  def before_destroy
+    # Remove redirects to this page
+    wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
   end
   
   def pretty_title
diff --git a/app/models/wiki_redirect.rb b/app/models/wiki_redirect.rb
new file mode 100644
index 000000000..adc2b24c1
--- /dev/null
+++ b/app/models/wiki_redirect.rb
@@ -0,0 +1,23 @@
+# redMine - project management software
+# Copyright (C) 2006-2007  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+class WikiRedirect < ActiveRecord::Base
+  belongs_to :wiki
+  
+  validates_presence_of :title, :redirects_to
+  validates_length_of :title, :redirects_to, :maximum => 255
+end
diff --git a/app/views/wiki/rename.rhtml b/app/views/wiki/rename.rhtml
new file mode 100644
index 000000000..0c069f43d
--- /dev/null
+++ b/app/views/wiki/rename.rhtml
@@ -0,0 +1,11 @@
+<h2><%= l(:button_rename) %>: <%= @original_title %></h2>
+
+<%= error_messages_for 'page' %>
+
+<% labelled_tabular_form_for :wiki_page, @page, :url => { :action => 'rename' } do |f| %>
+<div class="box">
+<p><%= f.text_field :title, :required => true, :size => 255  %></p>
+<p><%= f.check_box :redirect_existing_links %></p>
+</div>
+<%= submit_tag l(:button_rename) %>
+<% end %>
diff --git a/app/views/wiki/show.rhtml b/app/views/wiki/show.rhtml
index 4041ec018..de28ff1e2 100644
--- a/app/views/wiki/show.rhtml
+++ b/app/views/wiki/show.rhtml
@@ -1,5 +1,6 @@
 <div class="contextual">
 <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit') if @content.version == @page.content.version %>
+<%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :page => @page.title}, :class => 'icon icon-move') if @content.version == @page.content.version %>
 <%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :page => @page.title}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %>
 <%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :page => @page.title, :version => @content.version }, :class => 'icon icon-cancel') if @content.version < @page.content.version %>
 <%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
@@ -26,8 +27,8 @@
 
 <div class="contextual">
 <%= l(:label_export_to) %>
-<%= link_to 'HTML', {:export => 'html', :version => @content.version}, :class => 'icon icon-html' %>,
-<%= link_to 'TXT', {:export => 'txt', :version => @content.version}, :class => 'icon icon-txt' %>
+<%= link_to 'HTML', {:page => @page.title, :export => 'html', :version => @content.version}, :class => 'icon icon-html' %>,
+<%= link_to 'TXT', {:page => @page.title, :export => 'txt', :version => @content.version}, :class => 'icon icon-txt' %>
 </div>
 
 <% if authorize_for('wiki', 'add_attachment') %>
diff --git a/db/migrate/067_create_wiki_redirects.rb b/db/migrate/067_create_wiki_redirects.rb
new file mode 100644
index 000000000..dda6ba6d5
--- /dev/null
+++ b/db/migrate/067_create_wiki_redirects.rb
@@ -0,0 +1,15 @@
+class CreateWikiRedirects < ActiveRecord::Migration
+  def self.up
+    create_table :wiki_redirects do |t|
+      t.column :wiki_id, :integer, :null => false
+      t.column :title, :string
+      t.column :redirects_to, :string
+      t.column :created_on, :datetime, :null => false
+    end
+    add_index :wiki_redirects, [:wiki_id, :title], :name => :wiki_redirects_wiki_id_title
+  end
+
+  def self.down
+    drop_table :wiki_redirects
+  end
+end
diff --git a/lang/bg.yml b/lang/bg.yml
index f34c3d451..57846c3de 100644
--- a/lang/bg.yml
+++ b/lang/bg.yml
@@ -157,6 +157,7 @@ field_is_filter: Използва се за филтър
 field_issue_to_id: Related issue
 field_delay: Delay
 field_assignable: Issues can be assigned to this role
+field_redirect_existing_links: Redirect existing links
 
 setting_app_title: Заглавие
 setting_app_subtitle: Описание
@@ -446,6 +447,7 @@ button_reply: Reply
 button_archive: Archive
 button_unarchive: Unarchive
 button_reset: Reset
+button_rename: Rename
 
 status_active: активен
 status_registered: регистриран
diff --git a/lang/de.yml b/lang/de.yml
index f40b8e1cc..a367c0886 100644
--- a/lang/de.yml
+++ b/lang/de.yml
@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
 field_issue_to_id: Related issue
 field_delay: Delay
 field_assignable: Issues can be assigned to this role
+field_redirect_existing_links: Redirect existing links
 
 setting_app_title: Applikation Titel
 setting_app_subtitle: Applikation Untertitel
@@ -446,6 +447,7 @@ button_reply: Reply
 button_archive: Archive
 button_unarchive: Unarchive
 button_reset: Reset
+button_rename: Rename
 
 status_active: aktiv
 status_registered: angemeldet
diff --git a/lang/en.yml b/lang/en.yml
index 0e3b74720..fd74c853a 100644
--- a/lang/en.yml
+++ b/lang/en.yml
@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
 field_issue_to_id: Related issue
 field_delay: Delay
 field_assignable: Issues can be assigned to this role
+field_redirect_existing_links: Redirect existing links
 
 setting_app_title: Application title
 setting_app_subtitle: Application subtitle
@@ -446,6 +447,7 @@ button_reply: Reply
 button_archive: Archive
 button_unarchive: Unarchive
 button_reset: Reset
+button_rename: Rename
 
 status_active: active
 status_registered: registered
diff --git a/lang/es.yml b/lang/es.yml
index b1a92fd44..366dfa01c 100644
--- a/lang/es.yml
+++ b/lang/es.yml
@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
 field_issue_to_id: Related issue
 field_delay: Delay
 field_assignable: Issues can be assigned to this role
+field_redirect_existing_links: Redirect existing links
 
 setting_app_title: Título del aplicación
 setting_app_subtitle: Subtítulo del aplicación
@@ -446,6 +447,7 @@ button_reply: Reply
 button_archive: Archive
 button_unarchive: Unarchive
 button_reset: Reset
+button_rename: Rename
 
 status_active: active
 status_registered: registered
diff --git a/lang/fr.yml b/lang/fr.yml
index 35fc48d69..b7ad0b0b4 100644
--- a/lang/fr.yml
+++ b/lang/fr.yml
@@ -157,6 +157,7 @@ field_is_filter: Utilisé comme filtre
 field_issue_to_id: Demande liée
 field_delay: Retard
 field_assignable: Demandes assignables à ce rôle
+field_redirect_existing_links: Rediriger les liens existants
 
 setting_app_title: Titre de l'application
 setting_app_subtitle: Sous-titre de l'application
@@ -446,6 +447,7 @@ button_reply: Répondre
 button_archive: Archiver
 button_unarchive: Désarchiver
 button_reset: Réinitialiser
+button_rename: Renommer
 
 status_active: actif
 status_registered: enregistré
diff --git a/lang/it.yml b/lang/it.yml
index 996f6d862..b30d2ab9c 100644
--- a/lang/it.yml
+++ b/lang/it.yml
@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
 field_issue_to_id: Related issue
 field_delay: Delay
 field_assignable: Issues can be assigned to this role
+field_redirect_existing_links: Redirect existing links
 
 setting_app_title: Titolo applicazione
 setting_app_subtitle: Sottotitolo applicazione
@@ -446,6 +447,7 @@ button_reply: Reply
 button_archive: Archive
 button_unarchive: Unarchive
 button_reset: Reset
+button_rename: Rename
 
 status_active: attivo
 status_registered: registrato
diff --git a/lang/ja.yml b/lang/ja.yml
index bbadf5c91..a0d6c168b 100644
--- a/lang/ja.yml
+++ b/lang/ja.yml
@@ -158,6 +158,7 @@ field_is_filter: フィルタとして使う
 field_issue_to_id: 関連する問題
 field_delay: 遅延
 field_assignable: Issues can be assigned to this role
+field_redirect_existing_links: Redirect existing links
 
 setting_app_title: アプリケーションのタイトル
 setting_app_subtitle: アプリケーションのサブタイトル
@@ -447,6 +448,7 @@ button_reply: 返答
 button_archive: 書庫に保存
 button_unarchive: 書庫から戻す
 button_reset: Reset
+button_rename: Rename
 
 status_active: 有効
 status_registered: 登録
diff --git a/lang/nl.yml b/lang/nl.yml
index b6163e29e..ba9817bbd 100644
--- a/lang/nl.yml
+++ b/lang/nl.yml
@@ -157,6 +157,7 @@ field_is_filter: Gebruikt als een filter
 field_issue_to_id: Gerelateerd issue
 field_delay: Vertraging
 field_assignable: Issues can be assigned to this role
+field_redirect_existing_links: Redirect existing links
 
 setting_app_title: Applicatie titel
 setting_app_subtitle: Applicatie ondertitel
@@ -446,6 +447,7 @@ button_reply: Antwoord
 button_archive: Archive
 button_unarchive: Unarchive
 button_reset: Reset
+button_rename: Rename
 
 status_active: Actief
 status_registered: geregistreerd
diff --git a/lang/pt-br.yml b/lang/pt-br.yml
index 2667a51f9..15089070a 100644
--- a/lang/pt-br.yml
+++ b/lang/pt-br.yml
@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
 field_issue_to_id: Related issue
 field_delay: Delay
 field_assignable: Issues can be assigned to this role
+field_redirect_existing_links: Redirect existing links
 
 setting_app_title: Titulo da aplicacao
 setting_app_subtitle: Sub-titulo da aplicacao
@@ -446,6 +447,7 @@ button_reply: Reply
 button_archive: Archive
 button_unarchive: Unarchive
 button_reset: Reset
+button_rename: Rename
 
 status_active: ativo
 status_registered: registrado
diff --git a/lang/pt.yml b/lang/pt.yml
index 76f03657e..d4b0e0cad 100644
--- a/lang/pt.yml
+++ b/lang/pt.yml
@@ -157,6 +157,7 @@ field_is_filter: Usado como filtro
 field_issue_to_id: Tarefa relacionada
 field_delay: Atraso
 field_assignable: Issues can be assigned to this role
+field_redirect_existing_links: Redirect existing links
 
 setting_app_title: Título da aplicação
 setting_app_subtitle: Sub-título da aplicação
@@ -446,6 +447,7 @@ button_reply: Reply
 button_archive: Archive
 button_unarchive: Unarchive
 button_reset: Reset
+button_rename: Rename
 
 status_active: ativo
 status_registered: registrado
diff --git a/lang/sv.yml b/lang/sv.yml
index c91dcfaa3..8e2c034dc 100644
--- a/lang/sv.yml
+++ b/lang/sv.yml
@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
 field_issue_to_id: Related issue
 field_delay: Delay
 field_assignable: Issues can be assigned to this role
+field_redirect_existing_links: Redirect existing links
 
 setting_app_title: Applikationstitel
 setting_app_subtitle: Applicationsunderrubrik
@@ -446,6 +447,7 @@ button_reply: Reply
 button_archive: Archive
 button_unarchive: Unarchive
 button_reset: Reset
+button_rename: Rename
 
 status_active: activ
 status_registered: registrerad
diff --git a/lang/zh.yml b/lang/zh.yml
index 3fc2319e3..e9f8cb7d9 100644
--- a/lang/zh.yml
+++ b/lang/zh.yml
@@ -160,6 +160,7 @@ field_is_filter: Used as a filter
 field_issue_to_id: Related issue
 field_delay: Delay
 field_assignable: Issues can be assigned to this role
+field_redirect_existing_links: Redirect existing links
 
 setting_app_title: 应用程序标题
 setting_app_subtitle: 应用程序子标题
@@ -448,6 +449,7 @@ button_reply: Reply
 button_archive: Archive
 button_unarchive: Unarchive
 button_reset: Reset
+button_rename: Rename
 
 status_active: 激活
 status_registered: 已注册
diff --git a/lib/redmine.rb b/lib/redmine.rb
index 1d27dd420..a0da981e4 100644
--- a/lib/redmine.rb
+++ b/lib/redmine.rb
@@ -53,6 +53,7 @@ Redmine::AccessControl.map do |map|
   # Wiki
   map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :special]
   map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
+  map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
   map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
   # Message boards
   map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
diff --git a/test/unit/wiki_redirect_test.rb b/test/unit/wiki_redirect_test.rb
new file mode 100644
index 000000000..12f6b7d89
--- /dev/null
+++ b/test/unit/wiki_redirect_test.rb
@@ -0,0 +1,73 @@
+# redMine - project management software
+# Copyright (C) 2006-2007  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WikiRedirectTest < Test::Unit::TestCase
+  fixtures :projects, :wikis
+
+  def setup
+    @wiki = Wiki.find(1)
+    @original = WikiPage.create(:wiki => @wiki, :title => 'Original title')
+  end
+  
+  def test_create_redirect
+    @original.title = 'New title'
+    assert @original.save
+    @original.reload
+    
+    assert_equal 'New_title', @original.title
+    assert @wiki.redirects.find_by_title('Original_title')
+    assert @wiki.find_page('Original title')
+  end
+  
+  def test_update_redirect
+    # create a redirect that point to this page
+    assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title')
+    
+    @original.title = 'New title'
+    @original.save
+    # make sure the old page now points to the new page
+    assert_equal 'New_title', @wiki.find_page('An old page').title
+  end
+  
+  def test_reverse_rename
+    # create a redirect that point to this page
+    assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title')
+
+    @original.title = 'An old page'
+    @original.save
+    assert !@wiki.redirects.find_by_title_and_redirects_to('An_old_page', 'An_old_page')
+    assert @wiki.redirects.find_by_title_and_redirects_to('Original_title', 'An_old_page')
+  end
+  
+  def test_rename_to_already_redirected
+    assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Other_page')
+
+    @original.title = 'An old page'
+    @original.save
+    # this redirect have to be removed since 'An old page' page now exists
+    assert !@wiki.redirects.find_by_title_and_redirects_to('An_old_page', 'Other_page')
+  end
+  
+  def test_redirects_removed_when_deleting_page
+    assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title')
+    
+    @original.destroy
+    assert !@wiki.redirects.find(:first)
+  end
+end
-- 
cgit v1.2.3