]> source.dussan.org Git - redmine.git/commitdiff
Added 'Bulk edit' functionality.
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Tue, 9 Oct 2007 19:07:19 +0000 (19:07 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Tue, 9 Oct 2007 19:07:19 +0000 (19:07 +0000)
This can be done by clicking on the edit link (little pen icon) at the upper-left corner of the issue list.
Most properties can be set (priority, assignee, category, fixed version, start and due dates, done ratio) and a note can be entered.
Only issues of the current project can be selected for bulk edit (subproject issues can't).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@817 e93f8b46-1217-0410-a6f0-8f06a7374b81

24 files changed:
app/controllers/projects_controller.rb
app/models/issue.rb
app/models/project.rb
app/views/issues/_bulk_edit_form.rhtml [new file with mode: 0644]
app/views/issues/_list.rhtml
app/views/projects/list_issues.rhtml
lang/bg.yml
lang/cs.yml
lang/de.yml
lang/en.yml
lang/es.yml
lang/fr.yml
lang/it.yml
lang/ja.yml
lang/nl.yml
lang/pl.yml
lang/pt-br.yml
lang/pt.yml
lang/ro.yml
lang/sv.yml
lang/zh.yml
lib/redmine.rb
public/javascripts/application.js
test/functional/projects_controller_test.rb

index cf132551841c1ef54de9ef4fe05d4c3a11718e5d..13253939a899702bed7fec2b84ba0715429a39c2 100644 (file)
@@ -336,6 +336,36 @@ class ProjectsController < ApplicationController
     @options_for_rfpdf[:file_name] = "export.pdf"
     render :layout => false
   end
+  
+  # Bulk edit issues
+  def bulk_edit_issues
+    if request.post?
+      priority = Enumeration.find_by_id(params[:priority_id])
+      assigned_to = User.find_by_id(params[:assigned_to_id])
+      issues = @project.issues.find_all_by_id(params[:issue_ids])
+      unsaved_issue_ids = []
+      issues.each do |issue|
+        issue.init_journal(User.current, params[:notes])
+        issue.priority = priority if priority
+        issue.assigned_to = assigned_to if assigned_to
+        issue.start_date = params[:start_date] unless params[:start_date].blank?
+        issue.due_date = params[:due_date] unless params[:due_date].blank?
+        issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
+        unsaved_issue_ids << issue.id unless issue.save
+      end
+      if unsaved_issue_ids.empty?
+        flash[:notice] = l(:notice_successful_update) unless issues.empty?
+      else
+        flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, issues.size, '#' + unsaved_issue_ids.join(', #'))
+      end
+      redirect_to :action => 'list_issues', :id => @project
+      return
+    end
+    render :update do |page|
+      page.hide 'query_form'
+      page.replace_html  'bulk-edit', :partial => 'issues/bulk_edit_form'
+    end
+  end
 
   def move_issues
     @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
index 52e21b416eb439b24353b6a01555928b64e2c385..fc7f6e1b8b26a6b341a0b419956ddc93bc0c5544 100644 (file)
@@ -143,7 +143,7 @@ class Issue < ActiveRecord::Base
   
   # Users the issue can be assigned to
   def assignable_users
-    project.members.select {|m| m.role.assignable?}.collect {|m| m.user}
+    project.assignable_users
   end
   
   def spent_hours
index b17f7ba7454cb10cfdbcf3d6b3cf23481969b451..3e6593f588f9f9ae51c45899fbbbc7b6883d8467 100644 (file)
@@ -113,6 +113,11 @@ class Project < ActiveRecord::Base
     children.select {|child| child.active?}
   end
   
+  # Users issues can be assigned to
+  def assignable_users
+    members.select {|m| m.role.assignable?}.collect {|m| m.user}
+  end
+  
   # Returns an array of all custom fields enabled for project issues
   # (explictly associated custom fields and custom fields enabled for all projects)
   def custom_fields_for_issues(tracker)
diff --git a/app/views/issues/_bulk_edit_form.rhtml b/app/views/issues/_bulk_edit_form.rhtml
new file mode 100644 (file)
index 0000000..fc0972f
--- /dev/null
@@ -0,0 +1,30 @@
+<div id="bulk-edit-fields">
+<fieldset class="box"><legend><%= l(:label_bulk_edit_selected_issues) %></legend>
+
+<p>
+<label><%= l(:field_priority) %>: 
+<%= select_tag('priority_id', "<option>#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(Enumeration.get_values('IPRI'), :id, :name)) %></label>
+<label><%= l(:field_assigned_to) %>: 
+<%= select_tag('assigned_to_id', "<option>#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.assignable_users, :id, :name)) %></label>
+<label><%= l(:field_category) %>: 
+<%= select_tag('category_id', "<option>#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.issue_categories, :id, :name)) %></label>
+<label><%= l(:field_fixed_version) %>: 
+<%= select_tag('fixed_version_id', "<option>#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.versions, :id, :name)) %></label>
+</p>
+
+<p>
+<label><%= l(:field_start_date) %>: 
+<%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label>
+<label><%= l(:field_due_date) %>: 
+<%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label>
+<label><%= l(:field_done_ratio) %>: 
+<%= select_tag 'done_ratio', options_for_select([l(:label_no_change_option)] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label>
+</p>
+
+<label for="notes"><%= l(:field_notes) %></label><br />
+<%= text_area_tag 'notes', '', :cols => 80, :rows => 5 %>
+
+</fieldset>
+<p><%= submit_tag l(:button_apply) %>
+<%= link_to l(:button_cancel), {}, :onclick => 'Element.hide("bulk-edit-fields"); if ($("query_form")) {Element.show("query_form")}; return false;' %></p>
+</div>
index 567ccba09f4fe783a5dd56d1bedaa46bc22a3bfc..b81559c29532ab9e7f62eb73fd312852d3b12111 100644 (file)
@@ -1,6 +1,11 @@
+<div id="bulk-edit"></div>
 <table class="list">
     <thead><tr>
-        <th></th>
+        <th><%= link_to_remote(image_tag('edit.png'),
+                               {:url => { :controller => 'projects', :action => 'bulk_edit_issues', :id => @project },
+                                :method => :get},
+                               {:title => l(:label_bulk_edit_selected_issues)}) if @project && User.current.allowed_to?(:edit_issues, @project) %>
+        </th>
                <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#') %>
         <% query.columns.each do |column| %>
           <%= column_header(column) %>
@@ -9,7 +14,7 @@
        <tbody>
        <% issues.each do |issue| %>
        <tr class="issue <%= cycle('odd', 'even') %>">
-           <td class="checkbox"><%= check_box_tag "issue_ids[]", issue.id, false, :id => "issue_#{issue.id}" %></td>
+           <td class="checkbox"><%= check_box_tag("issue_ids[]", issue.id, false, :id => "issue_#{issue.id}", :disabled => (!@project || @project != issue.project)) %></td>
                <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
         <% query.columns.each do |column| %>
           <%= content_tag 'td', column_content(column, issue), :class => column.name %>
index 60e8f7be275033427f0bbfb87f3be28a8faa303a..9f2ff870da6d32be98ee03a624cb7a67a5e0bdbc 100644 (file)
@@ -4,7 +4,6 @@
     
     <% form_tag({ :controller => 'queries', :action => 'new', :project_id => @project }, :id => 'query_form') do %>
     <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
-    <% end %>
     <div class="contextual">
     <%= link_to_remote l(:button_apply), 
                        { :url => { :controller => 'projects', :action => 'list_issues', :id => @project, :set_filter => 1 },
@@ -22,6 +21,8 @@
     <% end %>
     </div>
     <br />
+    &nbsp;
+    <% end %>
 <% else %>
     <div class="contextual">
     <% if @query.editable_by?(User.current) %>
@@ -31,6 +32,7 @@
     </div>
     
     <h2><%= @query.name %></h2>
+    <div id="query_form"></div>
     <% set_html_title @query.name %>
 <% end %>
 <%= error_messages_for 'query' %>
 <% if @issues.empty? %>
 <p class="nodata"><%= l(:label_no_data) %></p>
 <% else %>
-&nbsp;
-<% form_tag({:controller => 'projects', :action => 'move_issues', :id => @project}, :id => 'issues_form' ) do %>       
+<% form_tag({:controller => 'projects', :action => 'bulk_edit_issues', :id => @project}, :id => 'issues_form', :onsubmit => "if (!checkBulkEdit(this)) {alert('#{l(:notice_no_issue_selected)}'); return false;}" ) do %>      
 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
 <div class="contextual">
 <%= l(:label_export_to) %>
 <%= link_to 'CSV', {:action => 'export_issues_csv', :id => @project}, :class => 'icon icon-csv' %>,
 <%= link_to 'PDF', {:action => 'export_issues_pdf', :id => @project}, :class => 'icon icon-pdf' %>
 </div>
-<p><%= submit_tag(l(:button_move), :class => "button-small") if authorize_for('projects', 'move_issues') %>
+<p>
 <%= pagination_links_full @issue_pages %>
 [ <%= @issue_pages.current.first_item %> - <%= @issue_pages.current.last_item %> / <%= @issue_count %> ]
 </p>
 <% content_for :sidebar do %>
     <%= render :partial => 'issues/sidebar' %>
 <% end %>
+
+<% content_for :header_tags do %>
+    <%= javascript_include_tag 'calendar/calendar' %>
+    <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
+    <%= javascript_include_tag 'calendar/calendar-setup' %>
+    <%= stylesheet_link_tag 'calendar' %>
+<% end %>
index 12a5378d65c2cff9f342f5f3b3a51c773423ef70..e987e1adb76324ae62fb1f8c75dbd64fc3a50a3e 100644 (file)
@@ -516,3 +516,7 @@ field_column_names: Columns
 label_default_columns: Default columns
 setting_issue_list_default_columns: Default columns displayed on the issue list
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index 078f824f69881564ee7e22ff35f127d91144e86b..7b5c1a650a7422ba68ffdc5995e789d8862536f2 100644 (file)
@@ -516,3 +516,7 @@ field_column_names: Columns
 label_default_columns: Default columns
 setting_issue_list_default_columns: Default columns displayed on the issue list
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index 4707855798cd0fd5539b247e0a26e4abf8b62b21..23dd6518187707b846d9150c2731c7d81d0696d3 100644 (file)
@@ -516,3 +516,7 @@ field_column_names: Columns
 label_default_columns: Default columns
 setting_issue_list_default_columns: Default columns displayed on the issue list
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index 79e8f61d83587de0985767e74b526140be882c31..af3faf49bda6b9d6530b53f1e26d2bf4b5298783 100644 (file)
@@ -73,6 +73,8 @@ notice_not_authorized: You are not authorized to access this page.
 notice_email_sent: An email was sent to %s
 notice_email_error: An error occurred while sending mail (%s)
 notice_feeds_access_key_reseted: Your RSS access key was reseted.
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
 
 mail_subject_lost_password: Your redMine password
 mail_subject_register: redMine account activation
@@ -427,6 +429,8 @@ label_jump_to_a_project: Jump to a project...
 label_file_plural: Files
 label_changeset_plural: Changesets
 label_default_columns: Default columns
+label_no_change_option: (No change)
+label_bulk_edit_selected_issues: Bulk edit selected issues
 
 button_login: Login
 button_submit: Submit
index 2e8e6efbc73da2fc0f00b27f1bf3a260be1deb8c..40929e466933af27102defe58cb3eaa15246e423 100644 (file)
@@ -519,3 +519,7 @@ label_added_time_by: Added by %s %s ago
 field_estimated_hours: Estimated time
 label_changeset_plural: Changesets
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index bb875288e4ffe0f8b76cfc9b931a9dbe768302a2..a42ead91322c68247a2a1a95b694756483cf5c32 100644 (file)
@@ -73,6 +73,8 @@ notice_not_authorized: "Vous n'êtes pas autorisés à accéder à cette page."
 notice_email_sent: "Un email a été envoyé à %s"
 notice_email_error: "Erreur lors de l'envoi de l'email (%s)"
 notice_feeds_access_key_reseted: Votre clé d'accès aux flux RSS a été réinitialisée.
+notice_failed_to_save_issues: "%d demande(s) sur les %d sélectionnées n'ont pas pu être mise(s) à jour: %s."
+notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
 
 mail_subject_lost_password: Votre mot de passe redMine
 mail_subject_register: Activation de votre compte redMine
@@ -427,6 +429,8 @@ label_jump_to_a_project: Aller à un projet...
 label_file_plural: Fichiers
 label_changeset_plural: Révisions
 label_default_columns: Colonnes par défaut
+label_no_change_option: (Pas de changement)
+label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
 
 button_login: Connexion
 button_submit: Soumettre
index 4d114b15e75aafc32c89cee411e2641ad92bdb84..207a5e649d3cd8052b52b171f90d261cceec1ce5 100644 (file)
@@ -516,3 +516,7 @@ field_column_names: Columns
 label_default_columns: Default columns
 setting_issue_list_default_columns: Default columns displayed on the issue list
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index 433320aa1888ba71a5759e8a96ab197790ecbeba..41c1ba5d49c3378579ff56338a4c3f5a93ec69cc 100644 (file)
@@ -517,3 +517,7 @@ field_column_names: Columns
 label_default_columns: Default columns
 setting_issue_list_default_columns: 問題の一覧で表示する項目
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index 7e252d9d1b6a91e54d5b3660f7df64ebd7663f0d..716618d79d575dbfb253e6dd3c4e616bbf4544c8 100644 (file)
@@ -517,3 +517,7 @@ field_column_names: Columns
 label_default_columns: Default columns
 setting_issue_list_default_columns: Default columns displayed on the issue list
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index 4db0d6761ecb8b6929137e01f54e1ab3a1b67221..f883e2d2f9ba359104cc3184a464b3e8bea81a02 100644 (file)
@@ -516,3 +516,7 @@ field_column_names: Columns
 label_default_columns: Default columns
 setting_issue_list_default_columns: Default columns displayed on the issue list
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index 59a5e1ccbbd001c407469efc472f2dc2b2a480df..91d487e5551112bd5ed90710d58973ae0beb5192 100644 (file)
@@ -516,3 +516,7 @@ field_column_names: Columns
 label_default_columns: Default columns\r
 setting_issue_list_default_columns: Default columns displayed on the issue list\r
 setting_repositories_encodings: Repositories encodings\r
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."\r
+label_bulk_edit_selected_issues: Bulk edit selected issues\r
+label_no_change_option: (No change)\r
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."\r
index b4e4a0cbd9b9174c6158ef4e804999e788d72e06..3c6cec37553f2c95989fc6192a1375f4c1e1f8fc 100644 (file)
@@ -516,3 +516,7 @@ field_column_names: Columns
 label_default_columns: Default columns
 setting_issue_list_default_columns: Default columns displayed on the issue list
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index 25c4887095fa5c8fcae65157730dc9150d6b310f..042b596fa3ebbf28985663faf5b8f1cd26a3b3f4 100644 (file)
@@ -516,3 +516,7 @@ field_column_names: Columns
 label_default_columns: Default columns
 setting_issue_list_default_columns: Default columns displayed on the issue list
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index e59e9233e829274ad740b27b66bb6a45a8b76efd..02ecc6e134c01b50a4095cefc579878b5b01c22a 100644 (file)
@@ -517,3 +517,7 @@ field_column_names: Columns
 label_default_columns: Default columns
 setting_issue_list_default_columns: Default columns displayed on the issue list
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index b881ee4df143265baa9ba6d76224bbc03bcb0e52..bbd08877269a8b0816b6ba6b252c4a67eb6c13fc 100644 (file)
@@ -519,3 +519,7 @@ field_column_names: Columns
 label_default_columns: Default columns
 setting_issue_list_default_columns: Default columns displayed on the issue list
 setting_repositories_encodings: Repositories encodings
+notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+label_bulk_edit_selected_issues: Bulk edit selected issues
+label_no_change_option: (No change)
+notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
index f5682a9593941e2f5987945ac1985c658d107fab..cb6af8aed1be868d7152a0c8dfd185387dbd68ef 100644 (file)
@@ -29,7 +29,8 @@ Redmine::AccessControl.map do |map|
                                   :queries => :index,
                                   :reports => :issue_report}, :public => true                    
     map.permission :add_issues, {:projects => :add_issue}, :require => :loggedin
-    map.permission :edit_issues, {:issues => [:edit, :destroy_attachment]}, :require => :loggedin
+    map.permission :edit_issues, {:projects => :bulk_edit_issues,
+                                  :issues => [:edit, :destroy_attachment]}, :require => :loggedin
     map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}, :require => :loggedin
     map.permission :add_issue_notes, {:issues => :add_note}, :require => :loggedin
     map.permission :change_issue_status, {:issues => :change_status}, :require => :loggedin
index 6a30e42e7a3093b8ef44ecb32dfdfbce2dbe2de4..bf86f398af9fe2b46d92d027aeb634bdd89aca29 100644 (file)
@@ -49,6 +49,16 @@ function promptToRemote(text, param, url) {
     }
 }
 
+/* checks that at least one checkbox is checked (used when submitting bulk edit form) */
+function checkBulkEdit(form) {
+       for (var i = 0; i < form.elements.length; i++) {
+        if (form.elements[i].checked) {
+            return true;
+        }
+    }
+    return false;
+}
+
 /* shows and hides ajax indicator */
 Ajax.Responders.register({
     onCreate: function(){
index b065d82b7963e0c31002c95ce2595ab2b957ae4e..e9ffa30231bbcb6450c42c34898b119305257098 100644 (file)
@@ -83,6 +83,16 @@ class ProjectsControllerTest < Test::Unit::TestCase
     assert_response :success\r
     assert_not_nil assigns(:issues)\r
   end
+  
+  def test_bulk_edit_issues
+    @request.session[:user_id] = 2
+    # update issues priority
+    post :bulk_edit_issues, :id => 1, :issue_ids => [1, 2], :priority_id => 7, :notes => "Bulk editing"
+    assert_response 302
+    # check that the issues were updated
+    assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
+    assert_equal "Bulk editing", Issue.find(1).journals.last.notes
+  end
 \r
   def test_list_news\r
     get :list_news, :id => 1\r