diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index dbb4940..85151e9 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -19,13 +19,14 @@ class IssuesController < ApplicationController layout 'base' menu_item :new_issue, :only => :new - before_filter :find_issue, :except => [:index, :changes, :preview, :new, :update_form] + before_filter :find_issue, :only => [:show, :edit, :destroy_attachment] + before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] before_filter :find_project, :only => [:new, :update_form] - before_filter :authorize, :except => [:index, :changes, :preview, :update_form] + before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu] before_filter :find_optional_project, :only => [:index, :changes] accept_key_auth :index, :changes - cache_sweeper :issue_sweeper, :only => [ :new, :edit, :destroy ] + cache_sweeper :issue_sweeper, :only => [ :new, :edit, :bulk_edit, :destroy ] helper :journals helper :projects @@ -152,18 +153,20 @@ class IssuesController < ApplicationController @priorities = Enumeration::get_values('IPRI') @custom_values = [] @edit_allowed = User.current.allowed_to?(:edit_issues, @project) + + @notes = params[:notes] + journal = @issue.init_journal(User.current, @notes) + # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed + if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue] + attrs = params[:issue].dup + attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed + attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s} + @issue.attributes = attrs + end + if request.get? @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) } else - @notes = params[:notes] - journal = @issue.init_journal(User.current, @notes) - # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed - if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue] - attrs = params[:issue].dup - attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed - attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s} - @issue.attributes = attrs - end # Update custom fields if user has :edit permission if @edit_allowed && params[:custom_fields] @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) } @@ -191,8 +194,78 @@ class IssuesController < ApplicationController flash.now[:error] = l(:notice_locking_conflict) end + # Bulk edit a set of issues + def bulk_edit + if request.post? + status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id]) + priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id]) + assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id]) + category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id]) + fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id]) + + unsaved_issue_ids = [] + @issues.each do |issue| + journal = issue.init_journal(User.current, params[:notes]) + issue.priority = priority if priority + issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none' + issue.category = category if category + issue.fixed_version = fixed_version if fixed_version + 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? + # Don't save any change to the issue if the user is not authorized to apply the requested status + if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save + # Send notification for each issue (if changed) + Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated') + else + # Keep unsaved issue ids to display them in flash error + unsaved_issue_ids << issue.id + end + 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 :controller => 'issues', :action => 'index', :project_id => @project + return + end + # Find potential statuses the user could be allowed to switch issues to + @available_statuses = Workflow.find(:all, :include => :new_status, + :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq + end + + def move + @allowed_projects = [] + # find projects to which the user is allowed to move the issue + if User.current.admin? + # admin is allowed to move issues to any active (visible) project + @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name') + else + User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)} + end + @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id] + @target_project ||= @project + @trackers = @target_project.trackers + if request.post? + new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) + unsaved_issue_ids = [] + @issues.each do |issue| + unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) + 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 :controller => 'issues', :action => 'index', :project_id => @project + return + end + render :layout => false if request.xhr? + end + def destroy - @issue.destroy + @issues.each(&:destroy) redirect_to :action => 'index', :project_id => @project end @@ -208,17 +281,27 @@ class IssuesController < ApplicationController end def context_menu + @issues = Issue.find_all_by_id(params[:ids], :include => :project) + if (@issues.size == 1) + @issue = @issues.first + @allowed_statuses = @issue.new_statuses_allowed_to(User.current) + @assignables = @issue.assignable_users + @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to) + end + projects = @issues.collect(&:project).compact.uniq + @project = projects.first if projects.size == 1 + + @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)), + :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))), + :move => (@project && User.current.allowed_to?(:move_issues, @project)), + :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), + :delete => (@project && User.current.allowed_to?(:delete_issues, @project)) + } + @priorities = Enumeration.get_values('IPRI').reverse @statuses = IssueStatus.find(:all, :order => 'position') - @allowed_statuses = @issue.new_statuses_allowed_to(User.current) - @assignables = @issue.assignable_users - @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to) - @can = {:edit => User.current.allowed_to?(:edit_issues, @project), - :assign => (@allowed_statuses.any? || User.current.allowed_to?(:edit_issues, @project)), - :add => User.current.allowed_to?(:add_issues, @project), - :move => User.current.allowed_to?(:move_issues, @project), - :copy => (@project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), - :delete => User.current.allowed_to?(:delete_issues, @project)} + @back = request.env['HTTP_REFERER'] + render :layout => false end @@ -242,6 +325,21 @@ private render_404 end + # Filter for bulk operations + def find_issues + @issues = Issue.find_all_by_id(params[:id] || params[:ids]) + raise ActiveRecord::RecordNotFound if @issues.empty? + projects = @issues.collect(&:project).compact.uniq + if projects.size == 1 + @project = projects.first + else + # TODO: let users bulk edit/move/destroy issues from different projects + render_error 'Can not bulk edit/move/destroy issues from different projects' and return false + end + rescue ActiveRecord::RecordNotFound + render_404 + end + def find_project @project = Project.find(params[:project_id]) rescue ActiveRecord::RecordNotFound diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 9560a45..cddfb6f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -22,7 +22,7 @@ class ProjectsController < ApplicationController menu_item :roadmap, :only => :roadmap menu_item :files, :only => [:list_files, :add_file] menu_item :settings, :only => :settings - menu_item :issues, :only => [:bulk_edit_issues, :changelog, :move_issues] + menu_item :issues, :only => [:changelog] before_filter :find_project, :except => [ :index, :list, :add ] before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ] @@ -182,83 +182,6 @@ class ProjectsController < ApplicationController end end - # Bulk edit issues - def bulk_edit_issues - if request.post? - status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id]) - priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id]) - assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id]) - category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id]) - fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id]) - issues = @project.issues.find_all_by_id(params[:issue_ids]) - unsaved_issue_ids = [] - issues.each do |issue| - journal = issue.init_journal(User.current, params[:notes]) - issue.priority = priority if priority - issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none' - issue.category = category if category - issue.fixed_version = fixed_version if fixed_version - 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? - # Don't save any change to the issue if the user is not authorized to apply the requested status - if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save - # Send notification for each issue (if changed) - Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated') - else - # Keep unsaved issue ids to display them in flash error - unsaved_issue_ids << issue.id - end - 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 :controller => 'issues', :action => 'index', :project_id => @project - return - end - # Find potential statuses the user could be allowed to switch issues to - @available_statuses = Workflow.find(:all, :include => :new_status, - :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq - 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] - redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues - - @projects = [] - # find projects to which the user is allowed to move the issue - if User.current.admin? - # admin is allowed to move issues to any active (visible) project - @projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name') - else - User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)} - end - @target_project = @projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id] - @target_project ||= @project - @trackers = @target_project.trackers - if request.post? - new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) - unsaved_issue_ids = [] - @issues.each do |issue| - unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) - 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 :controller => 'issues', :action => 'index', :project_id => @project - return - end - render :layout => false if request.xhr? - end - def add_file if request.post? @version = @project.versions.find_by_id(params[:version_id]) diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml index ff91f34..1f54702 100644 --- a/app/views/issues/_list.rhtml +++ b/app/views/issues/_list.rhtml @@ -1,10 +1,7 @@ -
- +<% form_tag({}) do -%> +
- <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %> <% query.columns.each do |column| %> @@ -12,14 +9,21 @@ <% end %> - <% issues.each do |issue| %> + <% issues.each do |issue| -%> "> - + - <% query.columns.each do |column| %> - <%= content_tag 'td', column_content(column, issue), :class => column.name %> - <% end %> + <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> - <% end %> + <% end -%>
<%= 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) %> + <%= link_to image_tag('edit.png'), {}, :onclick => 'toggleIssuesSelection(this.up("form")); return false;' %>
<%= check_box_tag("issue_ids[]", issue.id, false, :id => "issue_#{issue.id}", :disabled => (!@project || @project != issue.project)) %><%= check_box_tag("ids[]", issue.id, false) %> <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
+<% end -%> + +<% content_for :header_tags do -%> + <%= javascript_include_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu' %> +<% end -%> + + +<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> diff --git a/app/views/issues/_list_simple.rhtml b/app/views/issues/_list_simple.rhtml index eb93f8e..8900b73 100644 --- a/app/views/issues/_list_simple.rhtml +++ b/app/views/issues/_list_simple.rhtml @@ -1,5 +1,6 @@ -<% if issues.length > 0 %> - +<% if issues && issues.any? %> +<% form_tag({}) do %> +
@@ -9,6 +10,7 @@ <% for issue in issues %> ">
# <%=l(:field_tracker)%>
+ <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %> <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> <%=h issue.project.name %> - <%= issue.tracker.name %>
@@ -20,6 +22,7 @@ <% end %>
+<% end %> <% else %> - <%=l(:label_no_data)%> -<% end %> \ No newline at end of file +

