From b4d9ca8875898500ca198bff501dfc7d1c28bd71 2007-09-09 17:05:38 From: Jean-Philippe Lang Date: 2007-09-09 17:05:38 Subject: [PATCH] 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 --- diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index e9212a1..95f792c 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 ed473c7..b6cd661 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 074d36d..1ef8b7d 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 0000000..adc2b24 --- /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 0000000..0c069f4 --- /dev/null +++ b/app/views/wiki/rename.rhtml @@ -0,0 +1,11 @@ +

<%= l(:button_rename) %>: <%= @original_title %>

+ +<%= error_messages_for 'page' %> + +<% labelled_tabular_form_for :wiki_page, @page, :url => { :action => 'rename' } do |f| %> +
+

<%= f.text_field :title, :required => true, :size => 255 %>

+

<%= f.check_box :redirect_existing_links %>

+
+<%= submit_tag l(:button_rename) %> +<% end %> diff --git a/app/views/wiki/show.rhtml b/app/views/wiki/show.rhtml index 4041ec0..de28ff1 100644 --- a/app/views/wiki/show.rhtml +++ b/app/views/wiki/show.rhtml @@ -1,5 +1,6 @@
<%= 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 @@
<%= 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' %>
<% 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 0000000..dda6ba6 --- /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 f34c3d4..57846c3 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 f40b8e1..a367c08 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 0e3b747..fd74c85 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 b1a92fd..366dfa0 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 35fc48d..b7ad0b0 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 996f6d8..b30d2ab 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 bbadf5c..a0d6c16 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 b6163e2..ba9817b 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 2667a51..1508907 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 76f0365..d4b0e0c 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 c91dcfa..8e2c034 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 3fc2319..e9f8cb7 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 1d27dd4..a0da981 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 0000000..12f6b7d --- /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