@@ -0,0 +1,23 | |||
|
1 | # redMine - project management software | |
|
2 | # Copyright (C) 2006-2007 Jean-Philippe Lang | |
|
3 | # | |
|
4 | # This program is free software; you can redistribute it and/or | |
|
5 | # modify it under the terms of the GNU General Public License | |
|
6 | # as published by the Free Software Foundation; either version 2 | |
|
7 | # of the License, or (at your option) any later version. | |
|
8 | # | |
|
9 | # This program is distributed in the hope that it will be useful, | |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
12 | # GNU General Public License for more details. | |
|
13 | # | |
|
14 | # You should have received a copy of the GNU General Public License | |
|
15 | # along with this program; if not, write to the Free Software | |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
|
17 | ||
|
18 | class WikiRedirect < ActiveRecord::Base | |
|
19 | belongs_to :wiki | |
|
20 | ||
|
21 | validates_presence_of :title, :redirects_to | |
|
22 | validates_length_of :title, :redirects_to, :maximum => 255 | |
|
23 | end |
@@ -0,0 +1,11 | |||
|
1 | <h2><%= l(:button_rename) %>: <%= @original_title %></h2> | |
|
2 | ||
|
3 | <%= error_messages_for 'page' %> | |
|
4 | ||
|
5 | <% labelled_tabular_form_for :wiki_page, @page, :url => { :action => 'rename' } do |f| %> | |
|
6 | <div class="box"> | |
|
7 | <p><%= f.text_field :title, :required => true, :size => 255 %></p> | |
|
8 | <p><%= f.check_box :redirect_existing_links %></p> | |
|
9 | </div> | |
|
10 | <%= submit_tag l(:button_rename) %> | |
|
11 | <% end %> |
@@ -0,0 +1,15 | |||
|
1 | class CreateWikiRedirects < ActiveRecord::Migration | |
|
2 | def self.up | |
|
3 | create_table :wiki_redirects do |t| | |
|
4 | t.column :wiki_id, :integer, :null => false | |
|
5 | t.column :title, :string | |
|
6 | t.column :redirects_to, :string | |
|
7 | t.column :created_on, :datetime, :null => false | |
|
8 | end | |
|
9 | add_index :wiki_redirects, [:wiki_id, :title], :name => :wiki_redirects_wiki_id_title | |
|
10 | end | |
|
11 | ||
|
12 | def self.down | |
|
13 | drop_table :wiki_redirects | |
|
14 | end | |
|
15 | end |
@@ -0,0 +1,73 | |||
|
1 | # redMine - project management software | |
|
2 | # Copyright (C) 2006-2007 Jean-Philippe Lang | |
|
3 | # | |
|
4 | # This program is free software; you can redistribute it and/or | |
|
5 | # modify it under the terms of the GNU General Public License | |
|
6 | # as published by the Free Software Foundation; either version 2 | |
|
7 | # of the License, or (at your option) any later version. | |
|
8 | # | |
|
9 | # This program is distributed in the hope that it will be useful, | |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
12 | # GNU General Public License for more details. | |
|
13 | # | |
|
14 | # You should have received a copy of the GNU General Public License | |
|
15 | # along with this program; if not, write to the Free Software | |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
|
17 | ||
|
18 | require File.dirname(__FILE__) + '/../test_helper' | |
|
19 | ||
|
20 | class WikiRedirectTest < Test::Unit::TestCase | |
|
21 | fixtures :projects, :wikis | |
|
22 | ||
|
23 | def setup | |
|
24 | @wiki = Wiki.find(1) | |
|
25 | @original = WikiPage.create(:wiki => @wiki, :title => 'Original title') | |
|
26 | end | |
|
27 | ||
|
28 | def test_create_redirect | |
|
29 | @original.title = 'New title' | |
|
30 | assert @original.save | |
|
31 | @original.reload | |
|
32 | ||
|
33 | assert_equal 'New_title', @original.title | |
|
34 | assert @wiki.redirects.find_by_title('Original_title') | |
|
35 | assert @wiki.find_page('Original title') | |
|
36 | end | |
|
37 | ||
|
38 | def test_update_redirect | |
|
39 | # create a redirect that point to this page | |
|
40 | assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title') | |
|
41 | ||
|
42 | @original.title = 'New title' | |
|
43 | @original.save | |
|
44 | # make sure the old page now points to the new page | |
|
45 | assert_equal 'New_title', @wiki.find_page('An old page').title | |
|
46 | end | |
|
47 | ||
|
48 | def test_reverse_rename | |
|
49 | # create a redirect that point to this page | |
|
50 | assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title') | |
|
51 | ||
|
52 | @original.title = 'An old page' | |
|
53 | @original.save | |
|
54 | assert !@wiki.redirects.find_by_title_and_redirects_to('An_old_page', 'An_old_page') | |
|
55 | assert @wiki.redirects.find_by_title_and_redirects_to('Original_title', 'An_old_page') | |
|
56 | end | |
|
57 | ||
|
58 | def test_rename_to_already_redirected | |
|
59 | assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Other_page') | |
|
60 | ||
|
61 | @original.title = 'An old page' | |
|
62 | @original.save | |
|
63 | # this redirect have to be removed since 'An old page' page now exists | |
|
64 | assert !@wiki.redirects.find_by_title_and_redirects_to('An_old_page', 'Other_page') | |
|
65 | end | |
|
66 | ||
|
67 | def test_redirects_removed_when_deleting_page | |
|
68 | assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title') | |
|
69 | ||
|
70 | @original.destroy | |
|
71 | assert !@wiki.redirects.find(:first) | |
|
72 | end | |
|
73 | end |
@@ -75,6 +75,18 class WikiController < ApplicationController | |||
|
75 | 75 | flash[:error] = l(:notice_locking_conflict) |
|
76 | 76 | end |
|
77 | 77 | |
|
78 | # rename a page | |
|
79 | def rename | |
|
80 | @page = @wiki.find_page(params[:page]) | |
|
81 | @page.redirect_existing_links = true | |
|
82 | # used to display the *original* title if some AR validation errors occur | |
|
83 | @original_title = @page.pretty_title | |
|
84 | if request.post? && @page.update_attributes(params[:wiki_page]) | |
|
85 | flash[:notice] = l(:notice_successful_update) | |
|
86 | redirect_to :action => 'index', :id => @project, :page => @page.title | |
|
87 | end | |
|
88 | end | |
|
89 | ||
|
78 | 90 | # show page history |
|
79 | 91 | def history |
|
80 | 92 | @page = @wiki.find_page(params[:page]) |
@@ -18,6 +18,7 | |||
|
18 | 18 | class Wiki < ActiveRecord::Base |
|
19 | 19 | belongs_to :project |
|
20 | 20 | has_many :pages, :class_name => 'WikiPage', :dependent => :destroy |
|
21 | has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all | |
|
21 | 22 | |
|
22 | 23 | validates_presence_of :start_page |
|
23 | 24 | validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/ |
@@ -25,14 +26,20 class Wiki < ActiveRecord::Base | |||
|
25 | 26 | # find the page with the given title |
|
26 | 27 | # if page doesn't exist, return a new page |
|
27 | 28 | def find_or_new_page(title) |
|
28 | title = Wiki.titleize(title || start_page) | |
|
29 | find_page(title) || WikiPage.new(:wiki => self, :title => title) | |
|
29 | find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title)) | |
|
30 | 30 | end |
|
31 | 31 | |
|
32 | 32 | # find the page with the given title |
|
33 | def find_page(title) | |
|
33 | def find_page(title, options = {}) | |
|
34 | 34 | title = start_page if title.blank? |
|
35 |
|
|
|
35 | title = Wiki.titleize(title) | |
|
36 | page = pages.find_by_title(title) | |
|
37 | if !page && !(options[:with_redirect] == false) | |
|
38 | # search for a redirect | |
|
39 | redirect = redirects.find_by_title(title) | |
|
40 | page = find_page(redirect.redirects_to, :with_redirect => false) if redirect | |
|
41 | end | |
|
42 | page | |
|
36 | 43 | end |
|
37 | 44 | |
|
38 | 45 | # turn a string into a valid page title |
@@ -22,13 +22,39 class WikiPage < ActiveRecord::Base | |||
|
22 | 22 | has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy |
|
23 | 23 | has_many :attachments, :as => :container, :dependent => :destroy |
|
24 | 24 | |
|
25 | attr_accessor :redirect_existing_links | |
|
26 | ||
|
25 | 27 | validates_presence_of :title |
|
26 | 28 | validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/ |
|
27 | 29 | validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false |
|
28 | 30 | validates_associated :content |
|
29 | ||
|
31 | ||
|
32 | def title=(value) | |
|
33 | value = Wiki.titleize(value) | |
|
34 | @previous_title = read_attribute(:title) if @previous_title.blank? | |
|
35 | write_attribute(:title, value) | |
|
36 | end | |
|
37 | ||
|
30 | 38 | def before_save |
|
31 | self.title = Wiki.titleize(title) | |
|
39 | self.title = Wiki.titleize(title) | |
|
40 | # Manage redirects if the title has changed | |
|
41 | if !@previous_title.blank? && (@previous_title != title) && !new_record? | |
|
42 | # Update redirects that point to the old title | |
|
43 | wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r| | |
|
44 | r.redirects_to = title | |
|
45 | r.title == r.redirects_to ? r.destroy : r.save | |
|
46 | end | |
|
47 | # Remove redirects for the new title | |
|
48 | wiki.redirects.find_all_by_title(title).each(&:destroy) | |
|
49 | # Create a redirect to the new title | |
|
50 | wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0" | |
|
51 | @previous_title = nil | |
|
52 | end | |
|
53 | end | |
|
54 | ||
|
55 | def before_destroy | |
|
56 | # Remove redirects to this page | |
|
57 | wiki.redirects.find_all_by_redirects_to(title).each(&:destroy) | |
|
32 | 58 | end |
|
33 | 59 | |
|
34 | 60 | def pretty_title |
@@ -1,5 +1,6 | |||
|
1 | 1 | <div class="contextual"> |
|
2 | 2 | <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit') if @content.version == @page.content.version %> |
|
3 | <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :page => @page.title}, :class => 'icon icon-move') if @content.version == @page.content.version %> | |
|
3 | 4 | <%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :page => @page.title}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %> |
|
4 | 5 | <%= 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 %> |
|
5 | 6 | <%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %> |
@@ -26,8 +27,8 | |||
|
26 | 27 | |
|
27 | 28 | <div class="contextual"> |
|
28 | 29 | <%= l(:label_export_to) %> |
|
29 | <%= link_to 'HTML', {:export => 'html', :version => @content.version}, :class => 'icon icon-html' %>, | |
|
30 | <%= link_to 'TXT', {:export => 'txt', :version => @content.version}, :class => 'icon icon-txt' %> | |
|
30 | <%= link_to 'HTML', {:page => @page.title, :export => 'html', :version => @content.version}, :class => 'icon icon-html' %>, | |
|
31 | <%= link_to 'TXT', {:page => @page.title, :export => 'txt', :version => @content.version}, :class => 'icon icon-txt' %> | |
|
31 | 32 | </div> |
|
32 | 33 | |
|
33 | 34 | <% if authorize_for('wiki', 'add_attachment') %> |
@@ -157,6 +157,7 field_is_filter: Използва се за филтър | |||
|
157 | 157 | field_issue_to_id: Related issue |
|
158 | 158 | field_delay: Delay |
|
159 | 159 | field_assignable: Issues can be assigned to this role |
|
160 | field_redirect_existing_links: Redirect existing links | |
|
160 | 161 | |
|
161 | 162 | setting_app_title: Заглавие |
|
162 | 163 | setting_app_subtitle: Описание |
@@ -446,6 +447,7 button_reply: Reply | |||
|
446 | 447 | button_archive: Archive |
|
447 | 448 | button_unarchive: Unarchive |
|
448 | 449 | button_reset: Reset |
|
450 | button_rename: Rename | |
|
449 | 451 | |
|
450 | 452 | status_active: активен |
|
451 | 453 | status_registered: регистриран |
@@ -157,6 +157,7 field_is_filter: Used as a filter | |||
|
157 | 157 | field_issue_to_id: Related issue |
|
158 | 158 | field_delay: Delay |
|
159 | 159 | field_assignable: Issues can be assigned to this role |
|
160 | field_redirect_existing_links: Redirect existing links | |
|
160 | 161 | |
|
161 | 162 | setting_app_title: Applikation Titel |
|
162 | 163 | setting_app_subtitle: Applikation Untertitel |
@@ -446,6 +447,7 button_reply: Reply | |||
|
446 | 447 | button_archive: Archive |
|
447 | 448 | button_unarchive: Unarchive |
|
448 | 449 | button_reset: Reset |
|
450 | button_rename: Rename | |
|
449 | 451 | |
|
450 | 452 | status_active: aktiv |
|
451 | 453 | status_registered: angemeldet |
@@ -157,6 +157,7 field_is_filter: Used as a filter | |||
|
157 | 157 | field_issue_to_id: Related issue |
|
158 | 158 | field_delay: Delay |
|
159 | 159 | field_assignable: Issues can be assigned to this role |
|
160 | field_redirect_existing_links: Redirect existing links | |
|
160 | 161 | |
|
161 | 162 | setting_app_title: Application title |
|
162 | 163 | setting_app_subtitle: Application subtitle |
@@ -446,6 +447,7 button_reply: Reply | |||
|
446 | 447 | button_archive: Archive |
|
447 | 448 | button_unarchive: Unarchive |
|
448 | 449 | button_reset: Reset |
|
450 | button_rename: Rename | |
|
449 | 451 | |
|
450 | 452 | status_active: active |
|
451 | 453 | status_registered: registered |
@@ -157,6 +157,7 field_is_filter: Used as a filter | |||
|
157 | 157 | field_issue_to_id: Related issue |
|
158 | 158 | field_delay: Delay |
|
159 | 159 | field_assignable: Issues can be assigned to this role |
|
160 | field_redirect_existing_links: Redirect existing links | |
|
160 | 161 | |
|
161 | 162 | setting_app_title: Título del aplicación |
|
162 | 163 | setting_app_subtitle: Subtítulo del aplicación |
@@ -446,6 +447,7 button_reply: Reply | |||
|
446 | 447 | button_archive: Archive |
|
447 | 448 | button_unarchive: Unarchive |
|
448 | 449 | button_reset: Reset |
|
450 | button_rename: Rename | |
|
449 | 451 | |
|
450 | 452 | status_active: active |
|
451 | 453 | status_registered: registered |
@@ -157,6 +157,7 field_is_filter: Utilisé comme filtre | |||
|
157 | 157 | field_issue_to_id: Demande liée |
|
158 | 158 | field_delay: Retard |
|
159 | 159 | field_assignable: Demandes assignables à ce rôle |
|
160 | field_redirect_existing_links: Rediriger les liens existants | |
|
160 | 161 | |
|
161 | 162 | setting_app_title: Titre de l'application |
|
162 | 163 | setting_app_subtitle: Sous-titre de l'application |
@@ -446,6 +447,7 button_reply: Répondre | |||
|
446 | 447 | button_archive: Archiver |
|
447 | 448 | button_unarchive: Désarchiver |
|
448 | 449 | button_reset: Réinitialiser |
|
450 | button_rename: Renommer | |
|
449 | 451 | |
|
450 | 452 | status_active: actif |
|
451 | 453 | status_registered: enregistré |
@@ -157,6 +157,7 field_is_filter: Used as a filter | |||
|
157 | 157 | field_issue_to_id: Related issue |
|
158 | 158 | field_delay: Delay |
|
159 | 159 | field_assignable: Issues can be assigned to this role |
|
160 | field_redirect_existing_links: Redirect existing links | |
|
160 | 161 | |
|
161 | 162 | setting_app_title: Titolo applicazione |
|
162 | 163 | setting_app_subtitle: Sottotitolo applicazione |
@@ -446,6 +447,7 button_reply: Reply | |||
|
446 | 447 | button_archive: Archive |
|
447 | 448 | button_unarchive: Unarchive |
|
448 | 449 | button_reset: Reset |
|
450 | button_rename: Rename | |
|
449 | 451 | |
|
450 | 452 | status_active: attivo |
|
451 | 453 | status_registered: registrato |
@@ -158,6 +158,7 field_is_filter: フィルタとして使う | |||
|
158 | 158 | field_issue_to_id: 関連する問題 |
|
159 | 159 | field_delay: 遅延 |
|
160 | 160 | field_assignable: Issues can be assigned to this role |
|
161 | field_redirect_existing_links: Redirect existing links | |
|
161 | 162 | |
|
162 | 163 | setting_app_title: アプリケーションのタイトル |
|
163 | 164 | setting_app_subtitle: アプリケーションのサブタイトル |
@@ -447,6 +448,7 button_reply: 返答 | |||
|
447 | 448 | button_archive: 書庫に保存 |
|
448 | 449 | button_unarchive: 書庫から戻す |
|
449 | 450 | button_reset: Reset |
|
451 | button_rename: Rename | |
|
450 | 452 | |
|
451 | 453 | status_active: 有効 |
|
452 | 454 | status_registered: 登録 |
@@ -157,6 +157,7 field_is_filter: Gebruikt als een filter | |||
|
157 | 157 | field_issue_to_id: Gerelateerd issue |
|
158 | 158 | field_delay: Vertraging |
|
159 | 159 | field_assignable: Issues can be assigned to this role |
|
160 | field_redirect_existing_links: Redirect existing links | |
|
160 | 161 | |
|
161 | 162 | setting_app_title: Applicatie titel |
|
162 | 163 | setting_app_subtitle: Applicatie ondertitel |
@@ -446,6 +447,7 button_reply: Antwoord | |||
|
446 | 447 | button_archive: Archive |
|
447 | 448 | button_unarchive: Unarchive |
|
448 | 449 | button_reset: Reset |
|
450 | button_rename: Rename | |
|
449 | 451 | |
|
450 | 452 | status_active: Actief |
|
451 | 453 | status_registered: geregistreerd |
@@ -157,6 +157,7 field_is_filter: Used as a filter | |||
|
157 | 157 | field_issue_to_id: Related issue |
|
158 | 158 | field_delay: Delay |
|
159 | 159 | field_assignable: Issues can be assigned to this role |
|
160 | field_redirect_existing_links: Redirect existing links | |
|
160 | 161 | |
|
161 | 162 | setting_app_title: Titulo da aplicacao |
|
162 | 163 | setting_app_subtitle: Sub-titulo da aplicacao |
@@ -446,6 +447,7 button_reply: Reply | |||
|
446 | 447 | button_archive: Archive |
|
447 | 448 | button_unarchive: Unarchive |
|
448 | 449 | button_reset: Reset |
|
450 | button_rename: Rename | |
|
449 | 451 | |
|
450 | 452 | status_active: ativo |
|
451 | 453 | status_registered: registrado |
@@ -157,6 +157,7 field_is_filter: Usado como filtro | |||
|
157 | 157 | field_issue_to_id: Tarefa relacionada |
|
158 | 158 | field_delay: Atraso |
|
159 | 159 | field_assignable: Issues can be assigned to this role |
|
160 | field_redirect_existing_links: Redirect existing links | |
|
160 | 161 | |
|
161 | 162 | setting_app_title: Título da aplicação |
|
162 | 163 | setting_app_subtitle: Sub-título da aplicação |
@@ -446,6 +447,7 button_reply: Reply | |||
|
446 | 447 | button_archive: Archive |
|
447 | 448 | button_unarchive: Unarchive |
|
448 | 449 | button_reset: Reset |
|
450 | button_rename: Rename | |
|
449 | 451 | |
|
450 | 452 | status_active: ativo |
|
451 | 453 | status_registered: registrado |
@@ -157,6 +157,7 field_is_filter: Used as a filter | |||
|
157 | 157 | field_issue_to_id: Related issue |
|
158 | 158 | field_delay: Delay |
|
159 | 159 | field_assignable: Issues can be assigned to this role |
|
160 | field_redirect_existing_links: Redirect existing links | |
|
160 | 161 | |
|
161 | 162 | setting_app_title: Applikationstitel |
|
162 | 163 | setting_app_subtitle: Applicationsunderrubrik |
@@ -446,6 +447,7 button_reply: Reply | |||
|
446 | 447 | button_archive: Archive |
|
447 | 448 | button_unarchive: Unarchive |
|
448 | 449 | button_reset: Reset |
|
450 | button_rename: Rename | |
|
449 | 451 | |
|
450 | 452 | status_active: activ |
|
451 | 453 | status_registered: registrerad |
@@ -160,6 +160,7 field_is_filter: Used as a filter | |||
|
160 | 160 | field_issue_to_id: Related issue |
|
161 | 161 | field_delay: Delay |
|
162 | 162 | field_assignable: Issues can be assigned to this role |
|
163 | field_redirect_existing_links: Redirect existing links | |
|
163 | 164 | |
|
164 | 165 | setting_app_title: 应用程序标题 |
|
165 | 166 | setting_app_subtitle: 应用程序子标题 |
@@ -448,6 +449,7 button_reply: Reply | |||
|
448 | 449 | button_archive: Archive |
|
449 | 450 | button_unarchive: Unarchive |
|
450 | 451 | button_reset: Reset |
|
452 | button_rename: Rename | |
|
451 | 453 | |
|
452 | 454 | status_active: 激活 |
|
453 | 455 | status_registered: 已注册 |
@@ -53,6 +53,7 Redmine::AccessControl.map do |map| | |||
|
53 | 53 | # Wiki |
|
54 | 54 | map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :special] |
|
55 | 55 | map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment] |
|
56 | map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member | |
|
56 | 57 | map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member |
|
57 | 58 | # Message boards |
|
58 | 59 | map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true |
General Comments 0
You need to be logged in to leave comments.
Login now