<%= l(:label_no_data) %>

+<% end %> diff --git a/app/views/issues/_bulk_edit_form.rhtml b/app/views/issues/bulk_edit.rhtml similarity index 78% rename from app/views/issues/_bulk_edit_form.rhtml rename to app/views/issues/bulk_edit.rhtml index e9e1cef..d192622 100644 --- a/app/views/issues/_bulk_edit_form.rhtml +++ b/app/views/issues/bulk_edit.rhtml @@ -1,6 +1,12 @@ -
-
<%= l(:label_bulk_edit_selected_issues) %> +

<%= l(:label_bulk_edit_selected_issues) %>

+ + +<% form_tag() do %> +<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %> +
+
+<%= l(:label_change_properties) %>

<% if @available_statuses.any? %>

- -
-<%= text_area_tag 'notes', '', :cols => 80, :rows => 5 %> -
-

<%= 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;' %>

+ +
<%= l(:field_notes) %> +<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %> +<%= wikitoolbar_for 'notes' %>
+ +

<%= submit_tag l(:button_submit) %> +<% end %> diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml index 46b1770..b3a03b0 100644 --- a/app/views/issues/context_menu.rhtml +++ b/app/views/issues/context_menu.rhtml @@ -1,40 +1,45 @@ -<% back_to = url_for(:controller => 'issues', :action => 'index', :project_id => @project) %>

diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml index 48697c5..c5f26df 100644 --- a/app/views/issues/index.rhtml +++ b/app/views/issues/index.rhtml @@ -31,7 +31,6 @@ <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> <% end %>
-

<%=h @query.name %>

<% html_title @query.name %> @@ -41,7 +40,6 @@ <% if @issues.empty? %>

<%= l(:label_no_data) %>

<% else %> -<% 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} %>
<%= l(:label_export_to) %> @@ -51,7 +49,6 @@

<%= pagination_links_full @issue_pages, @issue_count %>

<% end %> <% end %> -<% end %> <% content_for :sidebar do %> <%= render :partial => 'issues/sidebar' %> @@ -60,13 +57,4 @@ <% content_for :header_tags do %> <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %> <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %> - <%= javascript_include_tag 'calendar/calendar' %> - <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> - <%= javascript_include_tag 'calendar/calendar-setup' %> - <%= stylesheet_link_tag 'calendar' %> - <%= javascript_include_tag 'context_menu' %> - <%= stylesheet_link_tag 'context_menu' %> <% end %> - - -<%= javascript_tag 'new ContextMenu({})' %> diff --git a/app/views/projects/move_issues.rhtml b/app/views/issues/move.rhtml similarity index 50% rename from app/views/projects/move_issues.rhtml rename to app/views/issues/move.rhtml index 95eaf9d..c74270f 100644 --- a/app/views/projects/move_issues.rhtml +++ b/app/views/issues/move.rhtml @@ -1,23 +1,15 @@ -

<%=l(:button_move)%>

+

<%= l(:button_move) %>

+ -<% form_tag({:action => 'move_issues', :id => @project}, :class => 'tabular', :id => 'move_form') do %> +<% form_tag({}, :id => 'move_form') do %> +<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %> -
-

-<% for issue in @issues %> - <%= link_to_issue issue %>: <%=h issue.subject %> - <%= hidden_field_tag "issue_ids[]", issue.id %>
-<% end %> -(<%= @issues.length%> <%= lwr(:label_issue, @issues.length)%>)

- -  - - +

