diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index be1b9963d..d6fda560a 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_rss_auth :index accept_api_auth :index, :show, :create, :update, :destroy require_sudo_mode :destroy @@ -139,6 +139,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 5cdc593e9..012f10e3d 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -78,6 +78,8 @@ class Role < ActiveRecord::Base validates_presence_of :name validates_uniqueness_of :name 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), @@ -320,4 +322,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 18b15d840..cd1ffdb3e 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 e149b0011..7ca25fb38 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/de.yml b/config/locales/de.yml index 3975eadfa..d8b974eba 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -905,6 +905,7 @@ de: permission_add_issue_notes: Kommentare hinzufügen permission_add_issue_watchers: Beobachter hinzufügen permission_add_issues: Tickets hinzufügen + permission_copy_project: Projekt kopieren permission_add_messages: Forenbeiträge hinzufügen permission_add_project: Projekt erstellen permission_add_subprojects: Unterprojekte erstellen diff --git a/config/locales/en.yml b/config/locales/en.yml index 9d779a2fe..f190e9002 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -244,6 +244,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:' @@ -511,6 +512,7 @@ en: permission_add_project: Create project permission_add_subprojects: Create subprojects permission_edit_project: Edit project + permission_copy_project: Copy project permission_close_project: Close / reopen the project permission_delete_project: Delete the project permission_select_project_modules: Select project modules diff --git a/lib/redmine.rb b/lib/redmine.rb index 44023834b..17242f52c 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -90,6 +90,7 @@ Redmine::AccessControl.map do |map| 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 b6446c82e..dcfc6edd7 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -967,6 +967,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); @@ -1024,6 +1034,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 a538e8dc6..05ea2dd39 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -1234,8 +1234,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 @@ -1244,6 +1255,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}) @@ -1290,6 +1309,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 7e907233d..f2203d2da 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 @@ -228,4 +230,19 @@ class RoleTest < ActiveSupport::TestCase assert_equal Role::BUILTIN_NON_MEMBER, role.builtin end 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