Wiki: allows single section edit (#2222).

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@7829 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang 2011-11-18 16:25:00 +00:00
parent b38dc9a301
commit 6fc245327c
55 changed files with 440 additions and 41 deletions

View File

@ -100,6 +100,13 @@ class WikiController < ApplicationController
# To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version
@text = @content.text
if params[:section].present?
@section = params[:section].to_i
@text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
render_404 if @text.blank?
end
end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
@ -120,7 +127,17 @@ class WikiController < ApplicationController
redirect_to :action => 'show', :project_id => @project, :id => @page.title
return
end
@content.attributes = params[:content]
@content.comments = params[:content][:comments]
@text = params[:content][:text]
if params[:section].present?
@section = params[:section].to_i
@section_hash = params[:section_hash]
@content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
else
@content.version = params[:content][:version]
@content.text = @text
end
@content.author = User.current
# if page is new @page.save will also save content, but not if page isn't a new record
if (@page.new_record? ? @page.save : @content.save)
@ -132,7 +149,7 @@ class WikiController < ApplicationController
render :action => 'edit'
end
rescue ActiveRecord::StaleObjectError
rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
# Optimistic locking exception
flash.now[:error] = l(:notice_locking_conflict)
render :action => 'edit'

View File

@ -486,11 +486,11 @@ module ApplicationHelper
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
only_path = options.delete(:only_path) == false ? false : true
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
@parsed_headings = []
text = parse_non_pre_blocks(text) do |text|
[:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
[:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
send method_name, text, project, obj, attr, only_path, options
end
end
@ -728,7 +728,23 @@ module ApplicationHelper
end
end
HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
def parse_sections(text, project, obj, attr, only_path, options)
return unless options[:edit_section_links]
section = 0
text.gsub!(HEADING_RE) do
section += 1
if section > 1
content_tag('div',
link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => section)),
:class => 'contextual',
:title => l(:button_edit_section)) + $1
else
$1
end
end
end
# Headings and TOC
# Adds ids and links to headings unless options[:headings] is set to false
@ -736,7 +752,7 @@ module ApplicationHelper
return if options[:headings] == false
text.gsub!(HEADING_RE) do
level, attrs, content = $1.to_i, $2, $3
level, attrs, content = $2.to_i, $3, $4
item = strip_tags(content).strip
anchor = sanitize_anchor_name(item)
# used for single-file wiki export
@ -746,6 +762,33 @@ module ApplicationHelper
end
end
MACROS_RE = /
(!)? # escaping
(
\{\{ # opening tag
([\w]+) # macro name
(\(([^\}]*)\))? # optional arguments
\}\} # closing tag
)
/x unless const_defined?(:MACROS_RE)
# Macros substitution
def parse_macros(text, project, obj, attr, only_path, options)
text.gsub!(MACROS_RE) do
esc, all, macro = $1, $2, $3.downcase
args = ($5 || '').split(',').each(&:strip)
if esc.nil?
begin
exec_macro(macro, obj, args)
rescue => e
"<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
end || all
else
all
end
end
end
TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
# Renders the TOC with given headings

View File

@ -1,3 +1,4 @@
<div class="wiki">
<%= textilizable content, :text, :attachments => content.page.attachments %>
<div class="wiki wiki-page">
<%= textilizable content, :text, :attachments => content.page.attachments,
:edit_section_links => (content.is_a?(WikiContent) && @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) && {:controller => 'wiki', :action => 'edit', :project_id => @page.project, :id => @page.title}) %>
</div>

View File

@ -4,9 +4,13 @@
<% form_for :content, @content, :url => {:action => 'update', :id => @page.title}, :html => {:method => :put, :multipart => true, :id => 'wiki_form'} do |f| %>
<%= f.hidden_field :version %>
<% if @section %>
<%= hidden_field_tag 'section', @section %>
<%= hidden_field_tag 'section_hash', @section_hash %>
<% end %>
<%= error_messages_for 'content' %>
<p><%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %></p>
<p><%= text_area_tag 'content[text]', @text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %></p>
<p><label><%= l(:field_comments) %></label><br /><%= f.text_field :comments, :size => 120 %></p>
<p><label><%=l(:label_attachment_plural)%></label><br /><%= render :partial => 'attachments/form' %></p>

View File

@ -999,3 +999,4 @@ bg:
description_date_range_interval: Изберете диапазон чрез задаване на начална и крайна дати
description_date_from: Въведете начална дата
description_date_to: Въведете крайна дата
button_edit_section: Edit this section

View File

