Project

General

Profile

Feature #33422 » 0004-projects-bulk-delete.patch

Jens Krämer, 2022-03-31 13:00

View differences:

app/controllers/projects_controller.rb
23 23
  menu_item :projects, :only => [:index, :new, :copy, :create]
24 24

  
25 25
  before_action :find_project,
26
                :except => [:index, :autocomplete, :list, :new, :create, :copy]
26
                :except => [:index, :autocomplete, :list, :new, :create, :copy, :bulk_destroy]
27 27
  before_action :authorize,
28 28
                :except => [:index, :autocomplete, :list, :new, :create, :copy,
29 29
                            :archive, :unarchive,
30
                            :destroy]
30
                            :destroy, :bulk_destroy]
31 31
  before_action :authorize_global, :only => [:new, :create]
32
  before_action :require_admin, :only => [:copy, :archive, :unarchive]
32
  before_action :require_admin, :only => [:copy, :archive, :unarchive, :bulk_destroy]
33 33
  accept_atom_auth :index
34 34
  accept_api_auth :index, :show, :create, :update, :destroy, :archive, :unarchive, :close, :reopen
35
  require_sudo_mode :destroy
35
  require_sudo_mode :destroy, :bulk_destroy
36 36

  
37 37
  helper :custom_fields
38 38
  helper :issues
......
315 315
    @project = nil
316 316
  end
317 317

  
318
  # Delete selected projects
319
  def bulk_destroy
320
    @projects = Project.where(id: params[:ids]).
321
      where.not(status: Project::STATUS_SCHEDULED_FOR_DELETION).to_a
322

  
323
    if @projects.empty?
324
      render_404
325
      return
326
    end
327

  
328
    if params[:confirm] == I18n.t(:general_text_Yes)
329
      DestroyProjectsJob.schedule @projects
330
      flash[:notice] = l(:notice_successful_delete)
331
      redirect_to admin_projects_path
332
    end
333
  end
334

  
318 335
  private
319 336

  
320 337
  # Returns the ProjectEntry scope for index
app/jobs/destroy_projects_job.rb
1
# frozen_string_literal: true
2

  
3
class DestroyProjectsJob < ApplicationJob
4
  include Redmine::I18n
5

  
6
  def self.schedule(projects_to_delete, user: User.current)
7
    # make the projects disappear immediately
8
    projects_to_delete.each do |project|
9
      project.self_and_descendants.update_all status: Project::STATUS_SCHEDULED_FOR_DELETION
10
    end
11
    perform_later(projects_to_delete.map(&:id), user.id, user.remote_ip)
12
  end
13

  
14
  def perform(project_ids, user_id, remote_ip)
15
    user = User.active.find_by_id(user_id)
16
    unless user&.admin?
17
      info "[DestroyProjectsJob] --- User check failed: User #{user_id} triggering projects destroy does not exist anymore or isn't admin/active."
18
      return
19
    end
20

  
21
    project_ids.each do |project_id|
22
      DestroyProjectJob.perform_now(project_id, user_id, remote_ip)
23
    end
24
  end
25

  
26
  private
27

  
28
  def info(*msg)
29
    Rails.logger.info(*msg)
30
  end
31
end
app/views/context_menus/projects.html.erb
12 12
      <%= context_menu_link l(:button_delete), project_path(@project, back_url: @back),
13 13
        method: :delete, data: {confirm: l(:text_project_destroy_confirmation)}, class: 'icon icon-del' %>
14 14
    </li>
15
  <% else %>
16
    <li>
17
      <%= context_menu_link l(:button_delete),
18
        {controller: 'projects', action: 'bulk_destroy', ids: @projects.map(&:id), back_url: @back},
19
        method: :delete, data: {confirm: l(:text_projects_bulk_destroy_confirmation)}, class: 'icon icon-del' %>
20
    </li>
15 21
  <% end %>
16 22
</ul>
app/views/projects/bulk_destroy.html.erb
1
<%= title l(:label_confirmation) %>
2

  
3
<%= form_tag(bulk_destroy_projects_path(ids: @projects.map(&:id)), method: :delete) do %>
4
<div class="warning">
5

  
6
<p><%= simple_format l :text_projects_bulk_destroy_head %></p>
7

  
8
<% @projects.each do |project| %>
9
  <p>Project: <strong><%= project.to_s %></strong>
10
    <% if project.descendants.any? %>
11
      <br />
12
      <%= l :text_subprojects_bulk_destroy, project.descendants.map(&:to_s).join(', ') %>
13
    <% end %>
14
  </p>
