Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/controllers/maintenance_tasks/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class ApplicationController < MaintenanceTasks.parent_controller.constantize
policy.script_src_elem(
# <script> tag in app/views/layouts/maintenance_tasks/application.html.erb
"'sha256-NiHKryHWudRC2IteTqmY9v1VkaDUA/5jhgXkMTkgo2w='",
# <script> tag in app/views/maintenance_tasks/tasks/show.html.erb
"'sha256-oCsB8YG3WI4aqJRWK/T7XfMAd3GEq+jhwDCOkSokj68='",
# <script> tag for capybara-lockstep
*capybara_lockstep_scripts,
)
Expand Down
11 changes: 11 additions & 0 deletions app/controllers/maintenance_tasks/tasks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ def show
)
end

# Returns the estimated count of items for a Task as JSON.
def count
task_data = TaskDataShow.new(
params.fetch(:id),
arguments: params.except(:id, :controller, :action).permit!,
)
render(json: { count: task_data.count })
rescue StandardError
render(json: { count: nil })
end

private

def set_refresh
Expand Down
29 changes: 29 additions & 0 deletions app/models/maintenance_tasks/task_data_show.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "timeout"

module MaintenanceTasks
# Class that represents the data related to a Task. Such information can be
# sourced from a Task or from existing Run records for a Task that was since
Expand All @@ -11,6 +13,8 @@ module MaintenanceTasks
#
# @api private
class TaskDataShow
TIMEOUT = 15

# Initializes a Task Data with a name.
#
# @param name [String] the name of the Task subclass.
Expand Down Expand Up @@ -97,6 +101,11 @@ def csv_task?
!deleted? && Task.named(name).has_csv_content?
end

# @return [Boolean] whether the Task is collection-less.
def no_collection?
!deleted? && Task.named(name).no_collection?
end

# @return [Array<String>] the names of parameters the Task accepts.
def parameter_names
if deleted?
Expand All @@ -106,6 +115,26 @@ def parameter_names
end
end

# @return [Integer] the count of items to be processed.
# @return [nil] if the count is unavailable (e.g. CSV tasks where the
# collection depends on uploaded file content, tasks whose collection
# requires arguments, or when the query times out).
def count
return if deleted?
return if csv_task?

task_instance = new
return if task_instance.nil?

Timeout.timeout(TIMEOUT) do
result = task_instance.count
result = task_instance.collection.count if result == :no_count
result if result.is_a?(Integer)
end
rescue StandardError
nil
end

# @return [MaintenanceTasks::Task] an instance of the Task class.
# @return [nil] if the Task file was deleted.
def new
Expand Down
69 changes: 69 additions & 0 deletions app/views/maintenance_tasks/tasks/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
</div>
<% end %>
<%= render "maintenance_tasks/tasks/custom", form: form %>
<% unless @task.deleted? || @task.csv_task? || @task.no_collection? %>
<div id="task-count" data-url="<%= count_task_path(@task) %>" class="block">
<p class="has-text-grey">Estimating expected items count...</p>
</div>
<% end %>
<div class="block">
<%= form.submit 'Run', class: "button is-success is-rounded mb-4 has-text-white-ter", disabled: @task.deleted? %>
</div>
Expand Down Expand Up @@ -61,3 +66,67 @@
<%= link_to "Next page", task_path(@task, cursor: @task.runs_page.next_cursor) unless @task.runs_page.last? %>
<% end %>
<% end %>

<% unless @task.deleted? || @task.csv_task? || @task.no_collection? %>
<script>
(function() {
var countEl = document.getElementById('task-count');
if (!countEl) return;

var form = document.querySelector('form');
if (!form) return;

var url = countEl.dataset.url;
var abortCtrl;

function serializeParams() {
var params = new URLSearchParams();
new FormData(form).forEach(function(value, key) {
var m = key.match(/^task\[(.+)\]$/);
if (m) params.set(m[1], value);
});
return params.toString();
}

var lastParams = serializeParams();

function fetchCount() {
if (abortCtrl) abortCtrl.abort();
abortCtrl = new AbortController();

var p = countEl.querySelector('p');
p.textContent = 'Estimating expected items count...';
countEl.classList.remove('is-hidden');

var serialized = serializeParams();
lastParams = serialized;

fetch(url + '?' + serialized, { signal: abortCtrl.signal })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.count != null) {
p.textContent = '';
var strong = document.createElement('strong');
strong.textContent = data.count.toLocaleString();
p.appendChild(strong);
p.appendChild(document.createTextNode(
' ' + (data.count === 1 ? 'item' : 'items') + ' expected to be processed'
));
countEl.classList.remove('is-hidden');
} else {
countEl.classList.add('is-hidden');
}
})
.catch(function() {});
}