@ -1015,3 +1015,4 @@ bs:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1004,3 +1004,4 @@ ca:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1005,3 +1005,4 @@ cs:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1018,3 +1018,4 @@ da:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1022,3 +1022,4 @@ de:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1001,3 +1001,4 @@ el:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1004,3 +1004,4 @@ en-GB:
description_selected_columns: Selected Columns
label_parent_revision: Parent
label_child_revision: Child
button_edit_section: Edit this section

View File

@ -874,6 +874,7 @@ en:
button_quote: Quote
button_duplicate: Duplicate
button_show: Show
button_edit_section: Edit this section
status_active: active
status_registered: registered

View File

@ -1038,3 +1038,4 @@ es:
label_parent_revision: Parent
label_child_revision: Child
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1005,3 +1005,4 @@ eu:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1004,3 +1004,4 @@ fa:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1022,3 +1022,4 @@ fi:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -850,6 +850,7 @@ fr:
button_quote: Citer
button_duplicate: Dupliquer
button_show: Afficher
button_edit_section: Modifier cette section
status_active: actif
status_registered: enregistré

View File

@ -1013,3 +1013,4 @@ gl:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1006,3 +1006,4 @@ he:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1008,3 +1008,4 @@ hr:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1020,3 +1020,4 @@
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1009,3 +1009,4 @@ id:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1002,3 +1002,4 @@ it:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1031,3 +1031,4 @@ ja:
description_selected_columns: Selected Columns
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1053,3 +1053,4 @@ ko:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1061,3 +1061,4 @@ lt:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -996,3 +996,4 @@ lv:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1001,3 +1001,4 @@ mk:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1002,3 +1002,4 @@ mn:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -983,3 +983,4 @@ nl:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -991,3 +991,4 @@
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1018,3 +1018,4 @@ pl:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1022,3 +1022,4 @@ pt-BR:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1006,3 +1006,4 @@ pt:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -994,3 +994,4 @@ ro:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1114,3 +1114,4 @@ ru:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -996,3 +996,4 @@ sk:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1001,3 +1001,4 @@ sl:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1001,3 +1001,4 @@ sr-YU:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1002,3 +1002,4 @@ sr:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1042,3 +1042,4 @@ sv:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -998,3 +998,4 @@ th:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1020,3 +1020,4 @@ tr:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -997,3 +997,4 @@ uk:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1052,3 +1052,4 @@ vi:
label_child_revision: Child
error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size.
setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
button_edit_section: Edit this section

View File

@ -1081,3 +1081,4 @@
description_date_range_interval: 選擇起始與結束日期以設定範圍區間
description_date_from: 輸入起始日期
description_date_to: 輸入結束日期
button_edit_section: Edit this section

View File

@ -1003,3 +1003,4 @@ zh:
label_child_revision: 子修订
error_scm_annotate_big_text_file: 输入文本内容超长,无法输入。
setting_default_issue_start_date_to_creation_date: 使用当前日期作为新问题的开始日期
button_edit_section: Edit this section

View File

@ -17,6 +17,8 @@
module Redmine
module WikiFormatting
class StaleSectionError < Exception; end
@@formatters = {}
class << self
@ -29,6 +31,10 @@ module Redmine
@@formatters[name.to_s] = {:formatter => formatter, :helper => helper}
end
def formatter
formatter_for(Setting.text_formatting)
end
def formatter_for(name)
entry = @@formatters[name.to_s]
(entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
@ -43,7 +49,7 @@ module Redmine
@@formatters.keys.map
end
def to_html(format, text, options = {}, &block)
def to_html(format, text, options = {})
text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, options[:object], options[:attribute])
# Text retrieved from the cache store may be frozen
# We need to dup it so we can do in-place substitutions with gsub!
@ -53,9 +59,6 @@ module Redmine
else
formatter_for(format).new(text).to_html
end
if block_given?
execute_macros(text, block)
end
text
end
@ -70,33 +73,6 @@ module Redmine
def cache_store
ActionController::Base.cache_store
end
MACROS_RE = /
(!)? # escaping
(
\{\{ # opening tag
([\w]+) # macro name
(\(([^\}]*)\))? # optional arguments
\}\} # closing tag
)
/x unless const_defined?(:MACROS_RE)
# Macros substitution
def execute_macros(text, macros_runner)
text.gsub!(MACROS_RE) do
esc, all, macro = $1, $2, $3.downcase
args = ($5 || '').split(',').each(&:strip)
if esc.nil?
begin
macros_runner.call(macro, args)
rescue => e
"<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
end || all
else
all
end
end
end
end
# Default formatter module

View File

