]> source.dussan.org Git - redmine.git/commitdiff
Wiki: allows single section edit (#2222).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Fri, 18 Nov 2011 16:25:00 +0000 (16:25 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Fri, 18 Nov 2011 16:25:00 +0000 (16:25 +0000)
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@7829 e93f8b46-1217-0410-a6f0-8f06a7374b81

55 files changed:
app/controllers/wiki_controller.rb
app/helpers/application_helper.rb
app/views/wiki/_content.html.erb
app/views/wiki/edit.html.erb
config/locales/bg.yml
config/locales/bs.yml
config/locales/ca.yml
config/locales/cs.yml
config/locales/da.yml
config/locales/de.yml
config/locales/el.yml
config/locales/en-GB.yml
config/locales/en.yml
config/locales/es.yml
config/locales/eu.yml
config/locales/fa.yml
config/locales/fi.yml
config/locales/fr.yml
config/locales/gl.yml
config/locales/he.yml
config/locales/hr.yml
config/locales/hu.yml
config/locales/id.yml
config/locales/it.yml
config/locales/ja.yml
config/locales/ko.yml
config/locales/lt.yml
config/locales/lv.yml
config/locales/mk.yml
config/locales/mn.yml
config/locales/nl.yml
config/locales/no.yml
config/locales/pl.yml
config/locales/pt-BR.yml
config/locales/pt.yml
config/locales/ro.yml
config/locales/ru.yml
config/locales/sk.yml
config/locales/sl.yml
config/locales/sr-YU.yml
config/locales/sr.yml
config/locales/sv.yml
config/locales/th.yml
config/locales/tr.yml
config/locales/uk.yml
config/locales/vi.yml
config/locales/zh-TW.yml
config/locales/zh.yml
lib/redmine/wiki_formatting.rb
lib/redmine/wiki_formatting/textile/formatter.rb
public/stylesheets/application.css
test/fixtures/wiki_contents.yml
test/fixtures/wiki_pages.yml
test/functional/wiki_controller_test.rb
test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb

index 8111a617bcfd28baba2b2fdfe77228b473f97d09..721ea28de268565f34575a28dccafddf1c73eb1e 100644 (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'
index b037e72c8852682cf1845f41ee53fa9876be8853..bec54a83eb5bdf90a1f92eb7c5c1591dd5715995 100644 (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
index 421d26cbbe4d7a61d6eb92ea18c40f0e96f6920c..0e747799db7dc31952a7ca369be6fd36594b33eb 100644 (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>
index ae04a1a51f47f0a415b196e105533c1a80331da9..c628c2f7a75a7f65a66aca778458b4b5a14e3611 100644 (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>
 
index a7dabbda15b3c060995223942a5f6e971d20d4b7..17bbdbcc20a2c6642cedbd396f010d92bfc31074 100644 (file)
@@ -999,3 +999,4 @@ bg:
   description_date_range_interval: Изберете диапазон чрез задаване на начална и крайна дати
   description_date_from: Въведете начална дата
   description_date_to: Въведете крайна дата
+  button_edit_section: Edit this section
index 3b8bb76dc02f4a470f47899096ed1abdbae0a94e..653114b8848e7717e20cfe711b2f76bc6e07ad9b 100644 (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
index f0f17c55b76817d058ff81b87db5c852792e839d..5b648056877c12a4eb0725585abb24f1514bd22d 100644 (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
index 99e27b0d93aac675c4ace0fde5d56bc3d3001005..3c6a7fa6489f81c9d2e8d861f741c12a7a8ca8c1 100644 (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
index 56266600b41b6087f282f0aec07c9ab2b82f139c..bec03cea9b555e996a54835c6a84e79da73b60b7 100644 (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
index ce96a8644ebdac551cd2bca5e30549fc4699a03d..803579c75cfa453d2c45502cdda3cc2c22b75030 100644 (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
index 6a15477def49ab9dcfda7e7fa1661a4be3a20fc4..a3aea6555a80363d89d3e482728cf370359955ec 100644 (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
index 964ae1a52909bde73fca0bc1cd2e07010a80cb9e..ad5871be0a5b86a1a8b085b8135020db667e4716 100644 (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
index db13b545bf4423e22d84c4210243e454f67fb0f7..8c432550619328ebe308d41a2a72ff052ccf3925 100644 (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
index 4c8b248bbd79f0d398bce37bca082dee5f18f02f..d9db6ca89d5fd5031cb16535c5d8e76b0ad60bd7 100644 (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
index b9ea8ef406d441275044ef5e7cc0a9c16b7ddd6d..6d8a30570cabbb43e2dd0ff2c7bbb8146867e4d6 100644 (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
index a342b36113b7bded12f64f32868980f95227bbe7..02ceb84c5d1c5f72f07bf28faff384ad00918b02 100644 (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
index e02e97dd8afeea19511f8caa61f6d980ed78f79c..5b80d2e7118e7463547f8abf9ebb552e424b8fe7 100644 (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
index b119c1afee22c28e5dfe6728c70d6b1e443e6e6e..ea130ad10b40f59a7a2cc2981c381ec531176bf3 100644 (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é
index ff0c31806706f1663fed44ad10ee7e88b29e4603..3a6f122c7204a7bd75f5b1922ac0801549c8b2e2 100644 (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
index da1008fb4b9b13a7cbd3ab3d1e45d8207967b77b..c70c005d3b2d8cf01fcbaf6aa798cc8ed6fbc7b1 100644 (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
index 672dc71ff683060998624f01616d23e8447d387e..f1f60e134310e8cce1faa74e3702d1c5e9d2a516 100644 (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
index b93d84525f8267ef22a2fcba23e2252e44342206..345c9f4d7a7665af8bc42472071ff1ec2979fad0 100644 (file)
   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
index 4b7d54c4a7e8700ddc517c2349be92b2894ce6cc..dc790edd4d63f8c14b3c939a2c25ca0d2e5aad94 100644 (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
index c607caf050e4fc0371ab11cddfec87ae0a10e00f..d59b443ec12e82f67c24e91efa855085eda2c57e 100644 (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
index 80cf49bf94055137931d93f85f61bfe18a02191e..e3bd477ea184b032885f5bfd3cf178f3e9dac030 100644 (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
index 6e8cefa6e8fe7a6adeaa4c269dd7ef69fe113f63..8a042ad8b0e1189ff157b030a79d2a02ef000ce8 100644 (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
index 71c9ea02ff547a83c4d5c0ea3d4242ce9bd01ba3..ad7699697d8307e0238403249080d1f5cd6790bb 100644 (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
index 7839e8a60e64cdda0ce23194a25f5ce5c7f1f6d0..654e1300986fdf3a84ced93c72462e7e335b4c0c 100644 (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
index 96aea4fe29b7579fa22e6e880b097156c85327d2..a215cc702812a873543ee1f93eb5e55839118bcd 100644 (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
index cc4947e279b5814ff331dbeb876e5cbe2da75792..b243feb2e708faf8dffca27af798aa2db1a31de3 100644 (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
index 8e33be66b2c29b4516deedb60d5c0c2951141209..58caf941966b2dbca9123f89579ae830b59a0c72 100644 (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
index a76c925eb5a65c17adb704f42f5f0b0e5c09a109..1e189d5471770fd1c40fd2eef88f922d9bbf3286 100644 (file)
   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
index 5c70da426af51152e808a5c3cad21bb881d2f226..f38fed23f962a385162d4e3b7e4b859198fc327a 100644 (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
index 87f821839dee353edf8d9050659e17ced5744289..31bdda551117b3ec8410b0512092949a4d830f8f 100644 (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
index a6ec7e1e612d0313f341ff7fdffd6e169ed19525..4ae10b799a18e10c111e232368986a43b041011a 100644 (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
index 1bd5c0253bd303fb95edf978bf8903ce45b3027d..b040674e039ba2db42c24f592d2e9f261a406e31 100644 (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
index 5ff4076b432ce5e8ea2106b24ddc97ea54bb0649..7ea6c8bd29d470910e6dbdb074b8007abaf2efaf 100644 (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
index 9d9c282fa4d1317f557f781979afe35d066d2e04..f114ee44d3c12f9f25e0ecf01097832cc6817861 100644 (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
index 80b9ea7a1574edaf91f397a502d926997e0826dd..823b29091a7f3757b45e5caab4417b90c09899c2 100644 (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
index 723446bcbf757358280d82901e47a7d8fac33d54..12a38c3ebd94571a303da2234938394c88af7549 100644 (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
index ffef453aa6186c28608253d751d624924de5383e..7ecf860bb5cbbfbce024737f551c8a4318f3439d 100644 (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
index 90727b36ebdd79316c417a1fb9e6db5c76571222..31aedcbad3d9e3fff57f1ac846369dec6c54ee84 100644 (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
index 407e3e4d4675593a1526acc9d74cadeca7b0fefe..ba0fafb960fc2f88525d015f6925904a27872830 100644 (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
index 508e3f9a6d5d7d02bd8022b9a7231cbf5768b1da..86bb8dd10378ab1e69815b5aaeaedd4c277c3170 100644 (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
index edc4586dd68741de137345b6a0e1524e9de9897a..be10b95c6fb6a4232240df48ad0b442accc7a307 100644 (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
index 7718e429bd908c43a658ad93edf3cf3040b3fda1..fd1657c6239394d4b7fe42a24cc1c66a4e7bb3fc 100644 (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
index b99afe95042ed88928fa44132df962bff9cd96ba..aa3ad0575c1af1d39ba26febd894d253072ef647 100644 (file)
   description_date_range_interval: 選擇起始與結束日期以設定範圍區間
   description_date_from: 輸入起始日期
   description_date_to: 輸入結束日期
+  button_edit_section: Edit this section
index c79fe59803bb9a3bcf2e07a5d2b02058fa7c234d..19abd880e25307ed0ad1c2207a206076c633eb27 100644 (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
index 0635aad8be0f7ba5f6f70dd2152e3d5e27007ce4..2f25fe0e69dda6e9e0303b24a2808e769c9d635d 100644 (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
index 520e3f9adc79a3084562dfb3034dbe0cc72c3ad2..1beb9563c7e9e925b3434559ed82213f323b0249 100644 (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.
index 911646104593fc98993be0d62111307ba3f79653..4f708c9078875a04be70b36f3f40265e0a1cc023 100644 (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%; }
index acf5086af301795f95b8e3c381c12bc118c95889..b5fd0108003064d28376b766b105eade96a5114e 100644 (file)
@@ -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: 
+  
index 2d2e2ecebcf869b9681369b1429b86bbc46e2e9c..8a00e4af499d6d4d24d8dbf6342828703c13ca24 100644 (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: 
index 48db9e650b5144bcc3528acc4c4bee780471d047..1274e676b6b975e7167a76b7027078d15c92a38f 100644 (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',
index 4d9f90518fb543528481abab93734e25c22f11dd..ec9b883b0b8cb00de950929d9a499b33f4be48a2 100644 (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