summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorMarius Balteanu <marius.balteanu@zitec.com>2022-05-17 20:45:41 +0000
committerMarius Balteanu <marius.balteanu@zitec.com>2022-05-17 20:45:41 +0000
commit883aa3b5cca1645f2aae353f4f180f77c5693c7e (patch)
tree97c45c8f806b96f5a41dd4853d89dffd27b9f3c8 /app
parent3719eb32f44681987df6bd55c9ca1888190daecb (diff)
downloadredmine-883aa3b5cca1645f2aae353f4f180f77c5693c7e.tar.gz
redmine-883aa3b5cca1645f2aae353f4f180f77c5693c7e.zip
Background job for project deletion (#36691).
Due to the deletion of dependent objects (issues etc), project deletion may take a long time. This patch moves the actual project deletion into an ActiveJob job. It also introduces a new project status (SCHEDULED_FOR_DELETION) that is used to effectively hide the project that is about to be deleted (and any potential descendant projects) from the system immediately. A security notification is sent out to the user that deleted the project, informing about success / failure. The projects list is extended to be able to filter for the new status, so in case of a failure, the project can still be accessed for examination. Patch by Jens Krämer. git-svn-id: https://svn.redmine.org/redmine/trunk@21591 e93f8b46-1217-0410-a6f0-8f06a7374b81
Diffstat (limited to 'app')
-rw-r--r--app/controllers/projects_controller.rb3
-rw-r--r--app/helpers/admin_helper.rb3
-rw-r--r--app/jobs/application_job.rb4
-rw-r--r--app/jobs/destroy_project_job.rb77
-rw-r--r--app/models/project.rb7
-rw-r--r--app/models/project_query.rb1
-rw-r--r--app/views/context_menus/projects.html.erb2
-rw-r--r--app/views/projects/_list.html.erb4
8 files changed, 95 insertions, 6 deletions
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index da7259da1..d8b2e8b9a 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -300,7 +300,8 @@ class ProjectsController < ApplicationController
@project_to_destroy = @project
if api_request? || params[:confirm] == @project_to_destroy.identifier
- @project_to_destroy.destroy
+ DestroyProjectJob.schedule(@project_to_destroy)
+ flash[:notice] = l(:notice_successful_delete)
respond_to do |format|
format.html do
redirect_to(
diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb
index 7b4ef9b1b..68cb31277 100644
--- a/app/helpers/admin_helper.rb
+++ b/app/helpers/admin_helper.rb
@@ -22,7 +22,8 @@ module AdminHelper
options_for_select([[l(:label_all), ''],
[l(:project_status_active), '1'],
[l(:project_status_closed), '5'],
- [l(:project_status_archived), '9']], selected.to_s)
+ [l(:project_status_archived), '9'],
+ [l(:project_status_scheduled_for_deletion), '10']], selected.to_s)
end
def plugin_data_for_updates(plugins)
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
new file mode 100644
index 000000000..d92ffddcb
--- /dev/null
+++ b/app/jobs/application_job.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class ApplicationJob < ActiveJob::Base
+end
diff --git a/app/jobs/destroy_project_job.rb b/app/jobs/destroy_project_job.rb
new file mode 100644
index 000000000..372e77240
--- /dev/null
+++ b/app/jobs/destroy_project_job.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+class DestroyProjectJob < ApplicationJob
+ include Redmine::I18n
+
+ def self.schedule(project, user: User.current)
+ # make the project (and any children) disappear immediately
+ project.self_and_descendants.update_all status: Project::STATUS_SCHEDULED_FOR_DELETION
+ perform_later project.id, user.id, user.remote_ip
+ end
+
+ def perform(project_id, user_id, remote_ip)
+ user_current_was = User.current
+
+ unless @user = User.active.find_by_id(user_id)
+ info "User check failed: User #{user_id} triggering project destroy does not exist anymore or isn't active."
+ return
+ end
+ @user.remote_ip = remote_ip
+ User.current = @user
+ set_language_if_valid @user.language || Setting.default_language
+
+ unless @project = Project.find_by_id(project_id)
+ info "Project check failed: Project has already been deleted."
+ return
+ end
+
+ unless @project.deletable?
+ info "Project check failed: User #{user_id} lacks permissions."
+ return
+ end
+
+ message = if @project.descendants.any?
+ :mail_destroy_project_with_subprojects_successful
+ else
+ :mail_destroy_project_successful
+ end
+ delete_project ? success(message) : failure
+ ensure
+ User.current = user_current_was
+ info "End destroy project"
+ end
+
+ private
+
+ def delete_project
+ info "Starting with project deletion"
+ return !!@project.destroy
+ rescue
+ info "Error while deleting project: #{$!}"
+ false
+ end
+
+ def success(message)
+ Mailer.deliver_security_notification(
+ @user, @user,
+ message: message,
+ value: @project.name,
+ url: {controller: 'admin', action: 'projects'},
+ title: :label_project_plural
+ )
+ end
+
+ def failure
+ Mailer.deliver_security_notification(
+ @user, @user,
+ message: :mail_destroy_project_failed,
+ value: @project.name,
+ url: {controller: 'admin', action: 'projects'},
+ title: :label_project_plural
+ )
+ end
+
+ def info(msg)
+ Rails.logger.info("[DestroyProjectJob] --- #{msg}")
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 7ba9b4197..81ec37d10 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -25,6 +25,7 @@ class Project < ActiveRecord::Base
STATUS_ACTIVE = 1
STATUS_CLOSED = 5
STATUS_ARCHIVED = 9
+ STATUS_SCHEDULED_FOR_DELETION = 10
# Maximum length for project identifiers
IDENTIFIER_MAX_LENGTH = 100
@@ -182,7 +183,7 @@ class Project < ActiveRecord::Base
perm = Redmine::AccessControl.permission(permission)
base_statement =
if perm && perm.read?
- "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}"
+ "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Project.table_name}.status <> #{Project::STATUS_SCHEDULED_FOR_DELETION}"
else
"#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
end
@@ -399,6 +400,10 @@ class Project < ActiveRecord::Base
self.status == STATUS_ARCHIVED
end
+ def scheduled_for_deletion?
+ self.status == STATUS_SCHEDULED_FOR_DELETION
+ end
+
# Archives the project and its descendants
def archive
# Check that there is no issue of a non descendant project that is assigned
diff --git a/app/models/project_query.rb b/app/models/project_query.rb
index 35f05fbae..852beb95c 100644
--- a/app/models/project_query.rb
+++ b/app/models/project_query.rb
@@ -111,6 +111,7 @@ class ProjectQuery < Query
values = super
if self.admin_projects
values << [l(:project_status_archived), Project::STATUS_ARCHIVED.to_s]
+ values << [l(:project_status_scheduled_for_deletion), Project::STATUS_SCHEDULED_FOR_DELETION.to_s]
end
values
end
diff --git a/app/views/context_menus/projects.html.erb b/app/views/context_menus/projects.html.erb
index c5db9084a..444d84c58 100644
--- a/app/views/context_menus/projects.html.erb
+++ b/app/views/context_menus/projects.html.erb
@@ -1,5 +1,5 @@
<ul>
- <% if @project %>
+ <% if @project && !@project.scheduled_for_deletion? %>
<% if @project.archived? %>
<li><%= context_menu_link l(:button_unarchive), unarchive_project_path(@project), method: :post, class: 'icon icon-unlock' %></li>
<% else %>
diff --git a/app/views/projects/_list.html.erb b/app/views/projects/_list.html.erb
index 39978ee64..4c592247c 100644
--- a/app/views/projects/_list.html.erb
+++ b/app/views/projects/_list.html.erb
@@ -40,13 +40,13 @@
</tr>
<% end %>
<tr id="project-<%= entry.id %>" class="<%= cycle('odd', 'even') %> hascontextmenu <%= entry.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
- <% if @admin_list %>
+ <% if @admin_list && !entry.scheduled_for_deletion? %>
<td class="checkbox hide-when-print"><%= check_box_tag("ids[]", entry.id, false, :id => nil) %></td>
<% end %>
<% @query.inline_columns.each do |column| %>
<%= content_tag('td', column_content(column, entry), :class => column.css_classes) %>
<% end %>
- <% if @admin_list %>
+ <% if @admin_list && !entry.scheduled_for_deletion? %>
<td class="buttons"><%= link_to_context_menu %></td>
<% end %>
</tr>