From 6fc245327ce5bb4bb75f1378424635e347dfcd02 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 18 Nov 2011 16:25:00 +0000 Subject: [PATCH] Wiki: allows single section edit (#2222). git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@7829 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/wiki_controller.rb | 21 +++- app/helpers/application_helper.rb | 51 +++++++- app/views/wiki/_content.html.erb | 5 +- app/views/wiki/edit.html.erb | 6 +- config/locales/bg.yml | 1 + config/locales/bs.yml | 1 + config/locales/ca.yml | 1 + config/locales/cs.yml | 1 + config/locales/da.yml | 1 + config/locales/de.yml | 1 + config/locales/el.yml | 1 + config/locales/en-GB.yml | 1 + config/locales/en.yml | 1 + config/locales/es.yml | 1 + config/locales/eu.yml | 1 + config/locales/fa.yml | 1 + config/locales/fi.yml | 1 + config/locales/fr.yml | 1 + config/locales/gl.yml | 1 + config/locales/he.yml | 1 + config/locales/hr.yml | 1 + config/locales/hu.yml | 1 + config/locales/id.yml | 1 + config/locales/it.yml | 1 + config/locales/ja.yml | 1 + config/locales/ko.yml | 1 + config/locales/lt.yml | 1 + config/locales/lv.yml | 1 + config/locales/mk.yml | 1 + config/locales/mn.yml | 1 + config/locales/nl.yml | 1 + config/locales/no.yml | 1 + config/locales/pl.yml | 1 + config/locales/pt-BR.yml | 1 + config/locales/pt.yml | 1 + config/locales/ro.yml | 1 + config/locales/ru.yml | 1 + config/locales/sk.yml | 1 + config/locales/sl.yml | 1 + config/locales/sr-YU.yml | 1 + config/locales/sr.yml | 1 + config/locales/sv.yml | 1 + config/locales/th.yml | 1 + config/locales/tr.yml | 1 + config/locales/uk.yml | 1 + config/locales/vi.yml | 1 + config/locales/zh-TW.yml | 1 + config/locales/zh.yml | 1 + lib/redmine/wiki_formatting.rb | 38 ++---- .../wiki_formatting/textile/formatter.rb | 63 ++++++++++ public/stylesheets/application.css | 3 + test/fixtures/wiki_contents.yml | 23 +++- test/fixtures/wiki_pages.yml | 7 ++ test/functional/wiki_controller_test.rb | 115 ++++++++++++++++++ .../wiki_formatting/textile_formatter_test.rb | 105 ++++++++++++++++ 55 files changed, 440 insertions(+), 41 deletions(-) diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 8111a617b..721ea28de 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -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' diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index b037e72c8..bec54a83e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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)>/i unless const_defined?(:HEADING_RE) + HEADING_RE = /(]+)?>(.+?)<\/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 + "
Error executing the #{macro} macro (#{e})
" + end || all + else + all + end + end + end + TOC_RE = /

\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) # Renders the TOC with given headings diff --git a/app/views/wiki/_content.html.erb b/app/views/wiki/_content.html.erb index 421d26cbb..0e747799d 100644 --- a/app/views/wiki/_content.html.erb +++ b/app/views/wiki/_content.html.erb @@ -1,3 +1,4 @@ -

- <%= textilizable content, :text, :attachments => content.page.attachments %> +
+ <%= 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}) %>
diff --git a/app/views/wiki/edit.html.erb b/app/views/wiki/edit.html.erb index ae04a1a51..c628c2f7a 100644 --- a/app/views/wiki/edit.html.erb +++ b/app/views/wiki/edit.html.erb @@ -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' %> -

<%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %>

+

<%= text_area_tag 'content[text]', @text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %>


<%= f.text_field :comments, :size => 120 %>


<%= render :partial => 'attachments/form' %>

