Adds projects bulk delete (#36691).

Patch by Jens Krämer. 


git-svn-id: https://svn.redmine.org/redmine/trunk@21592 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Marius Balteanu
2022-05-17 20:50:37 +00:00
parent 883aa3b5cc
commit e1d6bfbdcc
8 changed files with 181 additions and 4 deletions

View File

@@ -23,16 +23,16 @@ class ProjectsController < ApplicationController
menu_item :projects, :only => [:index, :new, :copy, :create] menu_item :projects, :only => [:index, :new, :copy, :create]
before_action :find_project, before_action :find_project,
:except => [:index, :autocomplete, :list, :new, :create, :copy] :except => [:index, :autocomplete, :list, :new, :create, :copy, :bulk_destroy]
before_action :authorize, before_action :authorize,
:except => [:index, :autocomplete, :list, :new, :create, :copy, :except => [:index, :autocomplete, :list, :new, :create, :copy,
:archive, :unarchive, :archive, :unarchive,
:destroy] :destroy, :bulk_destroy]
before_action :authorize_global, :only => [:new, :create] before_action :authorize_global, :only => [:new, :create]
before_action :require_admin, :only => [:copy, :archive, :unarchive] before_action :require_admin, :only => [:copy, :archive, :unarchive, :bulk_destroy]
accept_atom_auth :index accept_atom_auth :index
accept_api_auth :index, :show, :create, :update, :destroy, :archive, :unarchive, :close, :reopen accept_api_auth :index, :show, :create, :update, :destroy, :archive, :unarchive, :close, :reopen
require_sudo_mode :destroy require_sudo_mode :destroy, :bulk_destroy
helper :custom_fields helper :custom_fields
helper :issues helper :issues
@@ -315,6 +315,23 @@ class ProjectsController < ApplicationController
@project = nil @project = nil
end end
# Delete selected projects
def bulk_destroy
@projects = Project.where(id: params[:ids]).
where.not(status: Project::STATUS_SCHEDULED_FOR_DELETION).to_a
if @projects.empty?
render_404
return
end
if params[:confirm] == I18n.t(:general_text_Yes)
DestroyProjectsJob.schedule @projects
flash[:notice] = l(:notice_successful_delete)
redirect_to admin_projects_path
end
end
private private
# Returns the ProjectEntry scope for index # Returns the ProjectEntry scope for index

View File

@@ -0,0 +1,31 @@
# frozen_string_literal: true
class DestroyProjectsJob < ApplicationJob
include Redmine::I18n
def self.schedule(projects_to_delete, user: User.current)
# make the projects disappear immediately
projects_to_delete.each do |project|
project.self_and_descendants.update_all status: Project::STATUS_SCHEDULED_FOR_DELETION
end
perform_later(projects_to_delete.map(&:id), user.id, user.remote_ip)
end
def perform(project_ids, user_id, remote_ip)
user = User.active.find_by_id(user_id)
unless user&.admin?
info "[DestroyProjectsJob] --- User check failed: User #{user_id} triggering projects destroy does not exist anymore or isn't admin/active."
return
end
project_ids.each do |project_id|
DestroyProjectJob.perform_now(project_id, user_id, remote_ip)
end
end
private
def info(*msg)
Rails.logger.info(*msg)
end
end

View File

@@ -11,5 +11,11 @@
<li> <li>
<%= context_menu_link l(:button_delete), project_path(@project, back_url: @back), method: :delete, class: 'icon icon-del' %> <%= context_menu_link l(:button_delete), project_path(@project, back_url: @back), method: :delete, class: 'icon icon-del' %>
</li> </li>
<% else %>
<li>
<%= context_menu_link l(:button_delete),
{controller: 'projects', action: 'bulk_destroy', ids: @projects.map(&:id), back_url: @back},
method: :delete, data: {confirm: l(:text_projects_bulk_destroy_confirmation)}, class: 'icon icon-del' %>
</li>
<% end %> <% end %>
</ul> </ul>

View File

@@ -0,0 +1,27 @@
<%= title l(:label_confirmation) %>
<%= form_tag(bulk_destroy_projects_path(ids: @projects.map(&:id)), method: :delete) do %>
<div class="warning">
<p><%= simple_format l :text_projects_bulk_destroy_head %></p>
<% @projects.each do |project| %>
<p>Project: <strong><%= project.to_s %></strong>
<% if project.descendants.any? %>
<br />
<%= l :text_subprojects_bulk_destroy, project.descendants.map(&:to_s).join(', ') %>
<% end %>
</p>
<% end %>
<p><%= l :text_projects_bulk_destroy_confirm, yes: l(:general_text_Yes) %></p>
<p><%= text_field_tag 'confirm' %></p>
</div>
<p>
<%= submit_tag l(:button_delete), class: 'btn-alert btn-small' %>
<%= link_to l(:button_cancel), admin_projects_path %>
</p>
<% end %>

View File

@@ -1212,7 +1212,14 @@ en:
text_select_mail_notifications: Select actions for which email notifications should be sent. text_select_mail_notifications: Select actions for which email notifications should be sent.
text_regexp_info: eg. ^[A-Z0-9]+$ text_regexp_info: eg. ^[A-Z0-9]+$
text_project_destroy_confirmation: Are you sure you want to delete this project and related data? text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
text_projects_bulk_destroy_confirmation: Are you sure you want to delete the selected projects and related data?
text_projects_bulk_destroy_head: |
You are about to permanently delete the following projects, including possible subprojects and any related data.
Please review the information below and confirm that this is indeed what you want to do.
This action cannot be undone.
text_projects_bulk_destroy_confirm: To confirm, please enter "%{yes}" in the box below.
text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted." text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
text_subprojects_bulk_destroy: "including its subproject(s): %{value}"
text_project_close_confirmation: Are you sure you want to close the '%{value}' project to make it read-only? text_project_close_confirmation: Are you sure you want to close the '%{value}' project to make it read-only?
text_project_reopen_confirmation: Are you sure you want to reopen the '%{value}' project? text_project_reopen_confirmation: Are you sure you want to reopen the '%{value}' project?
text_project_archive_confirmation: Are you sure you want to archive the '%{value}' project? text_project_archive_confirmation: Are you sure you want to archive the '%{value}' project?

View File

@@ -128,6 +128,7 @@ Rails.application.routes.draw do
resources :projects do resources :projects do
collection do collection do
get 'autocomplete' get 'autocomplete'
delete 'bulk_destroy'
end end
member do member do

View File

@@ -1227,6 +1227,31 @@ class ProjectsControllerTest < Redmine::ControllerTest
assert Project.find(1) assert Project.find(1)
end end
def test_bulk_destroy_should_require_admin
@request.session[:user_id] = 2 # non-admin
delete :bulk_destroy, params: { ids: [1, 2], confirm: 'Yes' }
assert_response 403
end
def test_bulk_destroy_should_require_confirmation
@request.session[:user_id] = 1 # admin
assert_difference 'Project.count', 0 do
delete :bulk_destroy, params: { ids: [1, 2] }
end
assert Project.find(1)
assert Project.find(2)
assert_response 200
end
def test_bulk_destroy_should_delete_projects
@request.session[:user_id] = 1 # admin
assert_difference 'Project.count', -2 do
delete :bulk_destroy, params: { ids: [2, 6], confirm: 'Yes' }
end
assert_equal 0, Project.where(id: [2, 6]).count
assert_redirected_to '/admin/projects'
end
def test_archive def test_archive
@request.session[:user_id] = 1 # admin @request.session[:user_id] = 1 # admin
post(:archive, :params => {:id => 1}) post(:archive, :params => {:id => 1})

View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2022 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require File.expand_path('../../../test_helper', __FILE__)
class DestroyProjectsJobTest < ActiveJob::TestCase
fixtures :users, :projects, :email_addresses
setup do
@projects = Project.where(id: [1, 2]).to_a
@user = User.find_by_admin true
end
test "schedule should mark projects and children for deletion" do
DestroyProjectsJob.schedule @projects, user: @user
@projects.each do |project|
project.reload
assert_equal Project::STATUS_SCHEDULED_FOR_DELETION, project.status
project.descendants.each do |child|
assert_equal Project::STATUS_SCHEDULED_FOR_DELETION, child.status
end
end
end
test "schedule should enqueue job" do
assert_enqueued_with(
job: DestroyProjectsJob,
args: [[1, 2], @user.id, '127.0.0.1']
) do
@user.remote_ip = '127.0.0.1'
DestroyProjectsJob.schedule @projects, user: @user
end
end
test "should destroy projects and send emails" do
assert_difference 'Project.count', -6 do
DestroyProjectsJob.perform_now @projects.map(&:id), @user.id, '127.0.0.1'
end
assert_enqueued_with(
job: ActionMailer::MailDeliveryJob,
args: ->(job_args){
job_args[1] == 'security_notification' &&
job_args[3].to_s.include?("mail_destroy_project_with_subprojects_successful")
}
)
end
end