diff --git a/app/controllers/context_menus_controller.rb b/app/controllers/context_menus_controller.rb index f9c8c69ee2..635c491483 100644 --- a/app/controllers/context_menus_controller.rb +++ b/app/controllers/context_menus_controller.rb @@ -20,6 +20,7 @@ class ContextMenusController < ApplicationController helper :watchers helper :issues + helper :projects before_action :find_issues, :only => :issues @@ -95,4 +96,31 @@ class ContextMenusController < ApplicationController render :layout => false end + + def versions + @versions = Version.where(:id => params[:ids]).preload(:project).to_a + + (render_404; return) unless @versions.present? + if @versions.size == 1 + @version = @versions.first + end + + @version_ids = @versions.collect(&:id).sort + + @allowed_statuses = Version::VERSION_STATUSES + @allowed_sharings = Version::VERSION_SHARINGS + + projects = @versions.collect(&:project).compact.uniq + project = projects.first if projects.size == 1 + edit_allowed = + if project + User.current.allowed_to?(:manage_versions, project) && @versions.all?{|version| version.project == project} + else + false + end + @can = {:edit => edit_allowed, :delete => edit_allowed} + @back = back_url + + render :layout => false + end end diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb index a4e84ae337..2ea5f61459 100644 --- a/app/controllers/versions_controller.rb +++ b/app/controllers/versions_controller.rb @@ -20,9 +20,10 @@ class VersionsController < ApplicationController menu_item :roadmap model_object Version - before_action :find_model_object, :except => [:index, :new, :create, :close_completed] - before_action :find_project_from_association, :except => [:index, :new, :create, :close_completed] + before_action :find_model_object, :except => [:index, :new, :create, :close_completed, :destroy, :bulk_update] + before_action :find_project_from_association, :except => [:index, :new, :create, :close_completed, :destroy, :bulk_update] before_action :find_project_by_project_id, :only => [:index, :new, :create, :close_completed] + before_action :find_versions, :only => [:destroy, :bulk_update] before_action :authorize accept_api_auth :index, :show, :create, :update, :destroy @@ -149,19 +150,32 @@ class VersionsController < ApplicationController end def destroy - if @version.deletable? - @version.destroy - respond_to do |format| - format.html {redirect_back_or_default settings_project_path(@project, :tab => 'versions')} - format.api {render_api_ok} + destroyed = Version.transaction do + @versions.each do |v| + unless v.deletable? + raise ActiveRecord::Rollback + end + unless v.destroy && v.destroyed? + raise ActiveRecord::Rollback + end end - else - respond_to do |format| - format.html do + end + + respond_to do |format| + format.html do + if destroyed + flash[:notice] = l(:notice_successful_delete) + else flash[:error] = l(:notice_unable_delete_version) - redirect_to settings_project_path(@project, :tab => 'versions') end - format.api {head :unprocessable_entity} + redirect_back_or_default settings_project_path(@project, :tab => 'versions') + end + format.api do + if destroyed + render_api_ok + else + head :unprocessable_entity + end end end end @@ -173,8 +187,52 @@ class VersionsController < ApplicationController end end + def bulk_update + attributes = parse_params_for_bulk_update(params[:version]) + + unsaved_versions = [] + saved_versions = [] + + Version.transaction do + @versions.each do |version| + version.reload + attrs = attributes.dup + attrs.delete('sharing') unless version.allowed_sharings.include?(attrs['sharing']) + version.safe_attributes = attrs + next unless version.changed? + + call_hook( + :controller_versions_bulk_update_before_save, + {:params => params, :version => version} + ) + if version.save + saved_versions << version + else + flash[:error] = version.errors.full_messages.join(', ') unless flash[:error] + unsaved_versions << version + raise ActiveRecord::Rollback + end + end + end + + if unsaved_versions.empty? && saved_versions.present? + flash[:notice] = l(:notice_successful_update) + end + redirect_back_or_default settings_project_path(@project, :tab => :versions) + end + private + def find_versions + @versions = Version.where(:id => params[:id] || params[:ids]).preload(:project).to_a + raise ActiveRecord::RecordNotFound if @versions.empty? + + projects = @versions.collect(&:project).compact.uniq + @project = projects.first if projects.size == 1 + rescue ActiveRecord::RecordNotFound + render_404 + end + def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil) if ids = params[:tracker_ids] @selected_tracker_ids = diff --git a/app/helpers/routes_helper.rb b/app/helpers/routes_helper.rb index 7c50bad1a6..3da69b8561 100644 --- a/app/helpers/routes_helper.rb +++ b/app/helpers/routes_helper.rb @@ -97,6 +97,14 @@ module RoutesHelper end end + def _bulk_update_versions_path(version, *args) + if version + version_path(version, *args) + else + bulk_update_versions_path(*args) + end + end + def board_path(board, *args) project_board_path(board.project, board, *args) end diff --git a/app/views/context_menus/versions.html.erb b/app/views/context_menus/versions.html.erb new file mode 100644 index 0000000000..66cf18544c --- /dev/null +++ b/app/views/context_menus/versions.html.erb @@ -0,0 +1,55 @@ + diff --git a/app/views/projects/settings.html.erb b/app/views/projects/settings.html.erb index eefcb7b069..5ba5e365bc 100644 --- a/app/views/projects/settings.html.erb +++ b/app/views/projects/settings.html.erb @@ -3,3 +3,5 @@ <%= render_tabs project_settings_tabs %> <% html_title(l(:label_settings)) -%> + +<%= context_menu %> diff --git a/app/views/projects/settings/_versions.html.erb b/app/views/projects/settings/_versions.html.erb index 80d67cc241..cd287ceabb 100644 --- a/app/views/projects/settings/_versions.html.erb +++ b/app/views/projects/settings/_versions.html.erb @@ -20,8 +20,16 @@   <% if @versions.present? %> +<%= form_tag({}, :data => {:cm_url => versions_context_menu_path}) do %> + <% is_allowed_manage_versions = User.current.allowed_to?(:manage_versions, @project) %> + @@ -33,7 +41,13 @@ <% @versions.each do |version| %> - + <% is_manage_version = (version.project == @project && is_allowed_manage_versions) %> + + @@ -42,7 +56,7 @@
+ <% if is_allowed_manage_versions && @versions.any?{|version| version.project == @project} %> + <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection', + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> + <% end %> + <%= l(:label_version) %> <%= l(:field_default_version) %> <%= l(:field_effective_date) %>
+ <% if is_manage_version %> + <%= check_box_tag("ids[]", version.id, false, :id => nil) %> + <% end %> + <%= link_to_version version %> <%= checked_image(version.id == @project.default_version_id) %> <%= format_date(version.effective_date) %> <%= link_to_if_authorized(version.wiki_page_title, {:controller => 'wiki', :action => 'show', :project_id => version.project, :id => Wiki.titleize(version.wiki_page_title)}) || h(version.wiki_page_title) unless version.wiki_page_title.blank? || version.project.wiki.nil? %> - <% if version.project == @project && User.current.allowed_to?(:manage_versions, @project) %> + <% if is_manage_version %> <%= link_to l(:button_edit), edit_version_path(version), :class => 'icon icon-edit' %> <%= delete_link version_path(version) %> <% end %> @@ -51,6 +65,7 @@ <% end %>
+<% end %> <% else %>

