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:
parent
b38dc9a301
commit
6fc245327c
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -999,3 +999,4 @@ bg:
|
|||
description_date_range_interval: Изберете диапазон чрез задаване на начална и крайна дати
|
||||
description_date_from: Въведете начална дата
|
||||
description_date_to: Въведете крайна дата
|
||||
button_edit_section: Edit this section
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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é
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1081,3 +1081,4 @@
|
|||
description_date_range_interval: 選擇起始與結束日期以設定範圍區間
|
||||
description_date_from: 輸入起始日期
|
||||
description_date_to: 輸入結束日期
|
||||
button_edit_section: Edit this section
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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%; }
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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"onclick=&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#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
|
||||
|
|
Loading…
Reference in New Issue