<%= select_tag "new_project_id", - options_from_collection_for_select(@projects, 'id', 'name', @target_project.id), - :onchange => remote_function(:url => {:action => 'move_issues' , :id => @project}, + options_from_collection_for_select(@allowed_projects, 'id', 'name', @target_project.id), + :onchange => remote_function(:url => {:action => 'move' , :id => @project}, :method => :get, :update => 'content', :with => "Form.serialize('move_form')") %>

@@ -25,5 +17,6 @@

<%= select_tag "new_tracker_id", "" + options_from_collection_for_select(@trackers, "id", "name") %>

+ <%= submit_tag l(:button_move) %> <% end %> diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index a16dc60..3392aef 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -3,7 +3,7 @@ <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %> <%= watcher_tag(@issue, User.current) %> <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> -<%= link_to_if_authorized l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon icon-move' %> +<%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %> <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
diff --git a/app/views/my/page.rhtml b/app/views/my/page.rhtml index 26ee44f..4d4c921 100644 --- a/app/views/my/page.rhtml +++ b/app/views/my/page.rhtml @@ -37,6 +37,6 @@ <% end %> -<%= javascript_tag 'new ContextMenu({})' %> +<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> <% html_title(l(:label_my_page)) -%> diff --git a/lang/bg.yml b/lang/bg.yml index bc0e070..164d676 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -568,3 +568,4 @@ label_associated_revisions: Асоциирани ревизии setting_user_format: Потребителски формат text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/cs.yml b/lang/cs.yml index 35bda2b..86a111c 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/de.yml b/lang/de.yml index fd296b6..c5a6cdf 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -568,3 +568,4 @@ enumeration_doc_categories: Dokumentenkategorien enumeration_activities: Aktivitäten (Zeiterfassung) text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/en.yml b/lang/en.yml index b86eeef..a37be93 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -542,6 +542,7 @@ text_user_mail_option: "For unselected projects, you will only receive notificat text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." text_load_default_configuration: Load the default configuration text_status_changed_by_changeset: Applied in changeset %s. +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' default_role_manager: Manager default_role_developper: Developer diff --git a/lang/es.yml b/lang/es.yml index 60e4a4d..1bc7d00 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -571,3 +571,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/fi.yml b/lang/fi.yml index 38cf19a..7a88483 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -572,3 +572,4 @@ label_associated_revisions: Liittyvät versiot setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/fr.yml b/lang/fr.yml index e428935..8c569fc 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -543,6 +543,7 @@ text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seule text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé." text_load_default_configuration: Charger le paramétrage par défaut text_status_changed_by_changeset: Appliqué par commit %s. +text_issues_destroy_confirmation: 'Etes-vous sûr de vouloir supprimer le(s) demandes(s) selectionnée(s) ?' default_role_manager: Manager default_role_developper: Développeur diff --git a/lang/he.yml b/lang/he.yml index ca57755..6631a45 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/it.yml b/lang/it.yml index f227c69..3c77c97 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/ja.yml b/lang/ja.yml index 92b79b6..50d23e8 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/ko.yml b/lang/ko.yml index 097d747..62ec95e 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/lt.yml b/lang/lt.yml index 3106bd7..a7b1273 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -569,3 +569,4 @@ label_associated_revisions: susijusios revizijos setting_user_format: Vartotojo atvaizdavimo formatas text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/nl.yml b/lang/nl.yml index 1c76180..7b5f6bf 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/pl.yml b/lang/pl.yml index 3ebce3e..5c12c65 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 688ce0c..4616e83 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/pt.yml b/lang/pt.yml index 4734c66..31df179 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/ro.yml b/lang/ro.yml index be566b9..52e8566 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -568,3 +568,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/ru.yml b/lang/ru.yml index 6921169..c720dfb 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -569,3 +569,4 @@ enumeration_doc_categories: Категории документов enumeration_activities: Действия (учет времени) text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/sr.yml b/lang/sr.yml index ddf84a5..c32394d 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/sv.yml b/lang/sv.yml index 4bec394..40a1ce8 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -569,3 +569,4 @@ label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 50a6384..601f5c2 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -568,3 +568,4 @@ enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (time tracking) text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lang/zh.yml b/lang/zh.yml index dc431f5..fd513a9 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -571,3 +571,4 @@ label_associated_revisions: 相关的版本 setting_user_format: 用户显示格式 text_status_changed_by_changeset: Applied in changeset %s. label_more: More +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/lib/redmine.rb b/lib/redmine.rb index 6915101..9bec554 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -31,11 +31,10 @@ Redmine::AccessControl.map do |map| :queries => :index, :reports => :issue_report}, :public => true map.permission :add_issues, {:issues => :new} - map.permission :edit_issues, {:projects => :bulk_edit_issues, - :issues => [:edit, :destroy_attachment]} + map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]} map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]} map.permission :add_issue_notes, {:issues => :edit} - map.permission :move_issues, {:projects => :move_issues}, :require => :loggedin + map.permission :move_issues, {:issues => :move}, :require => :loggedin map.permission :delete_issues, {:issues => :destroy}, :require => :member # Queries map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 5ad04e9..d77362a 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -1,3 +1,6 @@ +/* redMine - project management software + Copyright (C) 2006-2008 Jean-Philippe Lang */ + function checkAll (id, checked) { var el = document.getElementById(id); for (var i = 0; i < el.elements.length; i++) { @@ -49,16 +52,6 @@ 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; -} - function collapseScmEntry(id) { var els = document.getElementsByClassName(id, 'browser'); for (var i = 0; i < els.length; i++) { diff --git a/public/javascripts/context_menu.js b/public/javascripts/context_menu.js index 11754cd..e3f128d 100644 --- a/public/javascripts/context_menu.js +++ b/public/javascripts/context_menu.js @@ -1,47 +1,161 @@ +/* redMine - project management software + Copyright (C) 2006-2008 Jean-Philippe Lang */ + +var observingContextMenuClick; + ContextMenu = Class.create(); ContextMenu.prototype = { - initialize: function (options) { - this.options = Object.extend({selector: '.hascontextmenu'}, options || { }); - - Event.observe(document, 'click', function(e){ - var t = Event.findElement(e, 'a'); - if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) { - Event.stop(e); - } else { - $('context-menu').hide(); - if (this.selection) { - this.selection.removeClassName('context-menu-selection'); - } - } - - }.bind(this)); - - $$(this.options.selector).invoke('observe', (window.opera ? 'click' : 'contextmenu'), function(e){ - if (window.opera && !e.ctrlKey) { - return; - } - this.show(e); - }.bind(this)); - + initialize: function (url) { + this.url = url; + + // prevent selection when using Ctrl/Shit key + var tables = $$('table.issues'); + for (i=0; i 0) { inputs[0].checked = checked; } + }, + + isSelected: function(tr) { + return Element.hasClassName(tr, 'context-menu-selection'); + } +} - var tr = Event.findElement(e, 'tr'); - tr.addClassName('context-menu-selection'); - this.selection = tr; - var id = tr.id.substring(6, tr.id.length); - /* TODO: do not hard code path */ - new Ajax.Updater({success:'context-menu'}, '../../issues/context_menu/' + id, {asynchronous:true, evalScripts:true, onComplete:function(request){ - Effect.Appear('context-menu', {duration: 0.20}); - if (window.parseStylesheets) { window.parseStylesheets(); } - }}) +function toggleIssuesSelection(el) { + var boxes = el.getElementsBySelector('input[type=checkbox]'); + var all_checked = true; + for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } } + for (i = 0; i < boxes.length; i++) { + if (all_checked) { + boxes[i].checked = false; + boxes[i].up('tr').removeClassName('context-menu-selection'); + } else if (boxes[i].checked == false) { + boxes[i].checked = true; + boxes[i].up('tr').addClassName('context-menu-selection'); + } } } diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 6fdbb03..4f28ab2 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -197,6 +197,28 @@ class IssuesControllerTest < Test::Unit::TestCase assert_not_nil assigns(:issue) assert_equal Issue.find(1), assigns(:issue) end + + def test_get_edit_with_params + @request.session[:user_id] = 2 + get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 } + assert_response :success + assert_template 'edit' + + issue = assigns(:issue) + assert_not_nil issue + + assert_equal 5, issue.status_id + assert_tag :select, :attributes => { :name => 'issue[status_id]' }, + :child => { :tag => 'option', + :content => 'Closed', + :attributes => { :selected => 'selected' } } + + assert_equal 7, issue.priority_id + assert_tag :select, :attributes => { :name => 'issue[priority_id]' }, + :child => { :tag => 'option', + :content => 'Urgent', + :attributes => { :selected => 'selected' } } + end def test_post_edit @request.session[:user_id] = 2 @@ -305,12 +327,105 @@ class IssuesControllerTest < Test::Unit::TestCase # No email should be sent assert ActionMailer::Base.deliveries.empty? end + + def test_bulk_edit + @request.session[:user_id] = 2 + # update issues priority + post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => '' + 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.find(:first, :order => 'created_on DESC').notes + end + + def test_move_one_issue_to_another_project + @request.session[:user_id] = 1 + post :move, :id => 1, :new_project_id => 2 + assert_redirected_to 'projects/ecookbook/issues' + assert_equal 2, Issue.find(1).project_id + end + + def test_bulk_move_to_another_project + @request.session[:user_id] = 1 + post :move, :ids => [1, 2], :new_project_id => 2 + assert_redirected_to 'projects/ecookbook/issues' + # Issues moved to project 2 + assert_equal 2, Issue.find(1).project_id + assert_equal 2, Issue.find(2).project_id + # No tracker change + assert_equal 1, Issue.find(1).tracker_id + assert_equal 2, Issue.find(2).tracker_id + end + + def test_bulk_move_to_another_tracker + @request.session[:user_id] = 1 + post :move, :ids => [1, 2], :new_tracker_id => 2 + assert_redirected_to 'projects/ecookbook/issues' + assert_equal 2, Issue.find(1).tracker_id + assert_equal 2, Issue.find(2).tracker_id + end - def test_context_menu + def test_context_menu_one_issue + @request.session[:user_id] = 2 + get :context_menu, :ids => [1] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Edit', + :attributes => { :href => '/issues/edit/1', + :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Closed', + :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5', + :class => '' } + assert_tag :tag => 'a', :content => 'Immediate', + :attributes => { :href => '/issues/edit/1?issue%5Bpriority_id%5D=8', + :class => '' } + assert_tag :tag => 'a', :content => 'Dave Lopper', + :attributes => { :href => '/issues/edit/1?issue%5Bassigned_to_id%5D=3', + :class => '' } + assert_tag :tag => 'a', :content => 'Copy', + :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1', + :class => 'icon-copy' } + assert_tag :tag => 'a', :content => 'Move', + :attributes => { :href => '/issues/move?ids%5B%5D=1', + :class => 'icon-move' } + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '/issues/destroy?ids%5B%5D=1', + :class => 'icon-del' } + end + + def test_context_menu_one_issue_by_anonymous + get :context_menu, :ids => [1] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '#', + :class => 'icon-del disabled' } + end + + def test_context_menu_multiple_issues_of_same_project + @request.session[:user_id] = 2 + get :context_menu, :ids => [1, 2] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Edit', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2', + :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Move', + :attributes => { :href => '/issues/move?ids%5B%5D=1&ids%5B%5D=2', + :class => 'icon-move' } + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '/issues/destroy?ids%5B%5D=1&ids%5B%5D=2', + :class => 'icon-del' } + end + + def test_context_menu_multiple_issues_of_different_project @request.session[:user_id] = 2 - get :context_menu, :id => 1 + get :context_menu, :ids => [1, 2, 4] assert_response :success assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '#', + :class => 'icon-del disabled' } end def test_destroy diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 71a3e3d..92ac6f0 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -93,32 +93,6 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_nil Project.find_by_id(1) 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', :assigned_to_id => '' - 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.find(:first, :order => 'created_on DESC').notes - end - - def test_move_issues_to_another_project - @request.session[:user_id] = 1 - post :move_issues, :id => 1, :issue_ids => [1, 2], :new_project_id => 2 - assert_redirected_to 'projects/ecookbook/issues' - assert_equal 2, Issue.find(1).project_id - assert_equal 2, Issue.find(2).project_id - end - - def test_move_issues_to_another_tracker - @request.session[:user_id] = 1 - post :move_issues, :id => 1, :issue_ids => [1, 2], :new_tracker_id => 2 - assert_redirected_to 'projects/ecookbook/issues' - assert_equal 2, Issue.find(1).tracker_id - assert_equal 2, Issue.find(2).tracker_id - end - def test_list_files get :list_files, :id => 1 assert_response :success