<%= l(:label_no_data) %>

<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 8fd84ff13d..17fd4bdcb9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1288,6 +1288,7 @@ en: text_no_subject: no subject text_allowed_queries_to_select: Public (to any users) queries only selectable text_setting_config_change: You can configure the behaviour in config/configuration.yml. Please restart the application after editing it. + text_versions_destroy_confirmation: 'Are you sure you want to delete the selected version(s)?' default_role_manager: Manager default_role_developer: Developer diff --git a/config/routes.rb b/config/routes.rb index efb6db8e1e..b92c8f4755 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -233,8 +233,13 @@ Rails.application.routes.draw do match '/news/:id/comments', :to => 'comments#create', :via => :post match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete + match '/versions/context_menu', :to => 'context_menus#versions', :as => :versions_context_menu, :via => [:get, :post] + delete '/versions', :to => 'versions#destroy' resources :versions, :only => [:show, :edit, :update, :destroy] do post 'status_by', :on => :member + collection do + patch 'bulk_update' + end end resources :documents, :only => [:show, :edit, :update, :destroy] do diff --git a/lib/redmine.rb b/lib/redmine.rb index 5721667d81..aab16bf2c1 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -94,7 +94,7 @@ Redmine::AccessControl.map do |map| map.permission :select_project_modules, {:projects => :modules}, :require => :member map.permission :view_members, {:members => [:index, :show]}, :public => true, :read => true map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :new, :create, :edit, :update, :destroy, :autocomplete]}, :require => :member - map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member + map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy, :bulk_update]}, :require => :member map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member # Queries map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member diff --git a/test/functional/context_menus_controller_test.rb b/test/functional/context_menus_controller_test.rb index ccbc4dfba3..3ddcabe3ea 100644 --- a/test/functional/context_menus_controller_test.rb +++ b/test/functional/context_menus_controller_test.rb @@ -470,4 +470,54 @@ class ContextMenusControllerTest < Redmine::ControllerTest assert_select 'a.disabled', :text => 'Edit' end + + def test_context_menu_one_version_should_link_to_version_path + @request.session[:user_id] = 2 + get( + :versions, + :params => { + :ids => [2] + } + ) + assert_response :success + + assert_select 'a.icon-edit[href=?]', '/versions/2/edit', :text => 'Edit' + assert_select 'a.icon-del[href=?]', '/versions?ids%5B%5D=2', :text => 'Delete' + + # Statuses + assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bstatus%5D=open', :text => 'open' + assert_select 'a.icon-checked.disabled[href=?]', '#', :text => 'locked' + assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bstatus%5D=closed', :text => 'closed' + # Sharings + assert_select 'a.icon-checked.disabled[href=?]', '#', :text => 'Not shared' + assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bsharing%5D=descendants', :text => 'With subprojects' + assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bsharing%5D=hierarchy', :text => 'With project hierarchy' + assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bsharing%5D=tree', :text => 'With project tree' + assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bsharing%5D=system', :text => 'With all projects' + end + + def test_context_menu_multiple_versions_should_link_to_bulk_update_versions_path + @request.session[:user_id] = 2 + get( + :versions, + :params => { + :ids => [2, 3] + } + ) + assert_response :success + + assert_select 'a.icon-edit', :text => 'Edit', :count => 0 + assert_select 'a.icon-del[href=?]', '/versions?ids%5B%5D=2&ids%5B%5D=3', :text => 'Delete' + + # Statuses + assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bstatus%5D=open', :text => 'open' + assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bstatus%5D=locked', :text => 'locked' + assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bstatus%5D=closed', :text => 'closed' + # Sharings + assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bsharing%5D=none', :text => 'Not shared' + assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bsharing%5D=descendants', :text => 'With subprojects' + assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bsharing%5D=hierarchy', :text => 'With project hierarchy' + assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bsharing%5D=tree', :text => 'With project tree' + assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bsharing%5D=system', :text => 'With all projects' + end end diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index f7d2dee265..af7bacbe91 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -867,6 +867,52 @@ class ProjectsControllerTest < Redmine::ControllerTest end end + def test_versions_in_settings_should_show_context_menu + @request.session[:user_id] = 2 + get( + :settings, + :params => { + :id => 'ecookbook', + :tab => 'versions', + :version_status => '', + :version_name => '.1', + } + ) + assert_response :success + assert_select 'table.versions' do + assert_select 'thead' do + assert_select 'th.checkbox input[type=checkbox][name=?]', 'check_all', 1 + end + assert_select 'tbody tr.hascontextmenu' do + assert_select 'td.checkbox input[type=checkbox][name=?]', 'ids[]', 1 + assert_select 'td.name', :text => '0.1' + end + end + end + + def test_versions_in_settings_with_version_shared_by_other_project_should_not_show_context_menu + @request.session[:user_id] = 2 + get( + :settings, + :params => { + :id => 'ecookbook', + :tab => 'versions', + :version_status => '', + :version_name => 'subproject', + } + ) + assert_response :success + assert_select 'table.versions' do + assert_select 'thead' do + assert_select 'th.checkbox input[type=checkbox][name=?]', 'check_all', 0 + end + assert_select 'tbody tr.shared' do + assert_select 'td.checkbox input[type=checkbox][name=?]', 'ids[]', 0 + assert_select 'td.name', :text => 'Private child of eCookbook - Private Version of public subproject' + end + end + end + def test_settings_should_show_locked_members user = User.generate! member = User.add_to_project(user, Project.find(1)) diff --git a/test/functional/versions_controller_test.rb b/test/functional/versions_controller_test.rb index 3691f71f5f..024a715c3f 100644 --- a/test/functional/versions_controller_test.rb +++ b/test/functional/versions_controller_test.rb @@ -348,4 +348,78 @@ class VersionsControllerTest < Redmine::ControllerTest assert_include 'Assigned', response.body assert_include 'Closed', response.body end + + def test_bulk_update_on_status + ids = [2, 3] + update_attr = {:status => 'closed'} + assert_equal 2, Version.where(:id => ids).where.not(update_attr).count + + @request.session[:user_id] = 2 + patch :bulk_update, :params => { + :ids => ids, + :version => update_attr + } + assert_redirected_to :controller => :projects, :action => :settings, + :tab => :versions, :id => :ecookbook + assert_equal 'Successful update.', flash[:notice] + + assert_equal 2, Version.where(:id => ids).where(update_attr).count + end + + def test_bulk_update_on_sharing + ids = [2, 3] + update_attr = {:sharing => 'descendants'} + assert_equal 2, Version.where(:id => ids).where.not(update_attr).count + + @request.session[:user_id] = 2 + patch :bulk_update, :params => { + :ids => ids, + :version => update_attr + } + assert_redirected_to :controller => :projects, :action => :settings, + :tab => :versions, :id => :ecookbook + assert_equal 'Successful update.', flash[:notice] + + assert_equal 2, Version.where(:id => ids).where(update_attr).count + end + + def test_bulk_update_with_validation_failure + ids = [2, 3] + versions = Version.where(:id => ids).reorder(:id => :asc) + assert_equal 2, versions.count + + @request.session[:user_id] = 2 + patch :bulk_update, :params => { + :ids => ids, + :version => { + :status => 'invalid' + } + } + assert_redirected_to :controller => :projects, :action => :settings, + :tab => :versions, :id => :ecookbook + assert_equal 'Status is not included in the list', flash[:error] + + assert_equal versions, Version.where(:id => ids).reorder(:id => :asc) + end + + def test_bulk_destroy + Issue.update(:fixed_version_id => nil) + @request.session[:user_id] = 2 + assert_difference 'Version.count', -2 do + delete :destroy, :params => {:ids => [2, 3]} + end + assert_redirected_to :controller => :projects, :action => :settings, + :tab => :versions, :id => :ecookbook + assert_equal 'Successful deletion.', flash[:notice] + end + + def test_bulk_destroy_with_version_in_use_should_fail + @request.session[:user_id] = 2 + assert_no_difference 'Version.count' do + delete :destroy, :params => {:ids => [2, 3]} + end + assert_redirected_to :controller => :projects, :action => :settings, + :tab => :versions, :id => :ecookbook + assert_equal 'Unable to delete version.', flash[:error] + end end diff --git a/test/helpers/routes_helper_test.rb b/test/helpers/routes_helper_test.rb index 9f1ac7280b..fc87b8704c 100644 --- a/test/helpers/routes_helper_test.rb +++ b/test/helpers/routes_helper_test.rb @@ -20,7 +20,7 @@ require File.expand_path('../../test_helper', __FILE__) class RoutesHelperTest < Redmine::HelperTest - fixtures :projects, :issues + fixtures :projects, :issues, :versions include Rails.application.routes.url_helpers @@ -47,4 +47,9 @@ class RoutesHelperTest < Redmine::HelperTest assert_equal 'http://test.host/projects/ecookbook/issues?set_filter=1', _project_issues_url(Project.find(1), set_filter: 1) assert_equal 'http://test.host/issues?set_filter=1', _project_issues_url(nil, set_filter: 1) end + + def test_bulk_update_versions_path + assert_equal '/versions/bulk_update', _bulk_update_versions_path(nil, nil) + assert_equal '/versions/1', _bulk_update_versions_path(Version.find(1), nil) + end end diff --git a/test/integration/routing/context_menus_test.rb b/test/integration/routing/context_menus_test.rb index 8750e07176..895fa32efb 100644 --- a/test/integration/routing/context_menus_test.rb +++ b/test/integration/routing/context_menus_test.rb @@ -29,4 +29,9 @@ class RoutingContextMenusTest < Redmine::RoutingTest should_route 'GET /issues/context_menu' => 'context_menus#issues' should_route 'POST /issues/context_menu' => 'context_menus#issues' end + + def test_context_menus_versions + should_route 'GET /versions/context_menu' => 'context_menus#versions' + should_route 'POST /versions/context_menu' => 'context_menus#versions' + end end diff --git a/test/integration/routing/versions_test.rb b/test/integration/routing/versions_test.rb index cf5be2981c..b24e00b025 100644 --- a/test/integration/routing/versions_test.rb +++ b/test/integration/routing/versions_test.rb @@ -35,4 +35,9 @@ class RoutingVersionsTest < Redmine::RoutingTest should_route 'POST /versions/1/status_by' => 'versions#status_by', :id => '1' end + + def test_versions_bulk_edit + should_route 'PATCH /versions/bulk_update' => 'versions#bulk_update' + should_route 'DELETE /versions' => 'versions#destroy' + end end