@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'redcloth3'
require 'digest/md5'
module Redmine
module WikiFormatting
@ -38,6 +39,68 @@ module Redmine
super(*RULES).to_s
end
def get_section(index)
section = extract_sections(index)[1]
hash = Digest::MD5.hexdigest(section)
return section, hash
end
def update_section(index, update, hash=nil)
t = extract_sections(index)
if hash.present? && hash != Digest::MD5.hexdigest(t[1])
raise Redmine::WikiFormatting::StaleSectionError
end
t[1] = update unless t[1].blank?
t.reject(&:blank?).join "\n\n"
end
def extract_sections(index)
@pre_list = []
text = self.dup
rip_offtags text
before = ''
s = ''
after = ''
i = 0
l = 1
started = false
ended = false
text.scan(/(((?:.*?)(\A|\r?\n\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))? (.*?)$)|.*)/m).each do |all, content, lf, heading, level|
if heading.nil?
if ended
after << all
elsif started
s << all
else
before << all
end
break
end
i += 1
if ended
after << all
elsif i == index
l = level.to_i
before << content
s << heading
started = true
elsif i > index
s << content
if level.to_i > l
s << heading
else
after << heading
ended = true
end
else
before << all
end
end
sections = [before.strip, s.strip, after.strip]
sections.each {|section| smooth_offtags section}
sections
end
private
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.

View File

@ -358,6 +358,9 @@ table#time-report tbody tr.last-level { font-style: normal; color: #555; }
table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
table#time-report .hours-dec { font-size: 0.9em; }
div.wiki-page .contextual a {opacity: 0.4}
div.wiki-page .contextual a:hover {opacity: 1}
form .attributes { margin-bottom: 8px; }
form .attributes p { padding-top: 1px; padding-bottom: 2px; }
form .attributes select { width: 60%; }

View File

@ -103,4 +103,25 @@ wiki_contents_010:
version: 1
author_id: 1
comments:
wiki_contents_011:
text: |-
h1. Title
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
h2. Heading 1
Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.
h2. Heading 2
Morbi facilisis accumsan orci non pharetra.
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 11
id: 11
version: 3
author_id: 1
comments:

View File

@ -69,3 +69,10 @@ wiki_pages_010:
wiki_id: 1
protected: false
parent_id:
wiki_pages_011:
created_on: 2007-03-08 00:18:07 +01:00
title: Page_with_sections
id: 11
wiki_id: 1
protected: false
parent_id:

View File

@ -118,6 +118,44 @@ class WikiControllerTest < ActionController::TestCase
assert_equal 'testfile.txt', page.attachments.first.filename
end
def test_edit_page
@request.session[:user_id] = 2
get :edit, :project_id => 'ecookbook', :id => 'Another_page'
assert_response :success
assert_template 'edit'
assert_tag 'textarea',
:attributes => { :name => 'content[text]' },
:content => WikiPage.find_by_title('Another_page').content.text
end
def test_edit_section
@request.session[:user_id] = 2
get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 2
assert_response :success
assert_template 'edit'
page = WikiPage.find_by_title('Page_with_sections')
section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
assert_tag 'textarea',
:attributes => { :name => 'content[text]' },
:content => section
assert_tag 'input',
:attributes => { :name => 'section', :type => 'hidden', :value => '2' }
assert_tag 'input',
:attributes => { :name => 'section_hash', :type => 'hidden', :value => hash }
end
def test_edit_invalid_section_should_respond_with_404
@request.session[:user_id] = 2
get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 10
assert_response 404
end
def test_update_page
@request.session[:user_id] = 2
assert_no_difference 'WikiPage.count' do
@ -200,6 +238,83 @@ class WikiControllerTest < ActionController::TestCase
assert_equal 2, c.version
end
def test_update_section
@request.session[:user_id] = 2
page = WikiPage.find_by_title('Page_with_sections')
section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
text = page.content.text
assert_no_difference 'WikiPage.count' do
assert_no_difference 'WikiContent.count' do
assert_difference 'WikiContent::Version.count' do
put :update, :project_id => 1, :id => 'Page_with_sections',
:content => {
:text => "New section content",
:version => 3
},
:section => 2,
:section_hash => hash
end
end
end
assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text
end
def test_update_section_should_allow_stale_page_update
@request.session[:user_id] = 2
page = WikiPage.find_by_title('Page_with_sections')
section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
text = page.content.text
assert_no_difference 'WikiPage.count' do
assert_no_difference 'WikiContent.count' do
assert_difference 'WikiContent::Version.count' do
put :update, :project_id => 1, :id => 'Page_with_sections',
:content => {
:text => "New section content",
:version => 2 # Current version is 3
},
:section => 2,
:section_hash => hash
end
end
end
assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
page.reload
assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text
assert_equal 4, page.content.version
end
def test_update_section_should_not_allow_stale_section_update
@request.session[:user_id] = 2
assert_no_difference 'WikiPage.count' do
assert_no_difference 'WikiContent.count' do
assert_no_difference 'WikiContent::Version.count' do
put :update, :project_id => 1, :id => 'Page_with_sections',
:content => {
:comments => 'My comments',
:text => "Text should not be lost",
:version => 3
},
:section => 2,
:section_hash => Digest::MD5.hexdigest("wrong hash")
end
end
end
assert_response :success
assert_template 'edit'
assert_tag :div,
:attributes => { :class => /error/ },
:content => /Data has been updated by another user/
assert_tag 'textarea',
:attributes => { :name => 'content[text]' },
:content => /Text should not be lost/
assert_tag 'input',
:attributes => { :name => 'content[comments]', :value => 'My comments' }
end
def test_preview
@request.session[:user_id] = 2
xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',

View File

@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require File.expand_path('../../../../../test_helper', __FILE__)
require 'digest/md5'
class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
@ -203,6 +204,101 @@ EXPECTED
expected = '<p><img src="/images/comment.png&quot;onclick=&amp;#x61;&amp;#x6c;&amp;#x65;&amp;#x72;&amp;#x74;&amp;#x28;&amp;#x27;&amp;#x58;&amp;#x53;&amp;#x53;&amp;#x27;&amp;#x29;;&amp;#x22;" alt="" /></p>'
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
end
STR_WITHOUT_PRE = [
# 0
"h1. Title
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
# 1
"h2. Heading 2
Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.",
# 2
"h2. Heading 2
Morbi facilisis accumsan orci non pharetra.
h3. Heading 3
Nulla nunc nisi, egestas in ornare vel, posuere ac libero.",
# 3
"h3. Heading 3
Praesent eget turpis nibh, a lacinia nulla.",
# 4
"h2. Heading 2
Ut rhoncus elementum adipiscing."]
TEXT_WITHOUT_PRE = STR_WITHOUT_PRE.join("\n\n").freeze
def test_get_section_should_return_the_requested_section_and_its_hash
assert_section_with_hash STR_WITHOUT_PRE[1], TEXT_WITHOUT_PRE, 2
assert_section_with_hash STR_WITHOUT_PRE[2..3].join("\n\n"), TEXT_WITHOUT_PRE, 3
assert_section_with_hash STR_WITHOUT_PRE[3], TEXT_WITHOUT_PRE, 5
assert_section_with_hash STR_WITHOUT_PRE[4], TEXT_WITHOUT_PRE, 6
assert_section_with_hash '', TEXT_WITHOUT_PRE, 0
assert_section_with_hash '', TEXT_WITHOUT_PRE, 10
end
def test_update_section_should_update_the_requested_section
replacement = "New text"
assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement)
assert_equal [STR_WITHOUT_PRE[0..1], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(3, replacement)
assert_equal [STR_WITHOUT_PRE[0..2], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(5, replacement)
assert_equal [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement)
assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement)
assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(10, replacement)
end
def test_update_section_with_hash_should_update_the_requested_section
replacement = "New text"
assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"),
@formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement, Digest::MD5.hexdigest(STR_WITHOUT_PRE[1]))
end
def test_update_section_with_wrong_hash_should_raise_an_error
assert_raise Redmine::WikiFormatting::StaleSectionError do
@formatter.new(TEXT_WITHOUT_PRE).update_section(2, "New text", Digest::MD5.hexdigest("Old text"))
end
end
STR_WITH_PRE = [
# 0
"h1. Title
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
# 1
"h2. Heading 2
Morbi facilisis accumsan orci non pharetra.
<pre>
Pre Content:
h2. Inside pre
Morbi facilisis accumsan orci non pharetra.
</pre>",
# 2
"h3. Heading 3
Nulla nunc nisi, egestas in ornare vel, posuere ac libero."]
def test_get_section_should_ignore_pre_content
text = STR_WITH_PRE.join("\n\n")
assert_section_with_hash STR_WITH_PRE[1..2].join("\n\n"), text, 2
assert_section_with_hash STR_WITH_PRE[2], text, 3
end
private
@ -215,4 +311,13 @@ EXPECTED
def to_html(text)
@formatter.new(text).to_html
end
def assert_section_with_hash(expected, text, index)
result = @formatter.new(text).get_section(index)
assert_kind_of Array, result
assert_equal 2, result.size
assert_equal expected, result.first, "section content did not match"
assert_equal Digest::MD5.hexdigest(expected), result.last, "section hash did not match"
end
end