##// END OF EJS Templates
Handle the case of a text formatter that doesn't support section edit (#2222)....
Jean-Philippe Lang -
r7711:1e8a9da13168
parent child
Show More
@@ -1,306 +1,309
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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
20 20 # The WikiController follows the Rails REST controller pattern but with
21 21 # a few differences
22 22 #
23 23 # * index - shows a list of WikiPages grouped by page or date
24 24 # * new - not used
25 25 # * create - not used
26 26 # * show - will also show the form for creating a new wiki page
27 27 # * edit - used to edit an existing or new page
28 28 # * update - used to save a wiki page update to the database, including new pages
29 29 # * destroy - normal
30 30 #
31 31 # Other member and collection methods are also used
32 32 #
33 33 # TODO: still being worked on
34 34 class WikiController < ApplicationController
35 35 default_search_scope :wiki_pages
36 36 before_filter :find_wiki, :authorize
37 37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
38 38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
39 39
40 40 helper :attachments
41 41 include AttachmentsHelper
42 42 helper :watchers
43 43 include Redmine::Export::PDF
44 44
45 45 # List of pages, sorted alphabetically and by parent (hierarchy)
46 46 def index
47 47 load_pages_for_index
48 48 @pages_by_parent_id = @pages.group_by(&:parent_id)
49 49 end
50 50
51 51 # List of page, by last update
52 52 def date_index
53 53 load_pages_for_index
54 54 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
55 55 end
56 56
57 57 # display a page (in editing mode if it doesn't exist)
58 58 def show
59 59 if @page.new_record?
60 60 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
61 61 edit
62 62 render :action => 'edit'
63 63 else
64 64 render_404
65 65 end
66 66 return
67 67 end
68 68 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
69 69 # Redirects user to the current version if he's not allowed to view previous versions
70 70 redirect_to :version => nil
71 71 return
72 72 end
73 73 @content = @page.content_for_version(params[:version])
74 74 if User.current.allowed_to?(:export_wiki_pages, @project)
75 75 if params[:format] == 'pdf'
76 76 send_data(wiki_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
77 77 return
78 78 elsif params[:format] == 'html'
79 79 export = render_to_string :action => 'export', :layout => false
80 80 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
81 81 return
82 82 elsif params[:format] == 'txt'
83 83 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
84 84 return
85 85 end
86 86 end
87 87 @editable = editable?
88 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) && params[:version].nil?
88 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
89 params[:version].nil? &&
90 Redmine::WikiFormatting.supports_section_edit?
91
89 92 render :action => 'show'
90 93 end
91 94
92 95 # edit an existing page or a new one
93 96 def edit
94 97 return render_403 unless editable?
95 98 @page.content = WikiContent.new(:page => @page) if @page.new_record?
96 99
97 100 @content = @page.content_for_version(params[:version])
98 101 @content.text = initial_page_content(@page) if @content.text.blank?
99 102 # don't keep previous comment
100 103 @content.comments = nil
101 104
102 105 # To prevent StaleObjectError exception when reverting to a previous version
103 106 @content.version = @page.content.version
104 107
105 108 @text = @content.text
106 if params[:section].present?
109 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
107 110 @section = params[:section].to_i
108 111 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
109 112 render_404 if @text.blank?
110 113 end
111 114 end
112 115
113 116 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
114 117 # Creates a new page or updates an existing one
115 118 def update
116 119 return render_403 unless editable?
117 120 @page.content = WikiContent.new(:page => @page) if @page.new_record?
118 121
119 122 @content = @page.content_for_version(params[:version])
120 123 @content.text = initial_page_content(@page) if @content.text.blank?
121 124 # don't keep previous comment
122 125 @content.comments = nil
123 126
124 127 if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
125 128 attachments = Attachment.attach_files(@page, params[:attachments])
126 129 render_attachment_warning_if_needed(@page)
127 130 # don't save if text wasn't changed
128 131 redirect_to :action => 'show', :project_id => @project, :id => @page.title
129 132 return
130 133 end
131 134
132 135 @content.comments = params[:content][:comments]
133 136 @text = params[:content][:text]
134 if params[:section].present?
137 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
135 138 @section = params[:section].to_i
136 139 @section_hash = params[:section_hash]
137 140 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
138 141 else
139 142 @content.version = params[:content][:version]
140 143 @content.text = @text
141 144 end
142 145 @content.author = User.current
143 146 # if page is new @page.save will also save content, but not if page isn't a new record
144 147 if (@page.new_record? ? @page.save : @content.save)
145 148 attachments = Attachment.attach_files(@page, params[:attachments])
146 149 render_attachment_warning_if_needed(@page)
147 150 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
148 151 redirect_to :action => 'show', :project_id => @project, :id => @page.title
149 152 else
150 153 render :action => 'edit'
151 154 end
152 155
153 156 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
154 157 # Optimistic locking exception
155 158 flash.now[:error] = l(:notice_locking_conflict)
156 159 render :action => 'edit'
157 160 end
158 161
159 162 # rename a page
160 163 def rename
161 164 return render_403 unless editable?
162 165 @page.redirect_existing_links = true
163 166 # used to display the *original* title if some AR validation errors occur
164 167 @original_title = @page.pretty_title
165 168 if request.post? && @page.update_attributes(params[:wiki_page])
166 169 flash[:notice] = l(:notice_successful_update)
167 170 redirect_to :action => 'show', :project_id => @project, :id => @page.title
168 171 end
169 172 end
170 173
171 174 verify :method => :post, :only => :protect, :redirect_to => { :action => :show }
172 175 def protect
173 176 @page.update_attribute :protected, params[:protected]
174 177 redirect_to :action => 'show', :project_id => @project, :id => @page.title
175 178 end
176 179
177 180 # show page history
178 181 def history
179 182 @version_count = @page.content.versions.count
180 183 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
181 184 # don't load text
182 185 @versions = @page.content.versions.find :all,
183 186 :select => "id, author_id, comments, updated_on, version",
184 187 :order => 'version DESC',
185 188 :limit => @version_pages.items_per_page + 1,
186 189 :offset => @version_pages.current.offset
187 190
188 191 render :layout => false if request.xhr?
189 192 end
190 193
191 194 def diff
192 195 @diff = @page.diff(params[:version], params[:version_from])
193 196 render_404 unless @diff
194 197 end
195 198
196 199 def annotate
197 200 @annotate = @page.annotate(params[:version])
198 201 render_404 unless @annotate
199 202 end
200 203
201 204 verify :method => :delete, :only => [:destroy], :redirect_to => { :action => :show }
202 205 # Removes a wiki page and its history
203 206 # Children can be either set as root pages, removed or reassigned to another parent page
204 207 def destroy
205 208 return render_403 unless editable?
206 209
207 210 @descendants_count = @page.descendants.size
208 211 if @descendants_count > 0
209 212 case params[:todo]
210 213 when 'nullify'
211 214 # Nothing to do
212 215 when 'destroy'
213 216 # Removes all its descendants
214 217 @page.descendants.each(&:destroy)
215 218 when 'reassign'
216 219 # Reassign children to another parent page
217 220 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
218 221 return unless reassign_to
219 222 @page.children.each do |child|
220 223 child.update_attribute(:parent, reassign_to)
221 224 end
222 225 else
223 226 @reassignable_to = @wiki.pages - @page.self_and_descendants
224 227 return
225 228 end
226 229 end
227 230 @page.destroy
228 231 redirect_to :action => 'index', :project_id => @project
229 232 end
230 233
231 234 # Export wiki to a single html file
232 235 def export
233 236 if User.current.allowed_to?(:export_wiki_pages, @project)
234 237 @pages = @wiki.pages.find :all, :order => 'title'
235 238 export = render_to_string :action => 'export_multiple', :layout => false
236 239 send_data(export, :type => 'text/html', :filename => "wiki.html")
237 240 else
238 241 redirect_to :action => 'show', :project_id => @project, :id => nil
239 242 end
240 243 end
241 244
242 245 def preview
243 246 page = @wiki.find_page(params[:id])
244 247 # page is nil when previewing a new page
245 248 return render_403 unless page.nil? || editable?(page)
246 249 if page
247 250 @attachements = page.attachments
248 251 @previewed = page.content
249 252 end
250 253 @text = params[:content][:text]
251 254 render :partial => 'common/preview'
252 255 end
253 256
254 257 def add_attachment
255 258 return render_403 unless editable?
256 259 attachments = Attachment.attach_files(@page, params[:attachments])
257 260 render_attachment_warning_if_needed(@page)
258 261 redirect_to :action => 'show', :id => @page.title, :project_id => @project
259 262 end
260 263
261 264 private
262 265
263 266 def find_wiki
264 267 @project = Project.find(params[:project_id])
265 268 @wiki = @project.wiki
266 269 render_404 unless @wiki
267 270 rescue ActiveRecord::RecordNotFound
268 271 render_404
269 272 end
270 273
271 274 # Finds the requested page or a new page if it doesn't exist
272 275 def find_existing_or_new_page
273 276 @page = @wiki.find_or_new_page(params[:id])
274 277 if @wiki.page_found_with_redirect?
275 278 redirect_to params.update(:id => @page.title)
276 279 end
277 280 end
278 281
279 282 # Finds the requested page and returns a 404 error if it doesn't exist
280 283 def find_existing_page
281 284 @page = @wiki.find_page(params[:id])
282 285 if @page.nil?
283 286 render_404
284 287 return
285 288 end
286 289 if @wiki.page_found_with_redirect?
287 290 redirect_to params.update(:id => @page.title)
288 291 end
289 292 end
290 293
291 294 # Returns true if the current user is allowed to edit the page, otherwise false
292 295 def editable?(page = @page)
293 296 page.editable_by?(User.current)
294 297 end
295 298
296 299 # Returns the default content of a new wiki page
297 300 def initial_page_content(page)
298 301 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
299 302 extend helper unless self.instance_of?(helper)
300 303 helper.instance_method(:initial_page_content).bind(self).call(page)
301 304 end
302 305
303 306 def load_pages_for_index
304 307 @pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project})
305 308 end
306 309 end
@@ -1,107 +1,112
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 module Redmine
19 19 module WikiFormatting
20 20 class StaleSectionError < Exception; end
21 21
22 22 @@formatters = {}
23 23
24 24 class << self
25 25 def map
26 26 yield self
27 27 end
28 28
29 29 def register(name, formatter, helper)
30 30 raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name.to_s]
31 31 @@formatters[name.to_s] = {:formatter => formatter, :helper => helper}
32 32 end
33 33
34 34 def formatter
35 35 formatter_for(Setting.text_formatting)
36 36 end
37 37
38 38 def formatter_for(name)
39 39 entry = @@formatters[name.to_s]
40 40 (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
41 41 end
42 42
43 43 def helper_for(name)
44 44 entry = @@formatters[name.to_s]
45 45 (entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper
46 46 end
47 47
48 48 def format_names
49 49 @@formatters.keys.map
50 50 end
51 51
52 52 def to_html(format, text, options = {})
53 53 text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, options[:object], options[:attribute])
54 54 # Text retrieved from the cache store may be frozen
55 55 # We need to dup it so we can do in-place substitutions with gsub!
56 56 cache_store.fetch cache_key do
57 57 formatter_for(format).new(text).to_html
58 58 end.dup
59 59 else
60 60 formatter_for(format).new(text).to_html
61 61 end
62 62 text
63 63 end
64 64
65 # Returns true if the text formatter supports single section edit
66 def supports_section_edit?
67 (formatter.instance_methods & ['update_section', :update_section]).any?
68 end
69
65 70 # Returns a cache key for the given text +format+, +object+ and +attribute+ or nil if no caching should be done
66 71 def cache_key_for(format, object, attribute)
67 72 if object && attribute && !object.new_record? && object.respond_to?(:updated_on) && !format.blank?
68 73 "formatted_text/#{format}/#{object.class.model_name.cache_key}/#{object.id}-#{attribute}-#{object.updated_on.to_s(:number)}"
69 74 end
70 75 end
71 76
72 77 # Returns the cache store used to cache HTML output
73 78 def cache_store
74 79 ActionController::Base.cache_store
75 80 end
76 81 end
77 82
78 83 # Default formatter module
79 84 module NullFormatter
80 85 class Formatter
81 86 include ActionView::Helpers::TagHelper
82 87 include ActionView::Helpers::TextHelper
83 88 include ActionView::Helpers::UrlHelper
84 89
85 90 def initialize(text)
86 91 @text = text
87 92 end
88 93
89 94 def to_html(*args)
90 95 simple_format(auto_link(CGI::escapeHTML(@text)))
91 96 end
92 97 end
93 98
94 99 module Helper
95 100 def wikitoolbar_for(field_id)
96 101 end
97 102
98 103 def heads_for_wiki_formatter
99 104 end
100 105
101 106 def initial_page_content(page)
102 107 page.pretty_title.to_s
103 108 end
104 109 end
105 110 end
106 111 end
107 112 end
@@ -1,45 +1,55
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 Redmine::WikiFormattingTest < ActiveSupport::TestCase
21 21
22 22 def test_textile_formatter
23 23 assert_equal Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting.formatter_for('textile')
24 24 assert_equal Redmine::WikiFormatting::Textile::Helper, Redmine::WikiFormatting.helper_for('textile')
25 25 end
26 26
27 27 def test_null_formatter
28 28 assert_equal Redmine::WikiFormatting::NullFormatter::Formatter, Redmine::WikiFormatting.formatter_for('')
29 29 assert_equal Redmine::WikiFormatting::NullFormatter::Helper, Redmine::WikiFormatting.helper_for('')
30 30 end
31 31
32 32 def test_should_link_urls_and_email_addresses
33 33 raw = <<-DIFF
34 34 This is a sample *text* with a link: http://www.redmine.org
35 35 and an email address foo@example.net
36 36 DIFF
37 37
38 38 expected = <<-EXPECTED
39 39 <p>This is a sample *text* with a link: <a href="http://www.redmine.org">http://www.redmine.org</a><br />
40 40 and an email address <a href="mailto:foo@example.net">foo@example.net</a></p>
41 41 EXPECTED
42 42
43 43 assert_equal expected.gsub(%r{[\r\n\t]}, ''), Redmine::WikiFormatting::NullFormatter::Formatter.new(raw).to_html.gsub(%r{[\r\n\t]}, '')
44 44 end
45
46 def test_supports_section_edit
47 with_settings :text_formatting => 'textile' do
48 assert_equal true, Redmine::WikiFormatting.supports_section_edit?
49 end
50
51 with_settings :text_formatting => '' do
52 assert_equal false, Redmine::WikiFormatting.supports_section_edit?
53 end
54 end
45 55 end
General Comments 0
You need to be logged in to leave comments. Login now