function fetchCountIfChanged() {
if (serializeParams() !== lastParams) fetchCount();
}

fetchCount();
form.addEventListener('focusout', fetchCountIfChanged);
form.addEventListener('change', fetchCount);
})();
</script>
<% end %>
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

MaintenanceTasks::Engine.routes.draw do
resources :tasks, only: [:index, :show], format: false do
member do
get :count
end
resources :runs, only: [:create], format: false do
member do
post "pause"
Expand Down
61 changes: 61 additions & 0 deletions test/models/maintenance_tasks/task_data_show_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ class TaskDataShowTest < ActiveSupport::TestCase
refute_predicate TaskDataShow.new("Maintenance::DoesNotExist"), :csv_task?
end

test "#no_collection? returns true for a no-collection Task" do
assert_predicate TaskDataShow.new("Maintenance::NoCollectionTask"), :no_collection?
end

test "#no_collection? returns false for a Task with a collection" do
refute_predicate TaskDataShow.new("Maintenance::UpdatePostsTask"), :no_collection?
end

test "#no_collection? returns false if the Task is deleted" do
refute_predicate TaskDataShow.new("Maintenance::DoesNotExist"), :no_collection?
end

test "#refresh? returns true if there are active runs" do
assert_predicate TaskDataShow.new("Maintenance::UpdatePostsTask"), :refresh?
end
Expand Down Expand Up @@ -153,5 +165,54 @@ class TaskDataShowTest < ActiveSupport::TestCase
task_data = TaskDataShow.prepare("Maintenance::ParamsTask", arguments: { unknown: nil })
assert_nothing_raised { task_data.new }
end

test "#count returns the count for a Task with a count override" do
task_data = TaskDataShow.new("Maintenance::UpdatePostsThrottledTask")
assert_equal Post.count, task_data.count
end

test "#count returns the count for a custom enumerating Task" do
task_data = TaskDataShow.new("Maintenance::CustomEnumeratingTask")
assert_equal 3, task_data.count
end

test "#count falls back to collection count for a Task without a count override" do
task_data = TaskDataShow.new("Maintenance::TestTask")
assert_equal 2, task_data.count
end

test "#count returns nil when collection count errors" do
task_data = TaskDataShow.new("Maintenance::ParamsTask")
assert_nil task_data.count
end

test "#count returns the count for a no-collection Task" do
task_data = TaskDataShow.new("Maintenance::NoCollectionTask")
assert_equal 1, task_data.count
end

test "#count returns nil for a CSV Task" do
task_data = TaskDataShow.new("Maintenance::ImportPostsTask")
assert_nil task_data.count
end

test "#count returns nil for a deleted Task" do
task_data = TaskDataShow.new("Maintenance::DoesNotExist")
assert_nil task_data.count
end

test "#count returns nil when collection count times out" do
task_data = TaskDataShow.new("Maintenance::UpdatePostsThrottledTask")
Task.named("Maintenance::UpdatePostsThrottledTask").any_instance.stubs(:count).raises(Timeout::Error)
assert_nil task_data.count
end

test "#count returns the count for a Task with arguments" do
task_data = TaskDataShow.new(
"Maintenance::ParamsTask",
arguments: { post_ids: Post.first.id.to_s },
)
assert_equal 1, task_data.count
end
end
end
26 changes: 26 additions & 0 deletions test/system/maintenance_tasks/tasks_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,32 @@ class TasksTest < ApplicationSystemTestCase
assert_button "Run", disabled: true
end

test "show a Task with count indicator" do
visit maintenance_tasks.task_path("Maintenance::UpdatePostsThrottledTask")
assert_text "items expected to be processed"
end

test "count updates dynamically when params change" do
visit maintenance_tasks.task_path("Maintenance::ParamsTask")
assert_selector "#task-count:not(.is-hidden)"

fill_in "task[post_ids]", with: Post.first.id.to_s
find_field("task[post_ids]").send_keys(:tab)
within("#task-count") do
assert_text "1 item expected to be processed"
end
end

test "show a Task without count indicator" do
visit maintenance_tasks.task_path("Maintenance::ImportPostsTask")
assert_no_text "expected to be processed"
end

test "show a no-collection Task without count indicator" do
visit maintenance_tasks.task_path("Maintenance::NoCollectionTask")
assert_no_selector "#task-count"
end

test "visit main page through iframe" do
visit root_path

Expand Down