diff options
37 files changed, 485 insertions, 247 deletions
diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index dbb49405c..85151e905 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 9560a451f..cddfb6f81 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 ff91f34f7..1f5470200 100644 --- a/app/views/issues/_list.rhtml +++ b/app/views/issues/_list.rhtml @@ -1,10 +1,7 @@ -<div id="bulk-edit"></div> -<table class="list"> +<% form_tag({}) do -%> +<table class="list issues"> <thead><tr> - <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><%= link_to image_tag('edit.png'), {}, :onclick => 'toggleIssuesSelection(this.up("form")); return false;' %> </th> <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %> <% query.columns.each do |column| %> @@ -12,14 +9,21 @@ <% end %> </tr></thead> <tbody> - <% issues.each do |issue| %> + <% issues.each do |issue| -%> <tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>"> - <td class="checkbox"><%= check_box_tag("issue_ids[]", issue.id, false, :id => "issue_#{issue.id}", :disabled => (!@project || @project != issue.project)) %></td> + <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false) %></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 %> - <% end %> + <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> </tr> - <% end %> + <% end -%> </tbody> </table> +<% end -%> + +<% content_for :header_tags do -%> + <%= javascript_include_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu' %> +<% end -%> + +<div id="context-menu" style="display: none;"></div> +<%= 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 eb93f8ea1..8900b7359 100644 --- a/app/views/issues/_list_simple.rhtml +++ b/app/views/issues/_list_simple.rhtml @@ -1,5 +1,6 @@ -<% if issues.length > 0 %> - <table class="list"> +<% if issues && issues.any? %> +<% form_tag({}) do %> + <table class="list issues"> <thead><tr> <th>#</th> <th><%=l(:field_tracker)%></th> @@ -9,6 +10,7 @@ <% for issue in issues %> <tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>"> <td class="id"> + <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %> <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> </td> <td><%=h issue.project.name %> - <%= issue.tracker.name %><br /> @@ -20,6 +22,7 @@ <% end %> </tbody> </table> +<% end %> <% else %> - <i><%=l(:label_no_data)%></i> -<% end %>
\ No newline at end of file + <p class="nodata"><%= l(:label_no_data) %></p> +<% end %> diff --git a/app/views/issues/_bulk_edit_form.rhtml b/app/views/issues/bulk_edit.rhtml index e9e1cef86..d19262271 100644 --- a/app/views/issues/_bulk_edit_form.rhtml +++ b/app/views/issues/bulk_edit.rhtml @@ -1,6 +1,12 @@ -<div id="bulk-edit-fields"> -<fieldset class="box"><legend><%= l(:label_bulk_edit_selected_issues) %></legend> +<h2><%= l(:label_bulk_edit_selected_issues) %></h2> +<ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul> + +<% form_tag() do %> +<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %> +<div class="box"> +<fieldset> +<legend><%= l(:label_change_properties) %></legend> <p> <% if @available_statuses.any? %> <label><%= l(:field_status) %>: @@ -28,11 +34,12 @@ <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> + +<fieldset><legend><%= l(:field_notes) %></legend> +<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %> +<%= wikitoolbar_for 'notes' %> </div> + +<p><%= submit_tag l(:button_submit) %> +<% end %> diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml index 46b177067..b3a03b05d 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) %> <ul> +<% if !@issue.nil? -%> <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, :class => 'icon-edit', :disabled => !@can[:edit] %></li> <li class="folder"> <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a> <ul> - <% @statuses.each do |s| %> + <% @statuses.each do |s| -%> <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}}, - :selected => (s == @issue.status), :disabled => !(@allowed_statuses.include?(s)) %></li> - <% end %> + :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li> + <% end -%> </ul> </li> <li class="folder"> <a href="#" class="submenu"><%= l(:field_priority) %></a> <ul> - <% @priorities.each do |p| %> - <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => back_to}, :method => :post, + <% @priorities.each do |p| -%> + <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => @back}, :method => :post, :selected => (p == @issue.priority), :disabled => !@can[:edit] %></li> - <% end %> + <% end -%> </ul> </li> <li class="folder"> <a href="#" class="submenu"><%= l(:field_assigned_to) %></a> <ul> - <% @assignables.each do |u| %> - <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:assigned_to_id => u}, :back_to => back_to}, :method => :post, - :selected => (u == @issue.assigned_to), :disabled => !@can[:assign] %></li> - <% end %> - <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:assigned_to_id => nil}, :back_to => back_to}, :method => :post, - :selected => @issue.assigned_to.nil?, :disabled => !@can[:assign] %></li> + <% @assignables.each do |u| -%> + <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => @back}, :method => :post, + :selected => (u == @issue.assigned_to), :disabled => !@can[:update] %></li> + <% end -%> + <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => @back}, :method => :post, + :selected => @issue.assigned_to.nil?, :disabled => !@can[:update] %></li> </ul> </li> <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon-copy', :disabled => !@can[:copy] %></li> - <li><%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, - :class => 'icon-move', :disabled => !@can[:move] %> - <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, - :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon-del', :disabled => !@can[:delete] %></li> +<% else -%> + <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)}, + :class => 'icon-edit', :disabled => !@can[:edit] %></li> +<% end -%> + + <li><%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)}, + :class => 'icon-move', :disabled => !@can[:move] %></li> + <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)}, + :method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %></li> </ul> diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml index 48697c505..c5f26dfb6 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 %> </div> - <h2><%=h @query.name %></h2> <div id="query_form"></div> <% html_title @query.name %> @@ -41,7 +40,6 @@ <% if @issues.empty? %> <p class="nodata"><%= l(:label_no_data) %></p> <% 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} %> <div class="contextual"> <%= l(:label_export_to) %> @@ -51,7 +49,6 @@ <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p> <% 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 %> - -<div id="context-menu" style="display: none;"></div> -<%= javascript_tag 'new ContextMenu({})' %> diff --git a/app/views/projects/move_issues.rhtml b/app/views/issues/move.rhtml index 95eaf9dec..c74270f1a 100644 --- a/app/views/projects/move_issues.rhtml +++ b/app/views/issues/move.rhtml @@ -1,23 +1,15 @@ -<h2><%=l(:button_move)%></h2> +<h2><%= l(:button_move) %></h2> +<ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul> -<% 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 %> -<div class="box"> -<p><label><%= l(:label_issue_plural) %> :</label> -<% for issue in @issues %> - <%= link_to_issue issue %>: <%=h issue.subject %> - <%= hidden_field_tag "issue_ids[]", issue.id %><br /> -<% end %> -<i>(<%= @issues.length%> <%= lwr(:label_issue, @issues.length)%>)</i></p> - - - -<!--[form:issue]--> +<div class="box tabular"> <p><label for="new_project_id"><%=l(:field_project)%> :</label> <%= 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')") %></p> @@ -25,5 +17,6 @@ <p><label for="new_tracker_id"><%=l(:field_tracker)%> :</label> <%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p> </div> + <%= submit_tag l(:button_move) %> <% end %> diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index a16dc60e0..3392aef83 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' %> </div> diff --git a/app/views/my/page.rhtml b/app/views/my/page.rhtml index 26ee44fcc..4d4c921b6 100644 --- a/app/views/my/page.rhtml +++ b/app/views/my/page.rhtml @@ -37,6 +37,6 @@ <% end %> <div id="context-menu" style="display: none;"></div> -<%= 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 bc0e070c0..164d67671 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 35bda2b6a..86a111cc7 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 fd296b600..c5a6cdf95 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 b86eeef2b..a37be93c7 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 60e4a4d13..1bc7d00fb 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 38cf19a46..7a8848302 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 e428935af..8c569fc5a 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 ca577558a..6631a457f 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 f227c6942..3c77c97a6 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 92b79b6ef..50d23e80f 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 097d74788..62ec95ef3 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 3106bd764..a7b12736a 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 1c76180be..7b5f6bf3a 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 3ebce3e92..5c12c658f 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 688ce0cfd..4616e83a7 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 4734c66fc..31df179f2 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 be566b959..52e856683 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 6921169ce..c720dfb72 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 ddf84a5d3..c32394dc3 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 4bec394ce..40a1ce8f6 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 50a6384f2..601f5c2ec 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 dc431f5c0..fd513a96a 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 69151012b..9bec55409 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 5ad04e91d..d77362a06 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 11754cde8..e3f128d89 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<tables.length; i++) { + tables[i].onselectstart = function () { return false; } // ie + tables[i].onmousedown = function () { return false; } // mozilla + } + + if (!observingContextMenuClick) { + Event.observe(document, 'click', this.Click.bindAsEventListener(this)); + Event.observe(document, (window.opera ? 'click' : 'contextmenu'), this.RightClick.bindAsEventListener(this)); + observingContextMenuClick = true; + } + + this.unselectAll(); + this.lastSelected = null; }, - show: function(e) { + + RightClick: function(e) { + this.hideMenu(); + // do not show the context menu on links + if (Event.findElement(e, 'a') != document) { return; } + // right-click simulated by Alt+Click with Opera + if (window.opera && !e.altKey) { return; } + var tr = Event.findElement(e, 'tr'); + if ((tr == document) || !tr.hasClassName('hascontextmenu')) { return; } Event.stop(e); - Element.hide('context-menu'); - if (this.selection) { - this.selection.removeClassName('context-menu-selection'); + if (!this.isSelected(tr)) { + this.unselectAll(); + this.addSelection(tr); + this.lastSelected = tr; } + this.showMenu(e); + }, + + Click: function(e) { + this.hideMenu(); + if (Event.findElement(e, 'a') != document) { return; } + if (window.opera && e.altKey) { return; } + if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) { + var tr = Event.findElement(e, 'tr'); + if (tr!=document && tr.hasClassName('hascontextmenu')) { + // a row was clicked, check if the click was on checkbox + var box = Event.findElement(e, 'input'); + if (box!=document) { + // a checkbox may be clicked + if (box.checked) { + tr.addClassName('context-menu-selection'); + } else { + tr.removeClassName('context-menu-selection'); + } + } else { + if (e.ctrlKey) { + this.toggleSelection(tr); + } else if (e.shiftKey) { + if (this.lastSelected != null) { + var toggling = false; + var rows = $$('.hascontextmenu'); + for (i=0; i<rows.length; i++) { + if (toggling || rows[i]==tr) { + this.addSelection(rows[i]); + } + if (rows[i]==tr || rows[i]==this.lastSelected) { + toggling = !toggling; + } + } + } else { + this.addSelection(tr); + } + } else { + this.unselectAll(); + this.addSelection(tr); + } + this.lastSelected = tr; + } + } else { + // click is outside the rows + var t = Event.findElement(e, 'a'); + if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) { + Event.stop(e); + } + } + } + }, + + showMenu: function(e) { $('context-menu').style['left'] = (Event.pointerX(e) + 'px'); $('context-menu').style['top'] = (Event.pointerY(e) + 'px'); Element.update('context-menu', ''); + new Ajax.Updater({success:'context-menu'}, this.url, + {asynchronous:true, + evalScripts:true, + parameters:Form.serialize(Event.findElement(e, 'form')), + onComplete:function(request){ + Effect.Appear('context-menu', {duration: 0.20}); + if (window.parseStylesheets) { window.parseStylesheets(); } // IE + }}) + }, + + hideMenu: function() { + Element.hide('context-menu'); + }, + + addSelection: function(tr) { + tr.addClassName('context-menu-selection'); + this.checkSelectionBox(tr, true); + }, + + toggleSelection: function(tr) { + if (this.isSelected(tr)) { + this.removeSelection(tr); + } else { + this.addSelection(tr); + } + }, + + removeSelection: function(tr) { + tr.removeClassName('context-menu-selection'); + this.checkSelectionBox(tr, false); + }, + + unselectAll: function() { + var rows = $$('.hascontextmenu'); + for (i=0; i<rows.length; i++) { + this.removeSelection(rows[i]); + } + }, + + checkSelectionBox: function(tr, checked) { + var inputs = Element.getElementsBySelector(tr, 'input'); + if (inputs.length > 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 6fdbb0341..4f28ab224 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 71a3e3dfe..92ac6f09a 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
|