Commit b4d9ca88 authored by Jean-Philippe Lang's avatar Jean-Philippe Lang

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
parent f6fe1571
...@@ -75,6 +75,18 @@ class WikiController < ApplicationController ...@@ -75,6 +75,18 @@ class WikiController < ApplicationController
flash[:error] = l(:notice_locking_conflict) flash[:error] = l(:notice_locking_conflict)
end 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 # show page history
def history def history
@page = @wiki.find_page(params[:page]) @page = @wiki.find_page(params[:page])
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
class Wiki < ActiveRecord::Base class Wiki < ActiveRecord::Base
belongs_to :project belongs_to :project
has_many :pages, :class_name => 'WikiPage', :dependent => :destroy has_many :pages, :class_name => 'WikiPage', :dependent => :destroy
has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
validates_presence_of :start_page validates_presence_of :start_page
validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/ validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
...@@ -25,14 +26,20 @@ class Wiki < ActiveRecord::Base ...@@ -25,14 +26,20 @@ class Wiki < ActiveRecord::Base
# find the page with the given title # find the page with the given title
# if page doesn't exist, return a new page # if page doesn't exist, return a new page
def find_or_new_page(title) def find_or_new_page(title)
title = Wiki.titleize(title || start_page) find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
find_page(title) || WikiPage.new(:wiki => self, :title => title)
end end
# find the page with the given title # find the page with the given title
def find_page(title) def find_page(title, options = {})
title = start_page if title.blank? 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 end
# turn a string into a valid page title # turn a string into a valid page title
......
...@@ -22,13 +22,39 @@ class WikiPage < ActiveRecord::Base ...@@ -22,13 +22,39 @@ class WikiPage < ActiveRecord::Base
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy has_many :attachments, :as => :container, :dependent => :destroy
attr_accessor :redirect_existing_links
validates_presence_of :title validates_presence_of :title
validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/ validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
validates_associated :content 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 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 end
def pretty_title def pretty_title
......
# 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
<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 %>
<div class="contextual"> <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_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_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_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') %> <%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
...@@ -26,8 +27,8 @@ ...@@ -26,8 +27,8 @@
<div class="contextual"> <div class="contextual">
<%= l(:label_export_to) %> <%= l(:label_export_to) %>
<%= link_to 'HTML', {:export => 'html', :version => @content.version}, :class => 'icon icon-html' %>, <%= link_to 'HTML', {:page => @page.title, :export => 'html', :version => @content.version}, :class => 'icon icon-html' %>,
<%= link_to 'TXT', {:export => 'txt', :version => @content.version}, :class => 'icon icon-txt' %> <%= link_to 'TXT', {:page => @page.title, :export => 'txt', :version => @content.version}, :class => 'icon icon-txt' %>
</div> </div>
<% if authorize_for('wiki', 'add_attachment') %> <% if authorize_for('wiki', 'add_attachment') %>
......
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
...@@ -157,6 +157,7 @@ field_is_filter: Използва се за филтър ...@@ -157,6 +157,7 @@ field_is_filter: Използва се за филтър
field_issue_to_id: Related issue field_issue_to_id: Related issue
field_delay: Delay field_delay: Delay
field_assignable: Issues can be assigned to this role field_assignable: Issues can be assigned to this role
field_redirect_existing_links: Redirect existing links
setting_app_title: Заглавие setting_app_title: Заглавие
setting_app_subtitle: Описание setting_app_subtitle: Описание
...@@ -446,6 +447,7 @@ button_reply: Reply ...@@ -446,6 +447,7 @@ button_reply: Reply
button_archive: Archive button_archive: Archive
button_unarchive: Unarchive button_unarchive: Unarchive
button_reset: Reset button_reset: Reset
button_rename: Rename
status_active: активен status_active: активен
status_registered: регистриран status_registered: регистриран
......
...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter ...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
field_issue_to_id: Related issue field_issue_to_id: Related issue
field_delay: Delay field_delay: Delay
field_assignable: Issues can be assigned to this role field_assignable: Issues can be assigned to this role
field_redirect_existing_links: Redirect existing links
setting_app_title: Applikation Titel setting_app_title: Applikation Titel
setting_app_subtitle: Applikation Untertitel setting_app_subtitle: Applikation Untertitel
...@@ -446,6 +447,7 @@ button_reply: Reply ...@@ -446,6 +447,7 @@ button_reply: Reply
button_archive: Archive button_archive: Archive
button_unarchive: Unarchive button_unarchive: Unarchive
button_reset: Reset button_reset: Reset
button_rename: Rename
status_active: aktiv status_active: aktiv
status_registered: angemeldet status_registered: angemeldet
......
...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter ...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
field_issue_to_id: Related issue field_issue_to_id: Related issue
field_delay: Delay field_delay: Delay
field_assignable: Issues can be assigned to this role field_assignable: Issues can be assigned to this role
field_redirect_existing_links: Redirect existing links
setting_app_title: Application title setting_app_title: Application title
setting_app_subtitle: Application subtitle setting_app_subtitle: Application subtitle
...@@ -446,6 +447,7 @@ button_reply: Reply ...@@ -446,6 +447,7 @@ button_reply: Reply
button_archive: Archive button_archive: Archive
button_unarchive: Unarchive button_unarchive: Unarchive
button_reset: Reset button_reset: Reset
button_rename: Rename
status_active: active status_active: active
status_registered: registered status_registered: registered
......
...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter ...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
field_issue_to_id: Related issue field_issue_to_id: Related issue
field_delay: Delay field_delay: Delay
field_assignable: Issues can be assigned to this role 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_title: Título del aplicación
setting_app_subtitle: Subtítulo del aplicación setting_app_subtitle: Subtítulo del aplicación
...@@ -446,6 +447,7 @@ button_reply: Reply ...@@ -446,6 +447,7 @@ button_reply: Reply
button_archive: Archive button_archive: Archive
button_unarchive: Unarchive button_unarchive: Unarchive
button_reset: Reset button_reset: Reset
button_rename: Rename
status_active: active status_active: active
status_registered: registered status_registered: registered
......
...@@ -157,6 +157,7 @@ field_is_filter: Utilisé comme filtre ...@@ -157,6 +157,7 @@ field_is_filter: Utilisé comme filtre
field_issue_to_id: Demande liée field_issue_to_id: Demande liée
field_delay: Retard field_delay: Retard
field_assignable: Demandes assignables à ce rôle field_assignable: Demandes assignables à ce rôle
field_redirect_existing_links: Rediriger les liens existants
setting_app_title: Titre de l'application setting_app_title: Titre de l'application
setting_app_subtitle: Sous-titre de l'application setting_app_subtitle: Sous-titre de l'application
...@@ -446,6 +447,7 @@ button_reply: Répondre ...@@ -446,6 +447,7 @@ button_reply: Répondre
button_archive: Archiver button_archive: Archiver
button_unarchive: Désarchiver button_unarchive: Désarchiver
button_reset: Réinitialiser button_reset: Réinitialiser
button_rename: Renommer
status_active: actif status_active: actif
status_registered: enregistré status_registered: enregistré
......
...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter ...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
field_issue_to_id: Related issue field_issue_to_id: Related issue
field_delay: Delay field_delay: Delay
field_assignable: Issues can be assigned to this role field_assignable: Issues can be assigned to this role
field_redirect_existing_links: Redirect existing links
setting_app_title: Titolo applicazione setting_app_title: Titolo applicazione
setting_app_subtitle: Sottotitolo applicazione setting_app_subtitle: Sottotitolo applicazione
...@@ -446,6 +447,7 @@ button_reply: Reply ...@@ -446,6 +447,7 @@ button_reply: Reply
button_archive: Archive button_archive: Archive
button_unarchive: Unarchive button_unarchive: Unarchive
button_reset: Reset button_reset: Reset
button_rename: Rename
status_active: attivo status_active: attivo
status_registered: registrato status_registered: registrato
......
...@@ -158,6 +158,7 @@ field_is_filter: フィルタとして使う ...@@ -158,6 +158,7 @@ field_is_filter: フィルタとして使う
field_issue_to_id: 関連する問題 field_issue_to_id: 関連する問題
field_delay: 遅延 field_delay: 遅延
field_assignable: Issues can be assigned to this role field_assignable: Issues can be assigned to this role
field_redirect_existing_links: Redirect existing links
setting_app_title: アプリケーションのタイトル setting_app_title: アプリケーションのタイトル
setting_app_subtitle: アプリケーションのサブタイトル setting_app_subtitle: アプリケーションのサブタイトル
...@@ -447,6 +448,7 @@ button_reply: 返答 ...@@ -447,6 +448,7 @@ button_reply: 返答
button_archive: 書庫に保存 button_archive: 書庫に保存
button_unarchive: 書庫から戻す button_unarchive: 書庫から戻す
button_reset: Reset button_reset: Reset
button_rename: Rename
status_active: 有効 status_active: 有効
status_registered: 登録 status_registered: 登録
......
...@@ -157,6 +157,7 @@ field_is_filter: Gebruikt als een filter ...@@ -157,6 +157,7 @@ field_is_filter: Gebruikt als een filter
field_issue_to_id: Gerelateerd issue field_issue_to_id: Gerelateerd issue
field_delay: Vertraging field_delay: Vertraging
field_assignable: Issues can be assigned to this role field_assignable: Issues can be assigned to this role
field_redirect_existing_links: Redirect existing links
setting_app_title: Applicatie titel setting_app_title: Applicatie titel
setting_app_subtitle: Applicatie ondertitel setting_app_subtitle: Applicatie ondertitel
...@@ -446,6 +447,7 @@ button_reply: Antwoord ...@@ -446,6 +447,7 @@ button_reply: Antwoord
button_archive: Archive button_archive: Archive
button_unarchive: Unarchive button_unarchive: Unarchive
button_reset: Reset button_reset: Reset
button_rename: Rename
status_active: Actief status_active: Actief
status_registered: geregistreerd status_registered: geregistreerd
......
...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter ...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
field_issue_to_id: Related issue field_issue_to_id: Related issue
field_delay: Delay field_delay: Delay
field_assignable: Issues can be assigned to this role field_assignable: Issues can be assigned to this role
field_redirect_existing_links: Redirect existing links
setting_app_title: Titulo da aplicacao setting_app_title: Titulo da aplicacao
setting_app_subtitle: Sub-titulo da aplicacao setting_app_subtitle: Sub-titulo da aplicacao
...@@ -446,6 +447,7 @@ button_reply: Reply ...@@ -446,6 +447,7 @@ button_reply: Reply
button_archive: Archive button_archive: Archive
button_unarchive: Unarchive button_unarchive: Unarchive
button_reset: Reset button_reset: Reset
button_rename: Rename
status_active: ativo status_active: ativo
status_registered: registrado status_registered: registrado
......
...@@ -157,6 +157,7 @@ field_is_filter: Usado como filtro ...@@ -157,6 +157,7 @@ field_is_filter: Usado como filtro
field_issue_to_id: Tarefa relacionada field_issue_to_id: Tarefa relacionada
field_delay: Atraso field_delay: Atraso
field_assignable: Issues can be assigned to this role 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_title: Título da aplicação
setting_app_subtitle: Sub-título da aplicação setting_app_subtitle: Sub-título da aplicação
...@@ -446,6 +447,7 @@ button_reply: Reply ...@@ -446,6 +447,7 @@ button_reply: Reply
button_archive: Archive button_archive: Archive
button_unarchive: Unarchive button_unarchive: Unarchive
button_reset: Reset button_reset: Reset
button_rename: Rename
status_active: ativo status_active: ativo
status_registered: registrado status_registered: registrado
......
...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter ...@@ -157,6 +157,7 @@ field_is_filter: Used as a filter
field_issue_to_id: Related issue field_issue_to_id: Related issue
field_delay: Delay field_delay: Delay
field_assignable: Issues can be assigned to this role field_assignable: Issues can be assigned to this role
field_redirect_existing_links: Redirect existing links
setting_app_title: Applikationstitel setting_app_title: Applikationstitel
setting_app_subtitle: Applicationsunderrubrik setting_app_subtitle: Applicationsunderrubrik
...@@ -446,6 +447,7 @@ button_reply: Reply ...@@ -446,6 +447,7 @@ button_reply: Reply
button_archive: Archive button_archive: Archive
button_unarchive: Unarchive button_unarchive: Unarchive
button_reset: Reset button_reset: Reset
button_rename: Rename
status_active: activ status_active: activ
status_registered: registrerad status_registered: registrerad
......
...@@ -160,6 +160,7 @@ field_is_filter: Used as a filter ...@@ -160,6 +160,7 @@ field_is_filter: Used as a filter
field_issue_to_id: Related issue field_issue_to_id: Related issue
field_delay: Delay field_delay: Delay
field_assignable: Issues can be assigned to this role field_assignable: Issues can be assigned to this role
field_redirect_existing_links: Redirect existing links
setting_app_title: 应用程序标题 setting_app_title: 应用程序标题
setting_app_subtitle: 应用程序子标题 setting_app_subtitle: 应用程序子标题
...@@ -448,6 +449,7 @@ button_reply: Reply ...@@ -448,6 +449,7 @@ button_reply: Reply
button_archive: Archive button_archive: Archive
button_unarchive: Unarchive button_unarchive: Unarchive
button_reset: Reset button_reset: Reset
button_rename: Rename
status_active: 激活 status_active: 激活
status_registered: 已注册 status_registered: 已注册
......
...@@ -53,6 +53,7 @@ Redmine::AccessControl.map do |map| ...@@ -53,6 +53,7 @@ Redmine::AccessControl.map do |map|
# Wiki # Wiki
map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :special] map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :special]
map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment] 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 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
# Message boards # Message boards
map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
......
# 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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment