]> source.dussan.org Git - redmine.git/commitdiff
Adds workflow copy functionality (#1727).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 12 Dec 2009 10:06:07 +0000 (10:06 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 12 Dec 2009 10:06:07 +0000 (10:06 +0000)
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@3154 e93f8b46-1217-0410-a6f0-8f06a7374b81

13 files changed:
app/controllers/workflows_controller.rb
app/models/role.rb
app/models/tracker.rb
app/models/workflow.rb
app/views/workflows/_action_menu.rhtml [new file with mode: 0644]
app/views/workflows/copy.rhtml [new file with mode: 0644]
app/views/workflows/edit.rhtml
app/views/workflows/index.rhtml
config/locales/en.yml
config/locales/fr.yml
public/images/lightning.png [new file with mode: 0644]
public/stylesheets/application.css
test/functional/workflows_controller_test.rb

index 380d4e75217a8f7b1360437eab1be199a51ebb57..f11aa473049686d81e8635c0aa2c29a955d2d5d9 100644 (file)
@@ -42,4 +42,27 @@ class WorkflowsController < ApplicationController
     @trackers = Tracker.find(:all, :order => 'position')
     @statuses = IssueStatus.find(:all, :order => 'position')
   end
+  
+  def copy
+    @trackers = Tracker.find(:all, :order => 'position')
+    @roles = Role.find(:all, :order => 'builtin, position')
+    
+    @source_tracker = params[:source_tracker_id].blank? ? nil : Tracker.find_by_id(params[:source_tracker_id])
+    @source_role = params[:source_role_id].blank? ? nil : Role.find_by_id(params[:source_role_id])
+    
+    @target_trackers = params[:target_tracker_ids].blank? ? nil : Tracker.find_all_by_id(params[:target_tracker_ids])
+    @target_roles = params[:target_role_ids].blank? ? nil : Role.find_all_by_id(params[:target_role_ids])
+     
+    if request.post?
+      if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
+        flash.now[:error] = l(:error_workflow_copy_source)
+      elsif @target_trackers.nil? || @target_roles.nil?
+        flash.now[:error] = l(:error_workflow_copy_target)
+      else
+        Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
+        flash[:notice] = l(:notice_successful_update)
+        redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role
+      end
+    end
+  end
 end
index db582570257b1db013c0fc4e624fd101d890beb2..22ce2a6502d5ed6f4898388ff633bfb6b9f2bd86 100644 (file)
@@ -28,14 +28,8 @@ class Role < ActiveRecord::Base
   
   before_destroy :check_deletable
   has_many :workflows, :dependent => :delete_all do
-    def copy(role)
-      raise "Can not copy workflow from a #{role.class}" unless role.is_a?(Role)
-      raise "Can not copy workflow from/to an unsaved role" if proxy_owner.new_record? || role.new_record?
-      clear
-      connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" +
-                        " SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" +
-                        " FROM #{Workflow.table_name}" +
-                        " WHERE role_id = #{role.id}"
+    def copy(source_role)
+      Workflow.copy(nil, source_role, nil, proxy_owner)
     end
   end
   
index 7c5bae25089453517585e98e626fc8382e5397dc..8f7a98c39a2e0c874631499dc14fd974f8d7c9d4 100644 (file)
@@ -19,14 +19,8 @@ class Tracker < ActiveRecord::Base
   before_destroy :check_integrity  
   has_many :issues
   has_many :workflows, :dependent => :delete_all do
-    def copy(tracker)
-      raise "Can not copy workflow from a #{tracker.class}" unless tracker.is_a?(Tracker)
-      raise "Can not copy workflow from/to an unsaved tracker" if proxy_owner.new_record? || tracker.new_record?
-      clear
-      connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" +
-                        " SELECT #{proxy_owner.id}, old_status_id, new_status_id, role_id" +
-                        " FROM #{Workflow.table_name}" +
-                        " WHERE tracker_id = #{tracker.id}"
+    def copy(source_tracker)
+      Workflow.copy(source_tracker, nil, proxy_owner, nil)
     end
   end
   
index a96abaf189324bea08b162ffd6751010585e4f06..da358b5184d8b3bd7eb9fa848d5ade6c09e84e6a 100644 (file)
@@ -51,4 +51,50 @@ class Workflow < ActiveRecord::Base
       uniq.
       sort
   end
+  
+  # Copies workflows from source to targets
+  def self.copy(source_tracker, source_role, target_trackers, target_roles)
+    unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role)
+      raise ArgumentError.new "source_tracker or source_role must be specified"
+    end
+    
+    target_trackers = [target_trackers].flatten.compact
+    target_roles = [target_roles].flatten.compact
+    
+    target_trackers = Tracker.all if target_trackers.empty?
+    target_roles = Role.all if target_roles.empty?
+    
+    target_trackers.each do |target_tracker|
+      target_roles.each do |target_role|
+        copy_one(source_tracker || target_tracker,
+                   source_role || target_role,
+                   target_tracker,
+                   target_role)
+      end
+    end
+  end
+  
+  # Copies a single set of workflows from source to target
+  def self.copy_one(source_tracker, source_role, target_tracker, target_role)
+    unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? &&
+      source_role.is_a?(Role) && !source_role.new_record? &&
+      target_tracker.is_a?(Tracker) && !target_tracker.new_record? &&
+      target_role.is_a?(Role) && !target_role.new_record?
+      
+      raise ArgumentError.new("arguments can not be nil or unsaved objects")
+    end
+    
+    if source_tracker == target_tracker && source_role == target_role
+      false
+    else
+      transaction do
+        delete_all :tracker_id => target_tracker.id, :role_id => target_role.id
+        connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, role_id, old_status_id, new_status_id)" +
+                          " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id" +
+                          " FROM #{Workflow.table_name}" +
+                          " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}"
+      end
+      true
+    end
+  end
 end
