diff --git a/admin/tool/componentlibrary/content/moodle/components/task-indicator.md b/admin/tool/componentlibrary/content/moodle/components/task-indicator.md index 4216f49c3ef..6afc45f80ba 100644 --- a/admin/tool/componentlibrary/content/moodle/components/task-indicator.md +++ b/admin/tool/componentlibrary/content/moodle/components/task-indicator.md @@ -6,7 +6,7 @@ date: 2024-08-21T00:00:00+01:00 draft: false tags: - MDL-81714 -- 4.5 +- 5.0 --- {{< mustache template="core/task_indicator" >}} @@ -89,5 +89,34 @@ if ($taskindicator->has_task_record()) { {{< /php >}} +When the task begins running and the progress is updated, the progress bar will automatically be displayed. + If the optional `redirecturl` parameter is set when creating the indicator, the page will automatically reload or redirect to this URL when the progress bar completes. + +While the task is still queued, admins will see a "Run now" button below the indicator. This is designed for convenience if +a user is blocked on a job and needs the task run immediately. It will run the specific instance of the task tracked by the +indicator. + +{{< mustache template="core/task_indicator" >}} +{ + "heading": "Regrade in progress", + "icon": { + "attributes": [ + {"name": "src", "value": "/pix/i/timer.svg"}, + {"name": "alt", "value": ""} + ] + }, + "message": "Grades are being recalculated due to recent changes.", + "progress": { + "id": "progressbar_test", + "message": "Task pending", + "idnumber": "progressbar_test", + "class": "stored-progress-bar stored-progress-notstarted", + "width": "500", + "value": "0" + }, + "runurl": "http://example.com/runtask.php?id=1", + "runlabel": "Run now" +} +{{< /mustache >}} diff --git a/grade/tests/behat/grade_async_regrade.feature b/grade/tests/behat/grade_async_regrade.feature index a7a0f6efd8f..effe1f13cb7 100644 --- a/grade/tests/behat/grade_async_regrade.feature +++ b/grade/tests/behat/grade_async_regrade.feature @@ -54,15 +54,14 @@ Feature: Asynchronous regrade on a large course And I set the field "Maximum grade" to "50" And I press "Save and return to course" And I log out + And I change the viewport size to "medium" Scenario Outline: Task indicator displays on all grade reports when a calculation is pending Given I am on the "Test course 2" "" page logged in as "" - Then I should not see "Grades are being recalculated due to recent changes." + Then I should not see "The report will update automatically. You don't need to do anything." And should exist When I am on the "Test course 1" "" page logged in as "" - Then I should see "Grades are being recalculated due to recent changes." - And I should see "Task pending" - And I should see "0.0%" + Then I should see "The report will update automatically. You don't need to do anything." And should not exist Examples: @@ -77,10 +76,10 @@ Feature: Asynchronous regrade on a large course Scenario Outline: Gradebook settings can be accessed when a regrade is pending Given I am on the "Test course 2" "" page logged in as "teacher1" Then I should see "" - And I should not see "Grades are being recalculated due to recent changes." + And I should not see "The report will update automatically. You don't need to do anything." Given I am on the "Test course 1" "" page logged in as "teacher1" Then I should see "" - And I should not see "Grades are being recalculated due to recent changes." + And I should not see "The report will update automatically. You don't need to do anything." Examples: | page | text | @@ -92,36 +91,32 @@ Feature: Asynchronous regrade on a large course When I follow "Grades" in the user menu And I follow "Test course 2" Then "table.user-grade" "css_element" should exist - Then I should not see "Grades are being recalculated due to recent changes." + Then I should not see "The report will update automatically. You don't need to do anything." When I follow "Grades" in the user menu And I follow "Test course 1" Then "table.user-grade" "css_element" should not exist - Then I should see "Grades are being recalculated due to recent changes." + Then I should see "The report will update automatically. You don't need to do anything." Scenario: Task indicator progresses and redirects when the task is run. When I am on the "Test course 1" "grades > Grader report > View" page logged in as teacher1 - And I should see "Grades are being recalculated due to recent changes." - And I should see "Task pending" - And I should see "0.0%" + And I should see "The report will update automatically. You don't need to do anything." + And I should not see "Run now" + And I should not see "0.0%" And "user-grades" "table" should not exist - When I run all adhoc tasks + And I run all adhoc tasks # Progress bar should update. - # Manual wait is a bit of a fudge, but we need the progress bar to poll for an update. - And I wait "1" seconds - Then I should not see "Task pending" - And I should see "Recalculating grades" + And I wait until "Recalculating grades" "text" exists And I should see "100%" - When I wait "2" seconds # The page should reload after a short delay. - Then I should not see "Grades are being recalculated due to recent changes." + Then I wait until "Recalculating grades" "text" does not exist And I set the field "Search users" to "Student 1" And "user-grades" "table" should exist And "40.00" "text" should exist in the "student1@example.com" "table_row" Scenario: Admin should see a "Run now" button in the task indicator When I am on the "Test course 1" "grades > Grader report > View" page logged in as admin - And I should see "Grades are being recalculated due to recent changes." - And I should see "Task pending" + And I should see "The report will update automatically. You don't need to do anything." + And I should not see "0.0%" And I should see "Run now" Scenario: Making changes on course with less than 100 grades performs the regrade synchronously, no indicator is shown. @@ -131,17 +126,17 @@ Feature: Asynchronous regrade on a large course And I set the field "Maximum grade" to "50" And I press "Save and return to course" When I am on the "Test course 2" "grades > Grader report > View" page - Then I should not see "Grades are being recalculated due to recent changes." + Then I should not see "The report will update automatically. You don't need to do anything." And "user-grades" "table" should exist Scenario: Editing weights triggers a regrade, but further edits are possible Given I run all adhoc tasks And I am on the "Test course 1" "grades > Grader report > View" page logged in as "teacher1" - And I should not see "Grades are being recalculated due to recent changes." + And I should not see "The report will update automatically. You don't need to do anything." And I am on the "Test course 1" "grades > Gradebook setup" page When I set the field "Override weight of Test assignment 1" to "1" And I press "Save changes" And I am on the "Test course 1" "grades > Grader report > View" page - And I should see "Grades are being recalculated due to recent changes." + And I should see "The report will update automatically. You don't need to do anything." And I am on the "Test course 1" "grades > Gradebook setup" page - And I should not see "Grades are being recalculated due to recent changes." + And I should not see "The report will update automatically. You don't need to do anything." diff --git a/lib/amd/build/task_indicator.min.js b/lib/amd/build/task_indicator.min.js index da44e913379..858acf1becb 100644 --- a/lib/amd/build/task_indicator.min.js +++ b/lib/amd/build/task_indicator.min.js @@ -9,6 +9,6 @@ define("core/task_indicator",["exports"],(function(_exports){Object.defineProper * @author Mark Johnson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class{static init(id,redirectUrl){document.getElementById(id).addEventListener("update",(event=>{100===event.detail.percent&&window.setTimeout((()=>window.location.assign(redirectUrl)),2e3)}))}},_exports.default})); +class{static init(id,redirectUrl){const bar=document.getElementById(id);bar.addEventListener("update",(event=>{var _event$detail;const percent=null==event||null===(_event$detail=event.detail)||void 0===_event$detail?void 0:_event$detail.percent;if(percent>0){bar.classList.remove("stored-progress-notstarted");const runlink=document.querySelector(".runlink[data-idnumber=".concat(id,"]"));runlink&&runlink.remove()}""!==redirectUrl&&100===percent&&window.setTimeout((()=>window.location.assign(redirectUrl)),2e3)}))}},_exports.default})); //# sourceMappingURL=task_indicator.min.js.map \ No newline at end of file diff --git a/lib/amd/build/task_indicator.min.js.map b/lib/amd/build/task_indicator.min.js.map index 40edf051f79..35df3313865 100644 --- a/lib/amd/build/task_indicator.min.js.map +++ b/lib/amd/build/task_indicator.min.js.map @@ -1 +1 @@ -{"version":3,"file":"task_indicator.min.js","sources":["../src/task_indicator.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Task indicator\n *\n * Watches the progress bar inside the task indicator for updates, and redirects when the progress is complete.\n *\n * @module core/task_indicator\n * @copyright 2024 Catalyst IT Europe Ltd\n * @author Mark Johnson \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n /**\n * Watch the progress bar for updates.\n *\n * When the progress bar is updated to 100%, wait a couple of seconds so the user gets to see it if they are watching,\n * then redirect to the specified URL.\n *\n * @param {String} id\n * @param {String} redirectUrl\n */\n static init(id, redirectUrl) {\n document.getElementById(id).addEventListener('update', (event) => {\n if (event.detail.percent === 100) {\n window.setTimeout(() => window.location.assign(redirectUrl), 2000);\n }\n });\n }\n}\n"],"names":["id","redirectUrl","document","getElementById","addEventListener","event","detail","percent","window","setTimeout","location","assign"],"mappings":";;;;;;;;;;;kBAmCgBA,GAAIC,aACZC,SAASC,eAAeH,IAAII,iBAAiB,UAAWC,QACvB,MAAzBA,MAAMC,OAAOC,SACbC,OAAOC,YAAW,IAAMD,OAAOE,SAASC,OAAOV,cAAc"} \ No newline at end of file +{"version":3,"file":"task_indicator.min.js","sources":["../src/task_indicator.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Task indicator\n *\n * Watches the progress bar inside the task indicator for updates, and redirects when the progress is complete.\n *\n * @module core/task_indicator\n * @copyright 2024 Catalyst IT Europe Ltd\n * @author Mark Johnson \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n /**\n * Watch the progress bar for updates.\n *\n * When the progress bar is updated to 100%, wait a couple of seconds so the user gets to see it if they are watching,\n * then redirect to the specified URL.\n *\n * @param {String} id The ID of the progress bar element.\n * @param {String} redirectUrl Optional URL to redirect to once the task is complete.\n */\n static init(id, redirectUrl) {\n const bar = document.getElementById(id);\n bar.addEventListener('update', (event) => {\n const percent = event?.detail?.percent;\n if (percent > 0) {\n // Once progress starts, display the progress bar and remove the run link.\n bar.classList.remove('stored-progress-notstarted');\n const runlink = document.querySelector(`.runlink[data-idnumber=${id}]`);\n if (runlink) {\n runlink.remove();\n }\n }\n // Once the progress bar completes, redirect the page.\n if (redirectUrl !== '' && percent === 100) {\n window.setTimeout(() => window.location.assign(redirectUrl), 2000);\n }\n });\n }\n}\n"],"names":["id","redirectUrl","bar","document","getElementById","addEventListener","event","percent","detail","_event$detail","classList","remove","runlink","querySelector","window","setTimeout","location","assign"],"mappings":";;;;;;;;;;;kBAmCgBA,GAAIC,mBACNC,IAAMC,SAASC,eAAeJ,IACpCE,IAAIG,iBAAiB,UAAWC,gCACtBC,QAAUD,MAAAA,6BAAAA,MAAOE,uCAAPC,cAAeF,WAC3BA,QAAU,EAAG,CAEbL,IAAIQ,UAAUC,OAAO,oCACfC,QAAUT,SAASU,+CAAwCb,SAC7DY,SACAA,QAAQD,SAII,KAAhBV,aAAkC,MAAZM,SACtBO,OAAOC,YAAW,IAAMD,OAAOE,SAASC,OAAOhB,cAAc"} \ No newline at end of file diff --git a/lib/amd/src/task_indicator.js b/lib/amd/src/task_indicator.js index ba963e18edc..79cb07aec8d 100644 --- a/lib/amd/src/task_indicator.js +++ b/lib/amd/src/task_indicator.js @@ -30,12 +30,23 @@ export default class { * When the progress bar is updated to 100%, wait a couple of seconds so the user gets to see it if they are watching, * then redirect to the specified URL. * - * @param {String} id - * @param {String} redirectUrl + * @param {String} id The ID of the progress bar element. + * @param {String} redirectUrl Optional URL to redirect to once the task is complete. */ static init(id, redirectUrl) { - document.getElementById(id).addEventListener('update', (event) => { - if (event.detail.percent === 100) { + const bar = document.getElementById(id); + bar.addEventListener('update', (event) => { + const percent = event?.detail?.percent; + if (percent > 0) { + // Once progress starts, display the progress bar and remove the run link. + bar.classList.remove('stored-progress-notstarted'); + const runlink = document.querySelector(`.runlink[data-idnumber=${id}]`); + if (runlink) { + runlink.remove(); + } + } + // Once the progress bar completes, redirect the page. + if (redirectUrl !== '' && percent === 100) { window.setTimeout(() => window.location.assign(redirectUrl), 2000); } }); diff --git a/lib/classes/output/task_indicator.php b/lib/classes/output/task_indicator.php index e5167a7879a..2bf4a1914bc 100644 --- a/lib/classes/output/task_indicator.php +++ b/lib/classes/output/task_indicator.php @@ -16,14 +16,21 @@ namespace core\output; +use core\plugin_manager; use core\task\adhoc_task; use core\task\stored_progress_task_trait; use core\url; +use core\context\system; use stdClass; /** * Indicator for displaying status and progress of a background task * + * This will display a section containing an icon, heading and message describing the background task being performed, + * as well as a progress bar that is updated as the task progresses. Optionally, it will redirect to a given URL (or reload + * the current one) when the task completes. If the task is still waiting in the queue, an admin viewing the indicator + * will also see a "Run now" button. + * * @package core * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} * @author Mark Johnson @@ -36,6 +43,12 @@ class task_indicator implements renderable, templatable { /** @var ?stored_progress_bar $progressbar */ protected ?stored_progress_bar $progressbar; + /** @var ?url $runurl The URL to manually run the task. */ + protected ?url $runurl = null; + + /** @var string $runlabel Label for the link to run the task. */ + protected string $runlabel = ''; + /** * Find the task record, and get the progress bar object. * @@ -80,6 +93,16 @@ class task_indicator implements renderable, templatable { $this->task->set_id($this->taskrecord->id); $idnumber = stored_progress_bar::convert_to_idnumber($this->task::class, $this->task->get_id()); $this->progressbar = stored_progress_bar::get_by_idnumber($idnumber); + // As long as the tool_task plugin hasn't been removed, + // allow admins to trigger the task manually if it's not running yet. + if ( + array_key_exists('task', plugin_manager::instance()->get_present_plugins('tool')) + && is_null($this->taskrecord->timestarted) + && has_capability('moodle/site:config', system::instance()) + ) { + $this->runurl = new url('/admin/tool/task/run_adhoctasks.php', ['id' => $this->taskrecord->id]); + $this->runlabel = get_string('runnow', 'tool_task'); + } } } @@ -103,8 +126,10 @@ class task_indicator implements renderable, templatable { $export['message'] = $this->message; $export['progress'] = $this->progressbar->export_for_template($output); $export['icon'] = $this->icon ? $this->icon->export_for_template($output) : ''; - $export['redirecturl'] = $this->redirecturl->out(); + $export['redirecturl'] = $this->redirecturl?->out(); $export['extraclasses'] = implode(' ', $this->extraclasses); + $export['runurl'] = $this->runurl?->out(); + $export['runlabel'] = $this->runlabel; $this->progressbar->init_js(); } return $export; diff --git a/lib/templates/task_indicator.mustache b/lib/templates/task_indicator.mustache index e6732e32c8c..c399300bffa 100644 --- a/lib/templates/task_indicator.mustache +++ b/lib/templates/task_indicator.mustache @@ -33,29 +33,36 @@ "id": "progressbar_test", "message": "Recalculating grades", "idnumber": "progressbar_test", + "class": "stored-progress-bar", "width": "500", "value": "50" } } }}
- {{#icon}} -
- {{>core/pix_icon}} -
- {{/icon}} -

{{heading}}

-

{{message}}

+
+ {{#icon}} +
+ {{>core/pix_icon}} +
+ {{/icon}} +

{{heading}}

+

{{message}}

+
+ {{#runurl}} +

+ {{runlabel}} +

+ {{/runurl}} {{#progress}} {{>core/progress_bar}} {{/progress}} + +
-{{#redirecturl}} - {{#js}} - require(['core/task_indicator'], function(TaskIndicator) { - TaskIndicator.init('{{progress.idnumber}}', '{{redirecturl}}'); - }); - {{/js}} -{{/redirecturl}} - +{{#js}} + require(['core/task_indicator'], function(TaskIndicator) { + TaskIndicator.init('{{progress.idnumber}}', '{{redirecturl}}'); + }); +{{/js}}