diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index da7259da16..e1fc76cb41 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -23,13 +23,13 @@ class ProjectsController < ApplicationController menu_item :projects, :only => [:index, :new, :copy, :create] before_action :find_project, - :except => [:index, :autocomplete, :list, :new, :create, :copy] + :except => [:index, :autocomplete, :list, :new, :create] before_action :authorize, - :except => [:index, :autocomplete, :list, :new, :create, :copy, + :except => [:index, :autocomplete, :list, :new, :create, :archive, :unarchive, :destroy] before_action :authorize_global, :only => [:new, :create] - before_action :require_admin, :only => [:copy, :archive, :unarchive] + before_action :require_admin, :only => [:archive, :unarchive] accept_atom_auth :index accept_api_auth :index, :show, :create, :update, :destroy, :archive, :unarchive, :close, :reopen require_sudo_mode :destroy @@ -140,6 +140,7 @@ class ProjectsController < ApplicationController end def copy + @project = nil # Reset because source project was set in @project for authorize. @issue_custom_fields = IssueCustomField.sorted.to_a @trackers = Tracker.sorted.to_a @source_project = Project.find(params[:id]) diff --git a/app/models/role.rb b/app/models/role.rb index 790d09f271..b6130b77b0 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -80,6 +80,8 @@ class Role < ActiveRecord::Base validates_presence_of :name validates_uniqueness_of :name, :case_sensitive => true validates_length_of :name, :maximum => 255 + validate :check_the_prerequisites_for_copy_project_permission + validates_inclusion_of( :issues_visibility, :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first), @@ -318,4 +320,12 @@ class Role < ActiveRecord::Base role end private_class_method :find_or_create_system_role + + def check_the_prerequisites_for_copy_project_permission + if self.permissions.include?(:copy_project) && + self.permissions.exclude?(:add_project) && + self.permissions.exclude?(:add_subprojects) + errors.add(:base, l(:error_cannot_have_copy_project_permission)) + end + end end diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 5c7ef440c2..53fa3f3c5c 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -5,6 +5,9 @@ <% if User.current.allowed_to?(:add_subprojects, @project) %> <%= link_to l(:label_subproject_new), new_project_path(:parent_id => @project), :class => 'icon icon-add' %> <% end %> + <% if User.current.allowed_to?(:copy_project, @project) %> + <%= link_to(l(:button_copy), copy_project_path(@project), :class => 'icon icon-copy') %> + <% end %> <% if User.current.allowed_to?(:close_project, @project) %> <% if @project.active? %> <%= link_to l(:button_close), close_project_path(@project), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock' %> diff --git a/app/views/roles/_form.html.erb b/app/views/roles/_form.html.erb index e149b0011e..7ca25fb380 100644 --- a/app/views/roles/_form.html.erb +++ b/app/views/roles/_form.html.erb @@ -56,7 +56,8 @@ <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 62577376fa..0636ccfea2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -245,6 +245,7 @@ en: error_attachment_not_found: "Attachment %{name} not found" error_invalid_authenticity_token: "Invalid form authenticity token." error_query_statement_invalid: "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator." + error_cannot_have_copy_project_permission: "Can't have copy_project permission without add_project permission or add_subprojects permission." mail_subject_lost_password: "Your %{value} password" mail_body_lost_password: 'To change your password, click on the following link:' diff --git a/lib/redmine/preparation.rb b/lib/redmine/preparation.rb index 2ba3b5447f..bf10a2c6ec 100644 --- a/lib/redmine/preparation.rb +++ b/lib/redmine/preparation.rb @@ -44,6 +44,7 @@ module Redmine 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 :add_subprojects, {:projects => [:new, :create]}, :require => :member + map.permission :copy_project, {:projects => [:copy]}, :require => :member # Queries map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin diff --git a/public/javascripts/application.js b/public/javascripts/application.js index efff38da7d..df862e812c 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -972,6 +972,16 @@ function blockEventPropagation(event) { event.preventDefault(); } +function toggleCopyProjectCheckboxInit() { + $('input#role_permissions_add_project, input#role_permissions_add_subprojects').change(function () { + if (['input#role_permissions_add_project', 'input#role_permissions_add_subprojects'].some(el => $(el).is(':checked'))) { + $('input#role_permissions_copy_project').attr('disabled', false) + } else { + $('input#role_permissions_copy_project').attr('disabled', true) + } + }); +} + function toggleDisabledOnChange() { var checked = $(this).is(':checked'); $($(this).data('disables')).attr('disabled', checked); @@ -1029,6 +1039,7 @@ function toggleNewObjectDropdown() { $(document).ready(function(){ $('#content').on('change', 'input[data-disables], input[data-enables], input[data-shows]', toggleDisabledOnChange); toggleDisabledInit(); + toggleCopyProjectCheckboxInit(); $('#content').on('click', '.toggle-multiselect', function() { toggleMultiSelect($(this).siblings('select')); diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 837de343ab..1650b57534 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -1259,8 +1259,19 @@ class ProjectsControllerTest < Redmine::ControllerTest end end - def test_get_copy + def test_get_copy_by_admin_user @request.session[:user_id] = 1 # admin + orig = Project.find(1) # Login user is no member + get(:copy, :params => {:id => orig.id}) + assert_response :success + + assert_select 'textarea[name=?]', 'project[description]', :text => orig.description + assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1 + end + + def test_get_copy_by_non_admin_user_with_copy_project_permission + @request.session[:user_id] = 3 + Role.find(2).add_permission!(:copy_project, :add_project) orig = Project.find(1) get(:copy, :params => {:id => orig.id}) assert_response :success @@ -1269,6 +1280,14 @@ class ProjectsControllerTest < Redmine::ControllerTest assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1 end + def test_get_copy_by_non_admin_user_without_copy_project_permission_should_respond_with_403 + @request.session[:user_id] = 3 + Role.find(2).remove_permission! :copy_project + orig = Project.find(1) + get(:copy, :params => {:id => orig.id}) + assert_response 403 + end + def test_get_copy_with_invalid_source_should_respond_with_404 @request.session[:user_id] = 1 get(:copy, :params => {:id => 99}) @@ -1315,6 +1334,66 @@ class ProjectsControllerTest < Redmine::ControllerTest assert_equal 0, project.members.count end + def test_post_copy_by_non_admin_user_with_copy_project_and_add_project_permission + @request.session[:user_id] = 3 + Role.find(2).add_permission!(:copy_project, :add_project) + CustomField.delete_all + + assert_difference 'Project.count' do + post( + :copy, + :params => { + :id => 1, + :project => { + :name => 'Copy', + :identifier => 'unique-copy', + :tracker_ids => ['1', '2', '3', ''], + :enabled_module_names => %w(issue_tracking time_tracking) + }, + :only => %w(issues versions) + } + ) + end + project = Project.find('unique-copy') + source = Project.find(1) + assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort + + assert_equal source.versions.count, project.versions.count, "All versions were not copied" + assert_equal source.issues.count, project.issues.count, "All issues were not copied" + assert_equal 0, project.members.count + end + + def test_post_copy_by_non_admin_user_with_copy_project_and_add_subprojects_permission + @request.session[:user_id] = 3 + Role.find(2).add_permission!(:copy_project, :add_subprojects) + CustomField.delete_all + + assert_difference 'Project.count' do + post( + :copy, + :params => { + :id => 1, + :project => { + :name => 'Copy', + :identifier => 'unique-copy', + :tracker_ids => ['1', '2', '3', ''], + :enabled_module_names => %w(issue_tracking time_tracking), + :parent_id => 1 + }, + :only => %w(issues versions) + } + ) + end + project = Project.find('unique-copy') + source = Project.find(1) + assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort + assert_equal source, project.parent + + assert_equal source.versions.count, project.versions.count, "All versions were not copied" + assert_equal source.issues.count, project.issues.count, "All issues were not copied" + assert_equal 0, project.members.count + end + def test_post_copy_should_redirect_to_settings_when_successful @request.session[:user_id] = 1 # admin post( diff --git a/test/unit/role_test.rb b/test/unit/role_test.rb index 8a1eb76d35..5b7a9f5518 100644 --- a/test/unit/role_test.rb +++ b/test/unit/role_test.rb @@ -22,6 +22,8 @@ require File.expand_path('../../test_helper', __FILE__) class RoleTest < ActiveSupport::TestCase fixtures :roles, :workflows, :trackers + include Redmine::I18n + def setup User.current = nil end @@ -165,4 +167,19 @@ class RoleTest < ActiveSupport::TestCase assert_nil ActiveRecord::Base.connection.select_value("SELECT 1 FROM queries_roles WHERE role_id = #{role.id}") assert [1, 3], query.roles end + + def test_check_the_prerequisites_for_copy_project_permission + role = Role.find(2) + role.remove_permission!(:copy_project, :add_project, :add_subprojects) + + role.permissions = [:copy_project] + assert_not role.valid? + assert_equal l(:error_cannot_have_copy_project_permission), role.errors.messages[:base].first + + role.permissions = [:copy_project, :add_project] + assert role.valid? + + role.permissions = [:copy_project, :add_subprojects] + assert role.valid? + end end