diff --git a/app/views/workflows/_action_menu.rhtml b/app/views/workflows/_action_menu.rhtml
new file mode 100644 (file)
index 0000000..5772811
--- /dev/null
@@ -0,0 +1,5 @@
+<div class="contextual">
+<%= link_to l(:button_edit), {:action => 'edit'}, :class => 'icon icon-edit' %>
+<%= link_to l(:button_copy), {:action => 'copy'}, :class => 'icon icon-copy' %>
+<%= link_to l(:field_summary), {:action => 'index'}, :class => 'icon icon-summary' %>
+</div>
diff --git a/app/views/workflows/copy.rhtml b/app/views/workflows/copy.rhtml
new file mode 100644 (file)
index 0000000..2cecc84
--- /dev/null
@@ -0,0 +1,33 @@
+<%= render :partial => 'action_menu' %>
+
+<h2><%=l(:label_workflow)%></h2>
+
+<% form_tag({}, :id => 'workflow_copy_form') do %>
+<div class="tabular box">
+<p>
+       <label><%= l(:label_copy_source) %></label>
+       <%= l(:label_tracker) %><br />
+       <%= select_tag('source_tracker_id', 
+                                                                       "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
+                                                                       "<option value=\"any\">--- #{ l(:label_copy_same_as_target) } ---</option>" + 
+                                                                       options_from_collection_for_select(@trackers, 'id', 'name', @source_tracker && @source_tracker.id)) %><br />
+       <%= l(:label_role) %><br />
+       <%= select_tag('source_role_id',
+                                                                       "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
+                                                                       "<option value=\"any\">--- #{ l(:label_copy_same_as_target) } ---</option>" + 
+                                                                       options_from_collection_for_select(@roles, 'id', 'name', @source_role && @source_role.id)) %>
+</p>
+<p>
+       <label><%= l(:label_copy_target) %></label>
+       <%= l(:label_tracker) %><br />
+       <%= select_tag 'target_tracker_ids',
+                                                                       "<option value=\"\" disabled=\"disabled\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
+                                                                       options_from_collection_for_select(@trackers, 'id', 'name', @target_trackers && @target_trackers.map(&:id)), :multiple => true %><br />
+       <%= l(:label_role) %><br />
+       <%= select_tag 'target_role_ids',
+                                                                       "<option value=\"\" disabled=\"disabled\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
+                                                                       options_from_collection_for_select(@roles, 'id', 'name', @target_roles && @target_roles.map(&:id)), :multiple => true %>
+</p>
+</div>
+<%= submit_tag l(:button_copy) %>
+<% end %>
index 399825de21211e6876b7e67df33e22b8730c2fc1..4feaf3a7c3a3ec1a9e45fd40ab2392a23dedf7ef 100644 (file)
@@ -1,6 +1,4 @@
-<div class="contextual">
-<%= link_to l(:field_summary), :action => 'index' %>
-</div>
+<%= render :partial => 'action_menu' %>
 
 <h2><%=l(:label_workflow)%></h2>
 
index 2fd080d8f960e78b7c7ffe99e67ab110ebaec85a..9f06269b6e8d39e9c8e8aa56989f79681c37286c 100644 (file)
@@ -1,3 +1,5 @@
+<%= render :partial => 'action_menu' %>
+
 <h2><%=l(:label_workflow)%></h2>
 
 <% if @workflow_counts.empty? %>
index 6a439a33734dc5f533df901771acc6202ae6a29a..3f7e02191f016b6069304e6a6992705968885444 100644 (file)
@@ -159,7 +159,9 @@ en:
   error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened'
   error_can_not_archive_project: This project can not be archived
   error_issue_done_ratios_not_updated: "Issue done ratios not updated."
-
+  error_workflow_copy_source: 'Please select a source tracker or role'
+  error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
+  
   warning_attachments_not_saved: "{{count}} file(s) could not be saved."
   
   mail_subject_lost_password: "Your {{value}} password"
@@ -720,6 +722,9 @@ en:
   label_version_sharing_tree: With project tree
   label_version_sharing_system: With all projects
   label_update_issue_done_ratios: Update issue done ratios
+  label_copy_source: Source
+  label_copy_target: Target
+  label_copy_same_as_target: Same as target
   
   button_login: Login
   button_submit: Submit
index b39907c504a3e0fe504c1513a0e6502f82f1a475..7072a0a10caea633c9ada79c67df39a88119e228 100644 (file)
@@ -179,6 +179,8 @@ fr:
   error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
   error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
   error_can_not_archive_project: "Ce projet ne peut pas être archivé"
+  error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source'
+  error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles'
   
   warning_attachments_not_saved: "{{count}} fichier(s) n'ont pas pu être sauvegardés."
   
@@ -732,6 +734,9 @@ fr:
   label_version_sharing_hierarchy: Avec toute la hiérarchie
   label_version_sharing_tree: Avec tout l'arbre
   label_version_sharing_system: Avec tous les projets
+  label_copy_source: Source
+  label_copy_target: Cible
+  label_copy_same_as_target: Comme la cible
   
   button_login: Connexion
   button_submit: Soumettre
diff --git a/public/images/lightning.png b/public/images/lightning.png
new file mode 100644 (file)
index 0000000..9680afd
Binary files /dev/null and b/public/images/lightning.png differ
index 3d0c3b028ab7b4485a6d14e55f27e8aa03cf4b46..c3e7ca81fd106f9c5784aa2303734f9c68ff9289 100644 (file)
@@ -297,6 +297,8 @@ ul.properties li span {font-style:italic;}
 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
 
+#workflow_copy_form select { width: 200px; }
+
 .pagination {font-size: 90%}
 p.pagination {margin-top:8px;}
 
@@ -728,6 +730,7 @@ vertical-align: middle;
 .icon-details  { background-image: url(../images/zoom_in.png); }
 .icon-report  { background-image: url(../images/report.png); }
 .icon-comment  { background-image: url(../images/comment.png); }
+.icon-summary  { background-image: url(../images/lightning.png); }
 
 .icon-file { background-image: url(../images/files/default.png); }
 .icon-file.text-plain { background-image: url(../images/files/text.png); }
index 19187b7fed5ece45fb1949b08cc041bb0cdf6ae1..2b5216f55e1bae6ace9d2abf01e1df06d99acef0 100644 (file)
@@ -22,7 +22,7 @@ require 'workflows_controller'
 class WorkflowsController; def rescue_action(e) raise e end; end
 
 class WorkflowsControllerTest < ActionController::TestCase
-  fixtures :roles, :trackers, :workflows
+  fixtures :roles, :trackers, :workflows, :users
   
   def setup
     @controller = WorkflowsController.new
@@ -81,4 +81,50 @@ class WorkflowsControllerTest < ActionController::TestCase
     post :edit, :role_id => 2, :tracker_id => 1
     assert_equal 0, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
   end
+  
+  def test_get_copy
+    get :copy
+    assert_response :success
+    assert_template 'copy'
+  end
+  
+  def test_post_copy_one_to_one
+    source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
+    
+    post :copy, :source_tracker_id => '1', :source_role_id => '2',
+                :target_tracker_ids => ['3'], :target_role_ids => ['1']
+    assert_response 302
+    assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
+  end
+  
+  def test_post_copy_one_to_many
+    source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
+    
+    post :copy, :source_tracker_id => '1', :source_role_id => '2',
+                :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
+    assert_response 302
+    assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1)
+    assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
+    assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3)
+    assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3)
+  end
+  
+  def test_post_copy_many_to_many
+    source_t2 = status_transitions(:tracker_id => 2, :role_id => 2)
+    source_t3 = status_transitions(:tracker_id => 3, :role_id => 2)
+    
+    post :copy, :source_tracker_id => 'any', :source_role_id => '2',
+                :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
+    assert_response 302
+    assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1)
+    assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1)
+    assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3)
+    assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3)
+  end
+  
+  # Returns an array of status transitions that can be compared
+  def status_transitions(conditions)
+    Workflow.find(:all, :conditions => conditions,
+                        :order => 'tracker_id, role_id, old_status_id, new_status_id').collect {|w| [w.old_status, w.new_status_id]}
+  end
 end