##// END OF EJS Templates
Fixed that moving a wiki page with a child raises an error when target wiki contains a page with the same name as the child (#21900)....
Jean-Philippe Lang -
r14748:f4bcb1286e21
parent child
Show More
@@ -1,297 +1,297
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'diff'
19 19 require 'enumerator'
20 20
21 21 class WikiPage < ActiveRecord::Base
22 22 include Redmine::SafeAttributes
23 23
24 24 belongs_to :wiki
25 25 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
26 26 has_one :content_without_text, lambda {without_text.readonly}, :class_name => 'WikiContent', :foreign_key => 'page_id'
27 27
28 28 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
29 29 acts_as_tree :dependent => :nullify, :order => 'title'
30 30
31 31 acts_as_watchable
32 32 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
33 33 :description => :text,
34 34 :datetime => :created_on,
35 35 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
36 36
37 37 acts_as_searchable :columns => ['title', "#{WikiContent.table_name}.text"],
38 38 :scope => joins(:content, {:wiki => :project}),
39 39 :preload => [:content, {:wiki => :project}],
40 40 :permission => :view_wiki_pages,
41 41 :project_key => "#{Wiki.table_name}.project_id"
42 42
43 43 attr_accessor :redirect_existing_links
44 44
45 45 validates_presence_of :title
46 46 validates_format_of :title, :with => /\A[^,\.\/\?\;\|\s]*\z/
47 47 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
48 48 validates_associated :content
49 49 attr_protected :id
50 50
51 51 validate :validate_parent_title
52 52 before_destroy :delete_redirects
53 53 before_save :handle_rename_or_move
54 54 after_save :handle_children_move
55 55
56 56 # eager load information about last updates, without loading text
57 57 scope :with_updated_on, lambda { preload(:content_without_text) }
58 58
59 59 # Wiki pages that are protected by default
60 60 DEFAULT_PROTECTED_PAGES = %w(sidebar)
61 61
62 62 safe_attributes 'parent_id', 'parent_title', 'title', 'redirect_existing_links', 'wiki_id',
63 63 :if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)}
64 64
65 65 def initialize(attributes=nil, *args)
66 66 super
67 67 if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
68 68 self.protected = true
69 69 end
70 70 end
71 71
72 72 def visible?(user=User.current)
73 73 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
74 74 end
75 75
76 76 def title=(value)
77 77 value = Wiki.titleize(value)
78 78 write_attribute(:title, value)
79 79 end
80 80
81 81 def safe_attributes=(attrs, user=User.current)
82 82 return unless attrs.is_a?(Hash)
83 83 attrs = attrs.deep_dup
84 84
85 85 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
86 86 if (w_id = attrs.delete('wiki_id')) && safe_attribute?('wiki_id')
87 87 if (w = Wiki.find_by_id(w_id)) && w.project && user.allowed_to?(:rename_wiki_pages, w.project)
88 88 self.wiki = w
89 89 end
90 90 end
91 91
92 92 super attrs, user
93 93 end
94 94
95 95 # Manages redirects if page is renamed or moved
96 96 def handle_rename_or_move
97 97 if !new_record? && (title_changed? || wiki_id_changed?)
98 98 # Update redirects that point to the old title
99 99 WikiRedirect.where(:redirects_to => title_was, :redirects_to_wiki_id => wiki_id_was).each do |r|
100 100 r.redirects_to = title
101 101 r.redirects_to_wiki_id = wiki_id
102 102 (r.title == r.redirects_to && r.wiki_id == r.redirects_to_wiki_id) ? r.destroy : r.save
103 103 end
104 104 # Remove redirects for the new title
105 105 WikiRedirect.where(:wiki_id => wiki_id, :title => title).delete_all
106 106 # Create a redirect to the new title
107 107 unless redirect_existing_links == "0"
108 108 WikiRedirect.create(
109 109 :wiki_id => wiki_id_was, :title => title_was,
110 110 :redirects_to_wiki_id => wiki_id, :redirects_to => title
111 111 )
112 112 end
113 113 end
114 114 if !new_record? && wiki_id_changed? && parent.present?
115 115 unless parent.wiki_id == wiki_id
116 116 self.parent_id = nil
117 117 end
118 118 end
119 119 end
120 120 private :handle_rename_or_move
121 121
122 122 # Moves child pages if page was moved
123 123 def handle_children_move
124 124 if !new_record? && wiki_id_changed?
125 125 children.each do |child|
126 126 child.wiki_id = wiki_id
127 127 child.redirect_existing_links = redirect_existing_links
128 128 unless child.save
129 WikiPage.where(:id => child.id).update_all :parent_nil => nil
129 WikiPage.where(:id => child.id).update_all :parent_id => nil
130 130 end
131 131 end
132 132 end
133 133 end
134 134 private :handle_children_move
135 135
136 136 # Deletes redirects to this page
137 137 def delete_redirects
138 138 WikiRedirect.where(:redirects_to_wiki_id => wiki_id, :redirects_to => title).delete_all
139 139 end
140 140
141 141 def pretty_title
142 142 WikiPage.pretty_title(title)
143 143 end
144 144
145 145 def content_for_version(version=nil)
146 146 if content
147 147 result = content.versions.find_by_version(version.to_i) if version
148 148 result ||= content
149 149 result
150 150 end
151 151 end
152 152
153 153 def diff(version_to=nil, version_from=nil)
154 154 version_to = version_to ? version_to.to_i : self.content.version
155 155 content_to = content.versions.find_by_version(version_to)
156 156 content_from = version_from ? content.versions.find_by_version(version_from.to_i) : content_to.try(:previous)
157 157 return nil unless content_to && content_from
158 158
159 159 if content_from.version > content_to.version
160 160 content_to, content_from = content_from, content_to
161 161 end
162 162
163 163 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
164 164 end
165 165
166 166 def annotate(version=nil)
167 167 version = version ? version.to_i : self.content.version
168 168 c = content.versions.find_by_version(version)
169 169 c ? WikiAnnotate.new(c) : nil
170 170 end
171 171
172 172 def self.pretty_title(str)
173 173 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
174 174 end
175 175
176 176 def project
177 177 wiki.try(:project)
178 178 end
179 179
180 180 def text
181 181 content.text if content
182 182 end
183 183
184 184 def updated_on
185 185 content_attribute(:updated_on)
186 186 end
187 187
188 188 def version
189 189 content_attribute(:version)
190 190 end
191 191
192 192 # Returns true if usr is allowed to edit the page, otherwise false
193 193 def editable_by?(usr)
194 194 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
195 195 end
196 196
197 197 def attachments_deletable?(usr=User.current)
198 198 editable_by?(usr) && super(usr)
199 199 end
200 200
201 201 def parent_title
202 202 @parent_title || (self.parent && self.parent.pretty_title)
203 203 end
204 204
205 205 def parent_title=(t)
206 206 @parent_title = t
207 207 parent_page = t.blank? ? nil : self.wiki.find_page(t)
208 208 self.parent = parent_page
209 209 end
210 210
211 211 # Saves the page and its content if text was changed
212 212 # Return true if the page was saved
213 213 def save_with_content(content)
214 214 ret = nil
215 215 transaction do
216 216 ret = save
217 217 if content.text_changed?
218 218 begin
219 219 self.content = content
220 220 ret = ret && content.changed?
221 221 rescue ActiveRecord::RecordNotSaved
222 222 ret = false
223 223 end
224 224 end
225 225 raise ActiveRecord::Rollback unless ret
226 226 end
227 227 ret
228 228 end
229 229
230 230 protected
231 231
232 232 def validate_parent_title
233 233 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
234 234 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
235 235 if parent_id_changed? && parent && (parent.wiki_id != wiki_id)
236 236 errors.add(:parent_title, :not_same_project)
237 237 end
238 238 end
239 239
240 240 private
241 241
242 242 def content_attribute(name)
243 243 (association(:content).loaded? ? content : content_without_text).try(name)
244 244 end
245 245 end
246 246
247 247 class WikiDiff < Redmine::Helpers::Diff
248 248 attr_reader :content_to, :content_from
249 249
250 250 def initialize(content_to, content_from)
251 251 @content_to = content_to
252 252 @content_from = content_from
253 253 super(content_to.text, content_from.text)
254 254 end
255 255 end
256 256
257 257 class WikiAnnotate
258 258 attr_reader :lines, :content
259 259
260 260 def initialize(content)
261 261 @content = content
262 262 current = content
263 263 current_lines = current.text.split(/\r?\n/)
264 264 @lines = current_lines.collect {|t| [nil, nil, t]}
265 265 positions = []
266 266 current_lines.size.times {|i| positions << i}
267 267 while (current.previous)
268 268 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
269 269 d.each_slice(3) do |s|
270 270 sign, line = s[0], s[1]
271 271 if sign == '+' && positions[line] && positions[line] != -1
272 272 if @lines[positions[line]][0].nil?
273 273 @lines[positions[line]][0] = current.version
274 274 @lines[positions[line]][1] = current.author
275 275 end
276 276 end
277 277 end
278 278 d.each_slice(3) do |s|
279 279 sign, line = s[0], s[1]
280 280 if sign == '-'
281 281 positions.insert(line, -1)
282 282 else
283 283 positions[line] = nil
284 284 end
285 285 end
286 286 positions.compact!
287 287 # Stop if every line is annotated
288 288 break unless @lines.detect { |line| line[0].nil? }
289 289 current = current.previous
290 290 end
291 291 @lines.each { |line|
292 292 line[0] ||= current.version
293 293 # if the last known version is > 1 (eg. history was cleared), we don't know the author
294 294 line[1] ||= current.author if current.version == 1
295 295 }
296 296 end
297 297 end
@@ -1,183 +1,201
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class WikiPageTest < ActiveSupport::TestCase
21 21 fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
22 22
23 23 def setup
24 24 @wiki = Wiki.find(1)
25 25 @page = @wiki.pages.first
26 26 end
27 27
28 28 def test_create
29 29 page = WikiPage.new(:wiki => @wiki)
30 30 assert !page.save
31 31 assert_equal 1, page.errors.count
32 32
33 33 page.title = "Page"
34 34 assert page.save
35 35 page.reload
36 36 assert !page.protected?
37 37
38 38 @wiki.reload
39 39 assert @wiki.pages.include?(page)
40 40 end
41 41
42 42 def test_sidebar_should_be_protected_by_default
43 43 page = @wiki.find_or_new_page('sidebar')
44 44 assert page.new_record?
45 45 assert page.protected?
46 46 end
47 47
48 48 def test_find_or_new_page
49 49 page = @wiki.find_or_new_page("CookBook documentation")
50 50 assert_kind_of WikiPage, page
51 51 assert !page.new_record?
52 52
53 53 page = @wiki.find_or_new_page("Non existing page")
54 54 assert_kind_of WikiPage, page
55 55 assert page.new_record?
56 56 end
57 57
58 58 def test_parent_title
59 59 page = WikiPage.find_by_title('Another_page')
60 60 assert_nil page.parent_title
61 61
62 62 page = WikiPage.find_by_title('Page_with_an_inline_image')
63 63 assert_equal 'CookBook documentation', page.parent_title
64 64 end
65 65
66 66 def test_assign_parent
67 67 page = WikiPage.find_by_title('Another_page')
68 68 page.parent_title = 'CookBook documentation'
69 69 assert page.save
70 70 page.reload
71 71 assert_equal WikiPage.find_by_title('CookBook_documentation'), page.parent
72 72 end
73 73
74 74 def test_unassign_parent
75 75 page = WikiPage.find_by_title('Page_with_an_inline_image')
76 76 page.parent_title = ''
77 77 assert page.save
78 78 page.reload
79 79 assert_nil page.parent
80 80 end
81 81
82 82 def test_parent_validation
83 83 page = WikiPage.find_by_title('CookBook_documentation')
84 84
85 85 # A page that doesn't exist
86 86 page.parent_title = 'Unknown title'
87 87 assert !page.save
88 88 assert_include I18n.translate('activerecord.errors.messages.invalid'),
89 89 page.errors[:parent_title]
90 90 # A child page
91 91 page.parent_title = 'Page_with_an_inline_image'
92 92 assert !page.save
93 93 assert_include I18n.translate('activerecord.errors.messages.circular_dependency'),
94 94 page.errors[:parent_title]
95 95 # The page itself
96 96 page.parent_title = 'CookBook_documentation'
97 97 assert !page.save
98 98 assert_include I18n.translate('activerecord.errors.messages.circular_dependency'),
99 99 page.errors[:parent_title]
100 100 page.parent_title = 'Another_page'
101 101 assert page.save
102 102 end
103 103
104 104 def test_move_child_should_clear_parent
105 105 parent = WikiPage.create!(:wiki_id => 1, :title => 'Parent')
106 106 child = WikiPage.create!(:wiki_id => 1, :title => 'Child', :parent => parent)
107 107
108 108 child.wiki_id = 2
109 109 child.save!
110 110 assert_equal nil, child.reload.parent_id
111 111 end
112 112
113 113 def test_move_parent_should_move_child_page
114 114 parent = WikiPage.create!(:wiki_id => 1, :title => 'Parent')
115 115 child = WikiPage.create!(:wiki_id => 1, :title => 'Child', :parent => parent)
116 116 parent.reload
117 117
118 118 parent.wiki_id = 2
119 119 parent.save!
120 120 assert_equal 2, child.reload.wiki_id
121 121 assert_equal parent, child.parent
122 122 end
123 123
124 def test_move_parent_with_child_with_duplicate_name_should_not_move_child
125 parent = WikiPage.create!(:wiki_id => 1, :title => 'Parent')
126 child = WikiPage.create!(:wiki_id => 1, :title => 'Child', :parent_id => parent.id)
127 parent.reload
128 # page with the same name as the child in the target wiki
129 WikiPage.create!(:wiki_id => 2, :title => 'Child')
130
131 parent.wiki_id = 2
132 parent.save!
133
134 parent.reload
135 assert_equal 2, parent.wiki_id
136
137 child.reload
138 assert_equal 1, child.wiki_id
139 assert_nil child.parent_id
140 end
141
124 142 def test_destroy
125 143 page = WikiPage.find(1)
126 144 page.destroy
127 145 assert_nil WikiPage.find_by_id(1)
128 146 # make sure that page content and its history are deleted
129 147 assert_equal 0, WikiContent.where(:page_id => 1).count
130 148 assert_equal 0, WikiContent.versioned_class.where(:page_id => 1).count
131 149 end
132 150
133 151 def test_destroy_should_not_nullify_children
134 152 page = WikiPage.find(2)
135 153 child_ids = page.child_ids
136 154 assert child_ids.any?
137 155 page.destroy
138 156 assert_nil WikiPage.find_by_id(2)
139 157
140 158 children = WikiPage.where(:id => child_ids)
141 159 assert_equal child_ids.size, children.count
142 160 children.each do |child|
143 161 assert_nil child.parent_id
144 162 end
145 163 end
146 164
147 165 def test_with_updated_on_scope_should_preload_updated_on_and_version
148 166 page = WikiPage.with_updated_on.where(:id => 1).first
149 167 # make the assertions fail if attributes are not preloaded
150 168 WikiContent.update_all(:updated_on => '2001-01-01 10:00:00', :version => 1)
151 169
152 170 assert_equal Time.gm(2007, 3, 6, 23, 10, 51), page.updated_on
153 171 assert_equal 3, page.version
154 172 end
155 173
156 174 def test_descendants
157 175 page = WikiPage.create!(:wiki => @wiki, :title => 'Parent')
158 176 child1 = WikiPage.create!(:wiki => @wiki, :title => 'Child1', :parent => page)
159 177 child11 = WikiPage.create!(:wiki => @wiki, :title => 'Child11', :parent => child1)
160 178 child111 = WikiPage.create!(:wiki => @wiki, :title => 'Child111', :parent => child11)
161 179 child2 = WikiPage.create!(:wiki => @wiki, :title => 'Child2', :parent => page)
162 180
163 181 assert_equal %w(Child1 Child11 Child111 Child2), page.descendants.map(&:title).sort
164 182 assert_equal %w(Child1 Child11 Child111 Child2), page.descendants(nil).map(&:title).sort
165 183 assert_equal %w(Child1 Child11 Child2), page.descendants(2).map(&:title).sort
166 184 assert_equal %w(Child1 Child2), page.descendants(1).map(&:title).sort
167 185
168 186 assert_equal %w(Child1 Child11 Child111 Child2 Parent), page.self_and_descendants.map(&:title).sort
169 187 assert_equal %w(Child1 Child11 Child111 Child2 Parent), page.self_and_descendants(nil).map(&:title).sort
170 188 assert_equal %w(Child1 Child11 Child2 Parent), page.self_and_descendants(2).map(&:title).sort
171 189 assert_equal %w(Child1 Child2 Parent), page.self_and_descendants(1).map(&:title).sort
172 190 end
173 191
174 192 def test_diff_for_page_with_deleted_version_should_pick_the_previous_available_version
175 193 WikiContent::Version.find_by_page_id_and_version(1, 2).destroy
176 194
177 195 page = WikiPage.find(1)
178 196 diff = page.diff(3)
179 197 assert_not_nil diff
180 198 assert_equal 3, diff.content_to.version
181 199 assert_equal 1, diff.content_from.version
182 200 end
183 201 end
General Comments 0
You need to be logged in to leave comments. Login now