15
<% end %>
16

  
17
<p><%= l :text_projects_bulk_destroy_confirm, yes: l(:general_text_Yes) %></p>
18
<p><%= text_field_tag 'confirm' %></p>
19

  
20
</div>
21

  
22
<p>
23
  <%= submit_tag l(:button_delete), class: 'btn-alert btn-small' %>
24
  <%= link_to l(:button_cancel), admin_projects_path %>
25
</p>
26
<% end %>
27

  
config/locales/en.yml
1212 1212
  text_select_mail_notifications: Select actions for which email notifications should be sent.
1213 1213
  text_regexp_info: eg. ^[A-Z0-9]+$
1214 1214
  text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
1215
  text_projects_bulk_destroy_confirmation: Are you sure you want to delete the selected projects and related data?
1216
  text_projects_bulk_destroy_head: |
1217
    You are about to permanently delete the following projects, including possible subprojects and any related data.
1218
    Please review the information below and confirm that this is indeed what you want to do.
1219
    This action cannot be undone.
1220
  text_projects_bulk_destroy_confirm: To confirm, please enter "%{yes}" in the box below.
1215 1221
  text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
1222
  text_subprojects_bulk_destroy: "including its subproject(s): %{value}"
1216 1223
  text_workflow_edit: Select a role and a tracker to edit the workflow
1217 1224
  text_are_you_sure: Are you sure?
1218 1225
  text_journal_changed: "%{label} changed from %{old} to %{new}"
config/routes.rb
128 128
  resources :projects do
129 129
    collection do
130 130
      get 'autocomplete'
131
      delete 'bulk_destroy'
131 132
    end
132 133

  
133 134
    member do
test/functional/projects_controller_test.rb
1227 1227
    assert Project.find(1)
1228 1228
  end
1229 1229

  
1230
  def test_bulk_destroy_should_require_admin
1231
    @request.session[:user_id] = 2 # non-admin
1232
    delete :bulk_destroy, params: { ids: [1, 2], confirm: 'Yes' }
1233
    assert_response 403
1234
  end
1235

  
1236
  def test_bulk_destroy_should_require_confirmation
1237
    @request.session[:user_id] = 1 # admin
1238
    assert_difference 'Project.count', 0 do
1239
      delete :bulk_destroy, params: { ids: [1, 2] }
1240
    end
1241
    assert Project.find(1)
1242
    assert Project.find(2)
1243
    assert_response 200
1244
  end
1245

  
1246
  def test_bulk_destroy_should_delete_projects
1247
    @request.session[:user_id] = 1 # admin
1248
    assert_difference 'Project.count', -2 do
1249
      delete :bulk_destroy, params: { ids: [2, 6], confirm: 'Yes' }
1250
    end
1251
    assert_equal 0, Project.where(id: [2, 6]).count
1252
    assert_redirected_to '/admin/projects'
1253
  end
1254

  
1230 1255
  def test_archive
1231 1256
    @request.session[:user_id] = 1 # admin
1232 1257
    post(:archive, :params => {:id => 1})
test/unit/jobs/destroy_projects_job_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-2022  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require File.expand_path('../../../test_helper', __FILE__)
21

  
22
class DestroyProjectsJobTest < ActiveJob::TestCase
23
  fixtures :users, :projects, :email_addresses
24

  
25
  setup do
26
    @projects = Project.where(id: [1, 2]).to_a
27
    @user = User.find_by_admin true
28
  end
29

  
30
  test "schedule should mark projects and children for deletion" do
31
    DestroyProjectsJob.schedule @projects, user: @user
32
    @projects.each do |project|
33
      project.reload
34
      assert_equal Project::STATUS_SCHEDULED_FOR_DELETION, project.status
35
      project.descendants.each do |child|
36
        assert_equal Project::STATUS_SCHEDULED_FOR_DELETION, child.status
37
      end
38
    end
39
  end
40

  
41
  test "schedule should enqueue job" do
42
    assert_enqueued_with(
43
      job: DestroyProjectsJob,
44
      args: [[1, 2], @user.id, '127.0.0.1']
45
    ) do
46
      @user.remote_ip = '127.0.0.1'
47
      DestroyProjectsJob.schedule @projects, user: @user
48
    end
49
  end
50

  
51
  test "should destroy projects and send emails" do
52
    assert_difference 'Project.count', -6 do
53
      DestroyProjectsJob.perform_now @projects.map(&:id), @user.id, '127.0.0.1'
54
    end
55
    assert_enqueued_with(
56
      job: ActionMailer::MailDeliveryJob,
57
      args: ->(job_args){
58
        job_args[1] == 'security_notification' &&
59
        job_args[3].to_s.include?("mail_destroy_project_with_subprojects_successful")
60
      }
61
    )
62
  end
63
end
(6-6/6)