diff --git a/config/locales/bg.yml b/config/locales/bg.yml index a7dabbda1..17bbdbcc2 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -999,3 +999,4 @@ bg: description_date_range_interval: Изберете диапазон чрез задаване на начална и крайна дати description_date_from: Въведете начална дата description_date_to: Въведете крайна дата + button_edit_section: Edit this section diff --git a/config/locales/bs.yml b/config/locales/bs.yml index 3b8bb76dc..653114b88 100644 --- a/config/locales/bs.yml +++ b/config/locales/bs.yml @@ -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 diff --git a/config/locales/ca.yml b/config/locales/ca.yml index f0f17c55b..5b6480568 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -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 diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 99e27b0d9..3c6a7fa64 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -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 diff --git a/config/locales/da.yml b/config/locales/da.yml index 56266600b..bec03cea9 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -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 diff --git a/config/locales/de.yml b/config/locales/de.yml index ce96a8644..803579c75 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -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 diff --git a/config/locales/el.yml b/config/locales/el.yml index 6a15477de..a3aea6555 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -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 diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 964ae1a52..ad5871be0 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -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 diff --git a/config/locales/en.yml b/config/locales/en.yml index db13b545b..8c4325506 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/config/locales/es.yml b/config/locales/es.yml index 4c8b248bb..d9db6ca89 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -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 diff --git a/config/locales/eu.yml b/config/locales/eu.yml index b9ea8ef40..6d8a30570 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -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 diff --git a/config/locales/fa.yml b/config/locales/fa.yml index a342b3611..02ceb84c5 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -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 diff --git a/config/locales/fi.yml b/config/locales/fi.yml index e02e97dd8..5b80d2e71 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -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 diff --git a/config/locales/fr.yml b/config/locales/fr.yml index b119c1afe..ea130ad10 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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é diff --git a/config/locales/gl.yml b/config/locales/gl.yml index ff0c31806..3a6f122c7 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -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 diff --git a/config/locales/he.yml b/config/locales/he.yml index da1008fb4..c70c005d3 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -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 diff --git a/config/locales/hr.yml b/config/locales/hr.yml index 672dc71ff..f1f60e134 100644 --- a/config/locales/hr.yml +++ b/config/locales/hr.yml @@ -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 diff --git a/config/locales/hu.yml b/config/locales/hu.yml index b93d84525..345c9f4d7 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -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 diff --git a/config/locales/id.yml b/config/locales/id.yml index 4b7d54c4a..dc790edd4 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -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 diff --git a/config/locales/it.yml b/config/locales/it.yml index c607caf05..d59b443ec 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -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 diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 80cf49bf9..e3bd477ea 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -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 diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 6e8cefa6e..8a042ad8b 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -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 diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 71c9ea02f..ad7699697 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -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 diff --git a/config/locales/lv.yml b/config/locales/lv.yml index 7839e8a60..654e13009 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -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 diff --git a/config/locales/mk.yml b/config/locales/mk.yml index 96aea4fe2..a215cc702 100644 --- a/config/locales/mk.yml +++ b/config/locales/mk.yml @@ -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 diff --git a/config/locales/mn.yml b/config/locales/mn.yml index cc4947e27..b243feb2e 100644 --- a/config/locales/mn.yml +++ b/config/locales/mn.yml @@ -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 diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 8e33be66b..58caf9419 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -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 diff --git a/config/locales/no.yml b/config/locales/no.yml index a76c925eb..1e189d547 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -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 diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 5c70da426..f38fed23f 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -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 diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 87f821839..31bdda551 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -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 diff --git a/config/locales/pt.yml b/config/locales/pt.yml index a6ec7e1e6..4ae10b799 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -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 diff --git a/config/locales/ro.yml b/config/locales/ro.yml index 1bd5c0253..b040674e0 100644 --- a/config/locales/ro.yml +++ b/config/locales/ro.yml @@ -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 diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 5ff4076b4..7ea6c8bd2 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -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 diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 9d9c282fa..f114ee44d 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -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 diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 80b9ea7a1..823b29091 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -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 diff --git a/config/locales/sr-YU.yml b/config/locales/sr-YU.yml index 723446bcb..12a38c3eb 100644 --- a/config/locales/sr-YU.yml +++ b/config/locales/sr-YU.yml @@ -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 diff --git a/config/locales/sr.yml b/config/locales/sr.yml index ffef453aa..7ecf860bb 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -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 diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 90727b36e..31aedcbad 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -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 diff --git a/config/locales/th.yml b/config/locales/th.yml index 407e3e4d4..ba0fafb96 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -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 diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 508e3f9a6..86bb8dd10 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -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 diff --git a/config/locales/uk.yml b/config/locales/uk.yml index edc4586dd..be10b95c6 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -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 diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 7718e429b..fd1657c62 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -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 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index b99afe950..aa3ad0575 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -1081,3 +1081,4 @@ description_date_range_interval: 選擇起始與結束日期以設定範圍區間 description_date_from: 輸入起始日期 description_date_to: 輸入結束日期 + button_edit_section: Edit this section diff --git a/config/locales/zh.yml b/config/locales/zh.yml index c79fe5980..19abd880e 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -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 diff --git a/lib/redmine/wiki_formatting.rb b/lib/redmine/wiki_formatting.rb index 0635aad8b..2f25fe0e6 100644 --- a/lib/redmine/wiki_formatting.rb +++ b/lib/redmine/wiki_formatting.rb @@ -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 - "
Error executing the #{macro} macro (#{e})
" - end || all - else - all - end - end - end end # Default formatter module diff --git a/lib/redmine/wiki_formatting/textile/formatter.rb b/lib/redmine/wiki_formatting/textile/formatter.rb index 520e3f9ad..1beb9563c 100644 --- a/lib/redmine/wiki_formatting/textile/formatter.rb +++ b/lib/redmine/wiki_formatting/textile/formatter.rb @@ -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. diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 911646104..4f708c907 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -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%; } diff --git a/test/fixtures/wiki_contents.yml b/test/fixtures/wiki_contents.yml index acf5086af..b5fd01080 100644 --- a/test/fixtures/wiki_contents.yml +++ b/test/fixtures/wiki_contents.yml @@ -103,4 +103,25 @@ wiki_contents_010: version: 1 author_id: 1 comments: - \ No newline at end of file +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: + diff --git a/test/fixtures/wiki_pages.yml b/test/fixtures/wiki_pages.yml index 2d2e2eceb..8a00e4af4 100644 --- a/test/fixtures/wiki_pages.yml +++ b/test/fixtures/wiki_pages.yml @@ -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: diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb index 48db9e650..1274e676b 100644 --- a/test/functional/wiki_controller_test.rb +++ b/test/functional/wiki_controller_test.rb @@ -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', diff --git a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb index 4d9f90518..ec9b883b0 100644 --- a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb @@ -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 = '

' 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 Content:
+
+h2. Inside pre
+
+Morbi facilisis accumsan orci non pharetra.
+
", + # 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 -- 2.39.5