mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 05:58:34 +01:00
Merge branch 'MDL-70854-main' of https://github.com/junpataleta/moodle
This commit is contained in:
commit
bc5a65e444
5
.upgradenotes/MDL-70854-2024071306574741.yml
Normal file
5
.upgradenotes/MDL-70854-2024071306574741.yml
Normal file
@ -0,0 +1,5 @@
|
||||
issueNumber: MDL-70854
|
||||
notes:
|
||||
core:
|
||||
- message: Added stored progress bars
|
||||
type: improved
|
@ -49,6 +49,7 @@ class running_tasks_table extends \table_sql {
|
||||
'classname' => get_string('classname', 'tool_task'),
|
||||
'type' => get_string('tasktype', 'admin'),
|
||||
'time' => get_string('taskage', 'tool_task'),
|
||||
'progress' => get_string('progress', 'core'),
|
||||
'timestarted' => get_string('started', 'tool_task'),
|
||||
'hostname' => get_string('hostname', 'tool_task'),
|
||||
'pid' => get_string('pid', 'tool_task'),
|
||||
@ -153,4 +154,27 @@ class running_tasks_table extends \table_sql {
|
||||
public function col_timestarted($row): string {
|
||||
return userdate($row->timestarted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the progress column.
|
||||
*
|
||||
* @param \stdClass $row
|
||||
* @return string
|
||||
*/
|
||||
public function col_progress($row): string {
|
||||
// Check to see if there is a stored progress record for this task.
|
||||
if ($row->type === 'adhoc') {
|
||||
$idnumber = \core\output\stored_progress_bar::convert_to_idnumber($row->classname, $row->id);
|
||||
} else {
|
||||
$idnumber = \core\output\stored_progress_bar::convert_to_idnumber($row->classname);
|
||||
}
|
||||
|
||||
$bar = \core\output\stored_progress_bar::get_by_idnumber($idnumber);
|
||||
if ($bar) {
|
||||
return $bar->get_content();
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -38,3 +38,24 @@ Feature: See running scheduled tasks
|
||||
And I should see "2 days" in the "core\task\asynchronous_restore_task" "table_row"
|
||||
And I should see "c69335460f7f" in the "core\task\asynchronous_restore_task" "table_row"
|
||||
And I should see "1916" in the "core\task\asynchronous_restore_task" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: If a task with a stored progress bar is running, I should be able to observe the progress.
|
||||
Given the following config values are set as admin:
|
||||
| progresspollinterval | 1 |
|
||||
And the following "tool_task > scheduled tasks" exist:
|
||||
| classname | seconds | hostname | pid |
|
||||
| \core\task\delete_unconfirmed_users_task | 120 | c69335460f7f | 1917 |
|
||||
And the following "stored progress bars" exist:
|
||||
| idnumber | percent |
|
||||
| core_task_delete_unconfirmed_users_task | 50.00 |
|
||||
And I navigate to "Server > Tasks > Tasks running now" in site administration
|
||||
And I should see "2 mins" in the "Delete unconfirmed users" "table_row"
|
||||
And I should see "c69335460f7f" in the "Delete unconfirmed users" "table_row"
|
||||
And I should see "1917" in the "Delete unconfirmed users" "table_row"
|
||||
And I should see "50.0%" in the "Delete unconfirmed users" "table_row"
|
||||
When I set the stored progress bar "core_task_delete_unconfirmed_users_task" to "75.00"
|
||||
# Wait for the progress polling.
|
||||
And I wait "1" seconds
|
||||
Then I should not see "50.0%" in the "Delete unconfirmed users" "table_row"
|
||||
And I should see "75.0%" in the "Delete unconfirmed users" "table_row"
|
||||
|
@ -773,6 +773,15 @@ $CFG->admin = 'admin';
|
||||
// Defaults to 60 minutes.
|
||||
//
|
||||
// $CFG->enrolments_sync_interval = 3600
|
||||
//
|
||||
// Stored progress polling interval
|
||||
//
|
||||
// Stored progress bars which can be polled for updates via AJAX can be controlled by the
|
||||
// `progresspollinterval` config setting, to determine the interval (in seconds) at which the
|
||||
// polling should be done and latest update retrieved.
|
||||
// If no value is set, then it will default to 5 seconds.
|
||||
//
|
||||
// $CFG->progresspollinterval = 5;
|
||||
|
||||
//=========================================================================
|
||||
// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
|
||||
|
@ -1342,6 +1342,7 @@ $string['stickyblockscourseview'] = 'Course page';
|
||||
$string['stickyblocksduplicatenotice'] = 'If any block you add here is already present in a particular page, it will result in a duplicate.<br />Only the pinned block will be non-editable, the duplicate will still be editable.';
|
||||
$string['stickyblocksmymoodle'] = 'My Moodle';
|
||||
$string['stickyblockspagetype'] = 'Page type to configure';
|
||||
$string['storedprogressbarcleanuptask'] = 'Stored progress bar cleanup task';
|
||||
$string['strictformsrequired'] = 'Strict validation of required fields';
|
||||
$string['stripalltitletags'] = 'Remove HTML tags from all activity names';
|
||||
$string['supportandservices'] = 'Support and services';
|
||||
|
11
lib/amd/build/stored_progress.min.js
vendored
Normal file
11
lib/amd/build/stored_progress.min.js
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
define("core/stored_progress",["exports","core/ajax","core/notification"],(function(_exports,Ajax,_notification){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,Ajax=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}
|
||||
/**
|
||||
* Script to update stored_progress progress bars on the screen.
|
||||
*
|
||||
* @module core/stored_progress
|
||||
* @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @author Conn Warwicker <conn.warwicker@catalyst-eu.net>
|
||||
*/(Ajax),_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};var STORED_PROGRESS_LOADED=!1;function poll(ids,timeout){let promise=Ajax.call([{methodname:"core_output_poll_stored_progress",args:{ids:ids}}]),repollids=[];promise[0].then((function(results){return results.forEach((function(data){updateProgressBar(data.uniqueid,data.progress,data.message,data.estimated,data.error),data.progress<100&&!data.error&&repollids.push(data.id),data.timeout&&data.timeout>0&&(timeout=data.timeout)})),repollids.length>0&&setTimeout((()=>poll(repollids,timeout)),1e3*timeout)})).catch(_notification.default.exception)}_exports.init=timeout=>{if(!1===STORED_PROGRESS_LOADED){let ids=[];document.querySelectorAll(".stored-progress-bar").forEach((el=>{let id=el.dataset.recordid;ids.push(id)})),poll(ids,timeout),STORED_PROGRESS_LOADED=!0}}}));
|
||||
|
||||
//# sourceMappingURL=stored_progress.min.js.map
|
1
lib/amd/build/stored_progress.min.js.map
Normal file
1
lib/amd/build/stored_progress.min.js.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"stored_progress.min.js","sources":["../src/stored_progress.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Script to update stored_progress progress bars on the screen.\n *\n * @module core/stored_progress\n * @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/}\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @author Conn Warwicker <conn.warwicker@catalyst-eu.net>\n */\n\n/* global updateProgressBar */\n\nimport * as Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n/**\n * @var bool This AMD script is loaded multiple times, for each progress bar on a page.\n * So this stops it running multiple times.\n * */\nvar STORED_PROGRESS_LOADED = false;\n\n/**\n * Poll a given stored progress record.\n *\n * @param {array} ids\n * @param {integer} timeout\n */\nfunction poll(ids, timeout) {\n\n // Call AJAX request.\n let promise = Ajax.call([{\n methodname: 'core_output_poll_stored_progress', args: {'ids': ids}\n }]);\n\n let repollids = [];\n\n // When AJAX request returns, handle the results.\n promise[0].then(function(results) {\n\n results.forEach(function(data) {\n\n // Update the progress bar percentage and message using the core method from the javascript-static.js.\n updateProgressBar(data.uniqueid, data.progress, data.message, data.estimated, data.error);\n\n // Add the bar for re-polling if it's not completed.\n if (data.progress < 100 && !data.error) {\n repollids.push(data.id);\n }\n\n // If a different timeout came back from the script, use that instead.\n if (data.timeout && data.timeout > 0) {\n timeout = data.timeout;\n }\n\n });\n\n // If we still want to poll any of them, do it again.\n if (repollids.length > 0) {\n return setTimeout(() => poll(repollids, timeout), timeout * 1000);\n }\n\n return false;\n\n }).catch(Notification.exception);\n\n}\n\n/**\n * Initialise the polling process.\n *\n * @param {integer} timeout Timeout to use (seconds).\n */\nexport const init = (timeout) => {\n\n if (STORED_PROGRESS_LOADED === false) {\n\n let ids = [];\n\n // Find any stored progress bars we want to poll.\n document.querySelectorAll('.stored-progress-bar').forEach(el => {\n\n // Get its id and add to array.\n let id = el.dataset.recordid;\n ids.push(id);\n\n });\n\n // Poll for updates from these IDs.\n poll(ids, timeout);\n\n // Script has run, we don't want it to run again.\n STORED_PROGRESS_LOADED = true;\n\n }\n\n};"],"names":["STORED_PROGRESS_LOADED","poll","ids","timeout","promise","Ajax","call","methodname","args","repollids","then","results","forEach","data","updateProgressBar","uniqueid","progress","message","estimated","error","push","id","length","setTimeout","catch","Notification","exception","document","querySelectorAll","el","dataset","recordid"],"mappings":";;;;;;;;oFAiCIA,wBAAyB,WAQpBC,KAAKC,IAAKC,aAGXC,QAAUC,KAAKC,KAAK,CAAC,CACrBC,WAAY,mCAAoCC,KAAM,KAAQN,QAG9DO,UAAY,GAGhBL,QAAQ,GAAGM,MAAK,SAASC,gBAErBA,QAAQC,SAAQ,SAASC,MAGrBC,kBAAkBD,KAAKE,SAAUF,KAAKG,SAAUH,KAAKI,QAASJ,KAAKK,UAAWL,KAAKM,OAG/EN,KAAKG,SAAW,MAAQH,KAAKM,OAC7BV,UAAUW,KAAKP,KAAKQ,IAIpBR,KAAKV,SAAWU,KAAKV,QAAU,IAC/BA,QAAUU,KAAKV,YAMnBM,UAAUa,OAAS,GACZC,YAAW,IAAMtB,KAAKQ,UAAWN,UAAoB,IAAVA,YAKvDqB,MAAMC,sBAAaC,yBASLvB,cAEc,IAA3BH,uBAAkC,KAE9BE,IAAM,GAGVyB,SAASC,iBAAiB,wBAAwBhB,SAAQiB,SAGlDR,GAAKQ,GAAGC,QAAQC,SACpB7B,IAAIkB,KAAKC,OAKbpB,KAAKC,IAAKC,SAGVH,wBAAyB"}
|
110
lib/amd/src/stored_progress.js
Normal file
110
lib/amd/src/stored_progress.js
Normal file
@ -0,0 +1,110 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Script to update stored_progress progress bars on the screen.
|
||||
*
|
||||
* @module core/stored_progress
|
||||
* @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @author Conn Warwicker <conn.warwicker@catalyst-eu.net>
|
||||
*/
|
||||
|
||||
/* global updateProgressBar */
|
||||
|
||||
import * as Ajax from 'core/ajax';
|
||||
import Notification from 'core/notification';
|
||||
|
||||
/**
|
||||
* @var bool This AMD script is loaded multiple times, for each progress bar on a page.
|
||||
* So this stops it running multiple times.
|
||||
* */
|
||||
var STORED_PROGRESS_LOADED = false;
|
||||
|
||||
/**
|
||||
* Poll a given stored progress record.
|
||||
*
|
||||
* @param {array} ids
|
||||
* @param {integer} timeout
|
||||
*/
|
||||
function poll(ids, timeout) {
|
||||
|
||||
// Call AJAX request.
|
||||
let promise = Ajax.call([{
|
||||
methodname: 'core_output_poll_stored_progress', args: {'ids': ids}
|
||||
}]);
|
||||
|
||||
let repollids = [];
|
||||
|
||||
// When AJAX request returns, handle the results.
|
||||
promise[0].then(function(results) {
|
||||
|
||||
results.forEach(function(data) {
|
||||
|
||||
// Update the progress bar percentage and message using the core method from the javascript-static.js.
|
||||
updateProgressBar(data.uniqueid, data.progress, data.message, data.estimated, data.error);
|
||||
|
||||
// Add the bar for re-polling if it's not completed.
|
||||
if (data.progress < 100 && !data.error) {
|
||||
repollids.push(data.id);
|
||||
}
|
||||
|
||||
// If a different timeout came back from the script, use that instead.
|
||||
if (data.timeout && data.timeout > 0) {
|
||||
timeout = data.timeout;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// If we still want to poll any of them, do it again.
|
||||
if (repollids.length > 0) {
|
||||
return setTimeout(() => poll(repollids, timeout), timeout * 1000);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}).catch(Notification.exception);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the polling process.
|
||||
*
|
||||
* @param {integer} timeout Timeout to use (seconds).
|
||||
*/
|
||||
export const init = (timeout) => {
|
||||
|
||||
if (STORED_PROGRESS_LOADED === false) {
|
||||
|
||||
let ids = [];
|
||||
|
||||
// Find any stored progress bars we want to poll.
|
||||
document.querySelectorAll('.stored-progress-bar').forEach(el => {
|
||||
|
||||
// Get its id and add to array.
|
||||
let id = el.dataset.recordid;
|
||||
ids.push(id);
|
||||
|
||||
});
|
||||
|
||||
// Poll for updates from these IDs.
|
||||
poll(ids, timeout);
|
||||
|
||||
// Script has run, we don't want it to run again.
|
||||
STORED_PROGRESS_LOADED = true;
|
||||
|
||||
}
|
||||
|
||||
};
|
@ -316,6 +316,11 @@ class behat_core_generator extends behat_generator_base {
|
||||
'required' => ['subject', 'userfrom', 'userto'],
|
||||
'switchids' => ['userfrom' => 'userfromid', 'userto' => 'usertoid'],
|
||||
],
|
||||
'stored progress bars' => [
|
||||
'singular' => 'stored progress bar',
|
||||
'datagenerator' => 'stored_progress_bar',
|
||||
'required' => ['idnumber'],
|
||||
],
|
||||
];
|
||||
|
||||
return $entities;
|
||||
|
112
lib/classes/external/output/poll_stored_progress.php
vendored
Normal file
112
lib/classes/external/output/poll_stored_progress.php
vendored
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\external\output;
|
||||
|
||||
use core\output\stored_progress_bar;
|
||||
use core_external\external_api;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_multiple_structure;
|
||||
use core_external\external_single_structure;
|
||||
use core_external\external_value;
|
||||
|
||||
/**
|
||||
* Poll Stored Progress webservice.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @author Conn Warwicker <conn.warwicker@catalyst-eu.net>
|
||||
*/
|
||||
class poll_stored_progress extends external_api {
|
||||
|
||||
/**
|
||||
* Returns description of method parameters
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters([
|
||||
'ids' => new external_multiple_structure(
|
||||
new external_value(PARAM_INT, 'The stored_progress ID', VALUE_REQUIRED)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description of method return data
|
||||
*
|
||||
* @return external_multiple_structure
|
||||
*/
|
||||
public static function execute_returns(): external_multiple_structure {
|
||||
return new external_multiple_structure(
|
||||
new external_single_structure([
|
||||
'id' => new external_value(PARAM_INT, 'stored_progress record id'),
|
||||
'uniqueid' => new external_value(PARAM_TEXT, 'unique element id'),
|
||||
'progress' => new external_value(PARAM_FLOAT, 'percentage progress'),
|
||||
'estimated' => new external_value(PARAM_RAW, 'estimated time left string'),
|
||||
'message' => new external_value(PARAM_TEXT, 'message to be displayed with the bar'),
|
||||
'error' => new external_value(PARAM_TEXT, 'error', VALUE_OPTIONAL),
|
||||
'timeout' => new external_value(PARAM_TEXT, 'timeout to use in the polling', VALUE_OPTIONAL),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the database for the progress of stored progress objects
|
||||
*
|
||||
* @param array $ids
|
||||
* @return array
|
||||
*/
|
||||
public static function execute(array $ids): array {
|
||||
$params = self::validate_parameters(self::execute_parameters(), [
|
||||
'ids' => $ids,
|
||||
]);
|
||||
|
||||
$return = [];
|
||||
$ids = $params['ids'];
|
||||
foreach ($ids as $id) {
|
||||
// Load the stored progress bar object.
|
||||
$bar = stored_progress_bar::get_by_id($id);
|
||||
if ($bar) {
|
||||
// Return the updated bar data.
|
||||
$return[$id] = [
|
||||
'id' => $id,
|
||||
'uniqueid' => $bar->get_id(),
|
||||
'progress' => $bar->get_percent(),
|
||||
'estimated' => $bar->get_estimate_message($bar->get_percent()),
|
||||
'message' => $bar->get_message(),
|
||||
'timeout' => stored_progress_bar::get_timeout(),
|
||||
'error' => $bar->get_haserrored(),
|
||||
];
|
||||
|
||||
} else {
|
||||
// If we could not find the record, we still need to return the right arguments in the array for the webservice.
|
||||
$return[$id] = [
|
||||
'id' => $id,
|
||||
'uniqueid' => '',
|
||||
'progress' => 0,
|
||||
'estimated' => '',
|
||||
'message' => get_string('invalidrecordunknown', 'error'),
|
||||
'timeout' => stored_progress_bar::get_timeout(),
|
||||
'error' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
@ -4697,15 +4697,12 @@ EOD;
|
||||
* @param float $percent
|
||||
* @param string $msg Message
|
||||
* @param string $estimate time remaining message
|
||||
* @param bool $error Was there an error?
|
||||
* @return string ascii fragment
|
||||
*/
|
||||
public function render_progress_bar_update(string $id, float $percent, string $msg, string $estimate): string {
|
||||
return html_writer::script(js_writer::function_call('updateProgressBar', [
|
||||
$id,
|
||||
round($percent, 1),
|
||||
$msg,
|
||||
$estimate,
|
||||
]));
|
||||
public function render_progress_bar_update(string $id, float $percent, string $msg, string $estimate,
|
||||
bool $error = false): string {
|
||||
return html_writer::script(js_writer::function_call('updateProgressBar', [$id, $percent, $msg, $estimate, $error]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,9 +116,11 @@ class core_renderer_cli extends core_renderer {
|
||||
* @param float $percent
|
||||
* @param string $msg Message
|
||||
* @param string $estimate time remaining message
|
||||
* @param bool $error (Unused in cli)
|
||||
* @return string ascii fragment
|
||||
*/
|
||||
public function render_progress_bar_update(string $id, float $percent, string $msg, string $estimate): string {
|
||||
public function render_progress_bar_update(string $id, float $percent, string $msg, string $estimate,
|
||||
bool $error = false): string {
|
||||
$size = 55; // The width of the progress bar in chars.
|
||||
$ascii = '';
|
||||
|
||||
|
@ -34,35 +34,49 @@ use core\exception\coding_exception;
|
||||
* @category output
|
||||
*/
|
||||
class progress_bar implements renderable, templatable {
|
||||
/** @var string html id */
|
||||
private $htmlid;
|
||||
|
||||
/** @var bool Can use output buffering. */
|
||||
protected static $supportsoutputbuffering = false;
|
||||
|
||||
/** @var string unique id */
|
||||
protected $idnumber;
|
||||
|
||||
/** @var int total width */
|
||||
private $width;
|
||||
protected $width;
|
||||
|
||||
/** @var int last percentage printed */
|
||||
private $percent = 0;
|
||||
protected $percent = 0;
|
||||
|
||||
/** @var int time when last printed */
|
||||
private $lastupdate = 0;
|
||||
protected $lastupdate = 0;
|
||||
|
||||
/** @var int when did we start printing this */
|
||||
private $timestart = 0;
|
||||
protected $timestart = 0;
|
||||
|
||||
/** @var bool Whether or not to auto render updates to the screen */
|
||||
protected $autoupdate = true;
|
||||
|
||||
/** @var bool Whether or not an error has occured */
|
||||
protected $haserrored = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Prints JS code if $autostart true.
|
||||
*
|
||||
* @param string $htmlid The container ID.
|
||||
* @param string $htmlid The unique ID for the progress bar or HTML container id.
|
||||
* @param int $width The suggested width.
|
||||
* @param bool $autostart Whether to start the progress bar right away.
|
||||
*/
|
||||
public function __construct($htmlid = '', $width = 500, $autostart = false) {
|
||||
if (!CLI_SCRIPT && !NO_OUTPUT_BUFFERING) {
|
||||
if (!static::$supportsoutputbuffering && !CLI_SCRIPT && !NO_OUTPUT_BUFFERING) {
|
||||
debugging('progress_bar used in a non-CLI script without setting NO_OUTPUT_BUFFERING.', DEBUG_DEVELOPER);
|
||||
}
|
||||
|
||||
if (!empty($htmlid)) {
|
||||
$this->htmlid = $htmlid;
|
||||
$this->idnumber = $htmlid;
|
||||
} else {
|
||||
$this->htmlid = 'pbar_' . uniqid();
|
||||
$this->idnumber = 'pbar_'.uniqid();
|
||||
}
|
||||
|
||||
$this->width = $width;
|
||||
@ -77,7 +91,15 @@ class progress_bar implements renderable, templatable {
|
||||
* @return string id
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return $this->htmlid;
|
||||
return $this->idnumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the percent
|
||||
* @return float
|
||||
*/
|
||||
public function get_percent(): float {
|
||||
return $this->percent;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -86,15 +108,43 @@ class progress_bar implements renderable, templatable {
|
||||
* @return void Echo's output
|
||||
*/
|
||||
public function create() {
|
||||
global $OUTPUT;
|
||||
|
||||
$this->timestart = microtime(true);
|
||||
$this->render();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the progress bar.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render(): void {
|
||||
flush();
|
||||
echo $OUTPUT->render($this);
|
||||
echo $this->get_content();
|
||||
flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content to be rendered
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content(): string {
|
||||
global $OUTPUT;
|
||||
return $OUTPUT->render($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not to auto render updates to the screen
|
||||
*
|
||||
* @param bool $value
|
||||
* @return void
|
||||
*/
|
||||
public function auto_update(bool $value): void {
|
||||
$this->autoupdate = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the progress bar.
|
||||
*
|
||||
@ -103,7 +153,7 @@ class progress_bar implements renderable, templatable {
|
||||
* @return void Echo's output
|
||||
* @throws coding_exception
|
||||
*/
|
||||
private function update_raw($percent, $msg) {
|
||||
protected function update_raw($percent, $msg) {
|
||||
global $OUTPUT;
|
||||
|
||||
if (empty($this->timestart)) {
|
||||
@ -111,30 +161,20 @@ class progress_bar implements renderable, templatable {
|
||||
'argument to the constructor) before you try updating the progress bar.');
|
||||
}
|
||||
|
||||
$estimate = $this->estimate($percent);
|
||||
|
||||
if ($estimate === null) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
|
||||
// Always do the first and last updates.
|
||||
} else if ($estimate == 0) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
|
||||
// Always do the last updates.
|
||||
} else if ($this->lastupdate + 20 < time()) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
|
||||
// We must update otherwise browser would time out.
|
||||
} else if (round($this->percent, 2) === round($percent, 2)) {
|
||||
// No significant change, no need to update anything.
|
||||
// No significant change, no need to update anything.
|
||||
if (round($this->percent, 2) === round($percent, 2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$estimatemsg = '';
|
||||
if ($estimate != 0 && is_numeric($estimate)) {
|
||||
// Err on the conservative side and also avoid showing 'now' as the estimate.
|
||||
$estimatemsg = format_time(ceil($estimate));
|
||||
}
|
||||
$estimatemsg = $this->get_estimate_message($percent);
|
||||
|
||||
$this->percent = $percent;
|
||||
$this->lastupdate = microtime(true);
|
||||
|
||||
echo $OUTPUT->render_progress_bar_update($this->htmlid, $this->percent, $msg, $estimatemsg);
|
||||
flush();
|
||||
if ($this->autoupdate) {
|
||||
echo $OUTPUT->render_progress_bar_update($this->idnumber, sprintf("%.1f", $this->percent), $msg, $estimatemsg);
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -143,7 +183,7 @@ class progress_bar implements renderable, templatable {
|
||||
* @param int $pt From 1-100.
|
||||
* @return mixed Null (unknown), or int.
|
||||
*/
|
||||
private function estimate($pt) {
|
||||
protected function estimate($pt) {
|
||||
if ($this->lastupdate == 0) {
|
||||
return null;
|
||||
}
|
||||
@ -201,10 +241,68 @@ class progress_bar implements renderable, templatable {
|
||||
*/
|
||||
public function export_for_template(renderer_base $output) {
|
||||
return [
|
||||
'id' => $this->htmlid,
|
||||
'id' => '',
|
||||
'idnumber' => $this->idnumber,
|
||||
'width' => $this->width,
|
||||
'class' => '',
|
||||
'value' => 0,
|
||||
'error' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This gets the estimate message to be displayed with the progress bar.
|
||||
*
|
||||
* @param float $percent
|
||||
* @return string
|
||||
*/
|
||||
public function get_estimate_message(float $percent): string {
|
||||
$estimate = $this->estimate($percent);
|
||||
$estimatemsg = '';
|
||||
if ($estimate != 0 && is_numeric($estimate)) {
|
||||
$estimatemsg = format_time(ceil($estimate));
|
||||
}
|
||||
|
||||
return $estimatemsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error flag on the object
|
||||
*
|
||||
* @param bool $value
|
||||
* @return void
|
||||
*/
|
||||
protected function set_haserrored(bool $value): void {
|
||||
$this->haserrored = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the process has errored
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_haserrored(): bool {
|
||||
return $this->haserrored;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set that the process running has errored
|
||||
*
|
||||
* @param string $errormsg
|
||||
* @return void
|
||||
*/
|
||||
public function error(string $errormsg): void {
|
||||
global $OUTPUT;
|
||||
|
||||
$this->haserrored = true;
|
||||
$this->message = $errormsg;
|
||||
|
||||
if ($this->autoupdate) {
|
||||
echo $OUTPUT->render_progress_bar_update($this->idnumber, sprintf("%.1f", $this->percent), $errormsg, '', true);
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Alias this class to the old name.
|
||||
|
365
lib/classes/output/stored_progress_bar.php
Normal file
365
lib/classes/output/stored_progress_bar.php
Normal file
@ -0,0 +1,365 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\output;
|
||||
|
||||
/**
|
||||
* Stored progress bar class.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @author Conn Warwicker <conn.warwicker@catalyst-eu.net>
|
||||
*/
|
||||
class stored_progress_bar extends progress_bar {
|
||||
|
||||
/** @var bool Can use output buffering. */
|
||||
protected static $supportsoutputbuffering = true;
|
||||
|
||||
/** @var int DB record ID */
|
||||
protected $recordid;
|
||||
|
||||
/** @var string|null Message to associate with bar */
|
||||
protected $message = null;
|
||||
|
||||
/** @var \core\clock Clock object */
|
||||
protected $clock;
|
||||
|
||||
/**
|
||||
* This overwrites the progress_bar::__construct method.
|
||||
*
|
||||
* @param string $idnumber
|
||||
*/
|
||||
public function __construct($idnumber) {
|
||||
|
||||
$this->clock = \core\di::get(\core\clock::class);
|
||||
|
||||
// Construct from the parent.
|
||||
parent::__construct($idnumber, 0, true);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Just set the timestart, do not render the bar immediately.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function create(): void {
|
||||
$this->timestart = $this->clock->time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the stored progress bar from the database based on its uniqued idnumber
|
||||
*
|
||||
* @param string $idnumber Unique ID of the bar
|
||||
* @return stored_progress_bar|null
|
||||
*/
|
||||
public static function get_by_idnumber(string $idnumber): ?stored_progress_bar {
|
||||
global $DB;
|
||||
|
||||
$record = $DB->get_record('stored_progress', ['idnumber' => $idnumber]);
|
||||
if ($record) {
|
||||
return self::load($record);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the stored progress bar from the database, based on it's record ID
|
||||
*
|
||||
* @param int $id Database record ID
|
||||
* @return stored_progress_bar|null
|
||||
*/
|
||||
public static function get_by_id(int $id): ?stored_progress_bar {
|
||||
global $DB;
|
||||
|
||||
$record = $DB->get_record('stored_progress', ['id' => $id]);
|
||||
if ($record) {
|
||||
return self::load($record);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the stored progress bar object from its record in the database.
|
||||
*
|
||||
* @param stdClass $record
|
||||
* @return stored_progress_bar
|
||||
*/
|
||||
public static function load(\stdClass $record): stored_progress_bar {
|
||||
$progress = new stored_progress_bar($record->idnumber);
|
||||
$progress->set_record_id($record->id);
|
||||
$progress->set_time_started($record->timestart);
|
||||
$progress->set_last_updated($record->lastupdate);
|
||||
$progress->set_percent($record->percentcompleted);
|
||||
$progress->set_message($record->message);
|
||||
$progress->set_haserrored($record->haserrored);
|
||||
return $progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the DB record ID
|
||||
*
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
protected function set_record_id(int $id): void {
|
||||
$this->recordid = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time we started the process.
|
||||
*
|
||||
* @param int $value
|
||||
* @return void
|
||||
*/
|
||||
protected function set_time_started(int $value): void {
|
||||
$this->timestart = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time we started last updated the progress.
|
||||
*
|
||||
* @param int|null $value
|
||||
* @return void
|
||||
*/
|
||||
protected function set_last_updated(?int $value = null): void {
|
||||
$this->lastupdate = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the percent completed.
|
||||
*
|
||||
* @param float|null $value
|
||||
* @return void
|
||||
*/
|
||||
protected function set_percent($value = null): void {
|
||||
$this->percent = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the message.
|
||||
*
|
||||
* @param string|null $value
|
||||
* @return void
|
||||
*/
|
||||
protected function set_message(?string $value = null): void {
|
||||
$this->message = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set that the process running has errored and store that against the bar
|
||||
*
|
||||
* @param string $errormsg
|
||||
* @return void
|
||||
*/
|
||||
public function error(string $errormsg): void {
|
||||
// Update the error variables.
|
||||
parent::error($errormsg);
|
||||
|
||||
// Update the record.
|
||||
$this->update_record();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the progress bar message.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_message(): ?string {
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content to display the progress bar and start polling via AJAX
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content(): string {
|
||||
global $CFG, $PAGE, $OUTPUT;
|
||||
|
||||
$PAGE->requires->js_call_amd('core/stored_progress', 'init', [
|
||||
self::get_timeout(),
|
||||
]);
|
||||
|
||||
$context = $this->export_for_template($OUTPUT);
|
||||
return $OUTPUT->render_from_template('core/progress_bar', $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export for template.
|
||||
*
|
||||
* @param renderer_base $output The renderer.
|
||||
* @return array
|
||||
*/
|
||||
public function export_for_template(\renderer_base $output): array {
|
||||
return [
|
||||
'id' => $this->recordid,
|
||||
'idnumber' => $this->idnumber,
|
||||
'width' => $this->width,
|
||||
'class' => 'stored-progress-bar',
|
||||
'value' => $this->percent,
|
||||
'message' => $this->message,
|
||||
'error' => $this->haserrored,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the recording of the progress and store in the database
|
||||
*
|
||||
* @return int ID of the DB record
|
||||
*/
|
||||
public function start(): int {
|
||||
global $OUTPUT, $DB;
|
||||
|
||||
// If we are running in an non-interactive CLI environment, call the progress bar renderer to avoid warnings
|
||||
// when we do an update.
|
||||
if (defined('STDOUT') && !stream_isatty(STDOUT)) {
|
||||
$OUTPUT->render_progress_bar($this);
|
||||
}
|
||||
|
||||
// Delete any existing records for this.
|
||||
$this->clear_records();
|
||||
|
||||
// Create new progress record.
|
||||
$this->recordid = $DB->insert_record('stored_progress', [
|
||||
'idnumber' => $this->idnumber,
|
||||
'timestart' => (int)$this->timestart,
|
||||
]);
|
||||
|
||||
return $this->recordid;
|
||||
}
|
||||
|
||||
/**
|
||||
* End the polling progress and delete the DB record.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function clear_records(): void {
|
||||
global $DB;
|
||||
|
||||
$DB->delete_records('stored_progress', [
|
||||
'idnumber' => $this->idnumber,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the database record with the percentage and message
|
||||
*
|
||||
* @param float $percent
|
||||
* @param string $msg
|
||||
* @return void
|
||||
*/
|
||||
protected function update_raw($percent, $msg): void {
|
||||
$this->percent = $percent;
|
||||
$this->message = $msg;
|
||||
|
||||
// Update the database record with the new data.
|
||||
$this->update_record();
|
||||
|
||||
// Update any CLI script's progress with an ASCII progress bar.
|
||||
$this->render_update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an update to the CLI
|
||||
*
|
||||
* This will only work in CLI scripts, and not in scheduled/adhoc tasks even though they run via CLI,
|
||||
* as they seem to use a different renderer (core_renderer instead of core_renderer_cli).
|
||||
*
|
||||
* We also can't check this based on "CLI_SCRIPT" const as that is true for tasks.
|
||||
*
|
||||
* So this will just check a flag to see if we want auto rendering of updates.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function render_update(): void {
|
||||
global $OUTPUT;
|
||||
|
||||
// If no output buffering, don't render it at all.
|
||||
if (defined('NO_OUTPUT_BUFFERING') && NO_OUTPUT_BUFFERING) {
|
||||
$this->auto_update(false);
|
||||
}
|
||||
|
||||
// If we want the screen to auto update, render it.
|
||||
if ($this->autoupdate) {
|
||||
echo $OUTPUT->render_progress_bar_update(
|
||||
$this->idnumber, sprintf("%.1f", $this->percent), $this->message, $this->get_estimate_message($this->percent)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the database record
|
||||
*
|
||||
* @throws \moodle_exception
|
||||
* @return void
|
||||
*/
|
||||
protected function update_record(): void {
|
||||
global $DB;
|
||||
|
||||
if (is_null($this->recordid)) {
|
||||
throw new \moodle_exception('Polling has not been started. Cannot set iteration.');
|
||||
}
|
||||
|
||||
// Update time.
|
||||
$this->lastupdate = $this->clock->time();
|
||||
|
||||
// Update the database record.
|
||||
$record = new \stdClass();
|
||||
$record->id = $this->recordid;
|
||||
$record->lastupdate = (int)$this->lastupdate;
|
||||
$record->percentcompleted = $this->percent;
|
||||
$record->message = $this->message;
|
||||
$record->haserrored = $this->haserrored;
|
||||
$DB->update_record('stored_progress', $record);
|
||||
}
|
||||
|
||||
/**
|
||||
* We need a way to specify a unique idnumber for processes being monitored, so that
|
||||
* firstly we don't accidentally overwrite a running process, and secondly so we can
|
||||
* automatically load them in some cases, without having to manually code in its name.
|
||||
*
|
||||
* So this uses the classname of the object being monitored, along with its id.
|
||||
*
|
||||
* This method should be used when creating the stored_progress record to set it's idnumber.
|
||||
*
|
||||
* @param string $class Class name of the object being monitored, e.g. \local_something\task\my_task
|
||||
* @param int|null $id ID of an object from database, e.g. 123
|
||||
* @return string Converted string, e.g. local_something_task_my_task_123
|
||||
*/
|
||||
public static function convert_to_idnumber(string $class, ?int $id = null): string {
|
||||
$idnumber = preg_replace("/[^a-z0-9_]/", "_", ltrim($class, '\\'));
|
||||
if (!is_null($id)) {
|
||||
$idnumber .= '_' . $id;
|
||||
}
|
||||
|
||||
return $idnumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the polling timeout in seconds. Default: 5.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get_timeout(): int {
|
||||
global $CFG;
|
||||
return $CFG->progresspollinterval ?? 5;
|
||||
}
|
||||
|
||||
}
|
@ -27,6 +27,7 @@ namespace core\task;
|
||||
* Simple task to delete user accounts for users who have not confirmed in time.
|
||||
*/
|
||||
class delete_unconfirmed_users_task extends scheduled_task {
|
||||
use stored_progress_task_trait;
|
||||
|
||||
/**
|
||||
* Get a descriptive name for this task (shown to admins).
|
||||
@ -49,15 +50,26 @@ class delete_unconfirmed_users_task extends scheduled_task {
|
||||
// Delete users who haven't confirmed within required period.
|
||||
if (!empty($CFG->deleteunconfirmed)) {
|
||||
$cuttime = $timenow - ($CFG->deleteunconfirmed * 3600);
|
||||
$rs = $DB->get_recordset_sql ("SELECT *
|
||||
FROM {user}
|
||||
WHERE confirmed = 0 AND timecreated > 0
|
||||
AND timecreated < ? AND deleted = 0", array($cuttime));
|
||||
$select = "confirmed = 0 AND timecreated > 0 AND timecreated < ? AND deleted = 0";
|
||||
$params = [$cuttime];
|
||||
$count = $DB->count_records_select('user', $select, $params);
|
||||
|
||||
// Exit early if there are no records to process.
|
||||
if (!$count) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->start_stored_progress();
|
||||
$rs = $DB->get_recordset_select('user', $select, $params);
|
||||
$processed = 0;
|
||||
foreach ($rs as $user) {
|
||||
delete_user($user);
|
||||
mtrace(" Deleted unconfirmed user ".fullname($user, true)." ($user->id)");
|
||||
$message = " Deleted unconfirmed user ".fullname($user, true)." ($user->id)";
|
||||
$processed++;
|
||||
$this->progress->update($processed, $count, $message);
|
||||
}
|
||||
$rs->close();
|
||||
$this->progress->update($processed, $count, "Deleted $processed out of $count unconfirmed users");
|
||||
}
|
||||
}
|
||||
|
||||
|
53
lib/classes/task/stored_progress_bar_cleanup_task.php
Normal file
53
lib/classes/task/stored_progress_bar_cleanup_task.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\task;
|
||||
|
||||
/**
|
||||
* Scheduled task to clean up old stored_progress bar records.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @author Conn Warwicker <conn.warwicker@catalyst-eu.net>
|
||||
*/
|
||||
class stored_progress_bar_cleanup_task extends scheduled_task {
|
||||
|
||||
/**
|
||||
* Get a descriptive name for this task (shown to admins).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name() {
|
||||
return get_string('storedprogressbarcleanuptask', 'admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the old stored progress bar records.
|
||||
* By default this runs once per day at 1AM.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function execute(): void {
|
||||
global $DB;
|
||||
|
||||
$twentyfourhoursago = time() - DAYSECS;
|
||||
|
||||
$DB->delete_records_select('stored_progress', 'lastupdate < :ago', ['ago' => $twentyfourhoursago]);
|
||||
|
||||
mtrace('Deleted old stored_progress records');
|
||||
}
|
||||
}
|
59
lib/classes/task/stored_progress_task_trait.php
Normal file
59
lib/classes/task/stored_progress_task_trait.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\task;
|
||||
|
||||
/**
|
||||
* Trait to use in tasks to automatically add stored progress functionality.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2024 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @author Conn Warwicker <conn.warwicker@catalyst-eu.net>
|
||||
*/
|
||||
trait stored_progress_task_trait {
|
||||
|
||||
/** @var \core\output\stored_progress_bar|null $progress */
|
||||
protected $progress = null;
|
||||
|
||||
/**
|
||||
* Start a stored progress bar implementation for the task this trait is used in.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function start_stored_progress(): void {
|
||||
global $OUTPUT, $PAGE;
|
||||
|
||||
// To get around the issue in MDL-80770, we are manually setting the renderer to cli.
|
||||
$OUTPUT = $PAGE->get_renderer('core', null, 'cli');
|
||||
|
||||
// Construct a unique name for the progress bar.
|
||||
// For adhoc tasks, this will need the ID in it. For scheduled tasks just the class name.
|
||||
if (method_exists($this, 'get_id')) {
|
||||
$name = get_class($this) . '_' . $this->get_id();
|
||||
} else {
|
||||
$name = get_class($this);
|
||||
}
|
||||
|
||||
$this->progress = new \core\output\stored_progress_bar(
|
||||
\core\output\stored_progress_bar::convert_to_idnumber($name)
|
||||
);
|
||||
|
||||
// Start the progress.
|
||||
$this->progress->start();
|
||||
}
|
||||
|
||||
}
|
@ -4842,5 +4842,22 @@
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="stored_progress" COMMENT="Records for any long running tasks we want to poll for progress">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="idnumber" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="timestart" TYPE="int" LENGTH="20" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="lastupdate" TYPE="int" LENGTH="20" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="percentcompleted" TYPE="number" LENGTH="5" NOTNULL="false" DEFAULT="0" SEQUENCE="false" DECIMALS="2"/>
|
||||
<FIELD NAME="message" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="haserrored" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
</KEYS>
|
||||
<INDEXES>
|
||||
<INDEX NAME="uid_index" UNIQUE="false" FIELDS="idnumber"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
|
@ -3223,6 +3223,14 @@ $functions = array(
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
],
|
||||
'core_output_poll_stored_progress' => [
|
||||
'classname' => 'core\external\output\poll_stored_progress',
|
||||
'methodname' => 'execute',
|
||||
'description' => 'Polls for the current percentage progress of a stored progress object',
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
'readonlysession' => true,
|
||||
],
|
||||
);
|
||||
|
||||
$services = array(
|
||||
|
@ -476,4 +476,14 @@ $tasks = array(
|
||||
'dayofweek' => 'R',
|
||||
'disabled' => true,
|
||||
],
|
||||
[
|
||||
'classname' => 'core\task\stored_progress_bar_cleanup_task',
|
||||
'blocking' => 0,
|
||||
'minute' => '00',
|
||||
'hour' => '01',
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*',
|
||||
'disabled' => false,
|
||||
],
|
||||
);
|
||||
|
@ -1178,5 +1178,33 @@ function xmldb_main_upgrade($oldversion) {
|
||||
upgrade_main_savepoint(true, 2024070500.01);
|
||||
}
|
||||
|
||||
if ($oldversion < 2024071900.01) {
|
||||
// Define table stored_progress to be created.
|
||||
$table = new xmldb_table('stored_progress');
|
||||
|
||||
// Adding fields to table stored_progress.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('idnumber', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timestart', XMLDB_TYPE_INTEGER, '20', null, null, null, null);
|
||||
$table->add_field('lastupdate', XMLDB_TYPE_INTEGER, '20', null, null, null, null);
|
||||
$table->add_field('percentcompleted', XMLDB_TYPE_NUMBER, '5, 2', null, null, null, '0');
|
||||
$table->add_field('message', XMLDB_TYPE_CHAR, '255', null, null, null, null);
|
||||
$table->add_field('haserrored', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0');
|
||||
|
||||
// Adding keys to table stored_progress.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
|
||||
// Adding indexes to table stored_progress.
|
||||
$table->add_index('uid_index', XMLDB_INDEX_NOTUNIQUE, ['idnumber']);
|
||||
|
||||
// Conditionally launch create table for stored_progress.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2024071900.01);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1164,7 +1164,8 @@ function stripHTML(str) {
|
||||
throw new Error('stripHTML can not be used any more. Please use jQuery instead.');
|
||||
}
|
||||
|
||||
function updateProgressBar(id, percent, msg, estimate) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function updateProgressBar(id, percent, msg, estimate, error) {
|
||||
var event,
|
||||
el = document.getElementById(id),
|
||||
eventData = {};
|
||||
@ -1176,6 +1177,7 @@ function updateProgressBar(id, percent, msg, estimate) {
|
||||
eventData.message = msg;
|
||||
eventData.percent = percent;
|
||||
eventData.estimate = estimate;
|
||||
eventData.error = error;
|
||||
|
||||
try {
|
||||
event = new CustomEvent('update', {
|
||||
|
@ -25,17 +25,17 @@
|
||||
"width": "500"
|
||||
}
|
||||
}}
|
||||
<div id="{{id}}" class="progressbar_container mb-3">
|
||||
<div id="{{idnumber}}" class="progressbar_container mb-3 {{class}}" data-recordid="{{id}}">
|
||||
<div class="progress">
|
||||
<div id="{{id}}_bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-value="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%"></div>
|
||||
<div id="{{idnumber}}_bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-value="{{value}}" aria-valuemin="0" aria-valuemax="100" style="width: {{value}}%"></div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div style="flex: 1 1 0; min-width: 0;">
|
||||
<div id="{{id}}_status" class="text-truncate"> </div>
|
||||
<div id="{{idnumber}}_status" class="text-truncate"> </div>
|
||||
</div>
|
||||
<div class="text-right pl-3" style="flex: 0 0 content">
|
||||
<span id="{{id}}_estimate" class=""> </span>
|
||||
<span id="{{id}}_percentage" class="d-inline-block" style="width: 3em">0%</span>
|
||||
<span id="{{idnumber}}_estimate" class=""> </span>
|
||||
<span id="{{idnumber}}_percentage" class="d-inline-block" style="width: 3em">{{value}}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -43,21 +43,33 @@
|
||||
{{! We must not use the JS helper otherwise this gets executed too late. }}
|
||||
<script>
|
||||
(function() {
|
||||
var el = document.getElementById('{{id}}'),
|
||||
progressBar = document.getElementById('{{id}}_bar'),
|
||||
statusIndicator = document.getElementById('{{id}}_status'),
|
||||
estimateIndicator = document.getElementById('{{id}}_estimate');
|
||||
percentageIndicator = document.getElementById('{{id}}_percentage');
|
||||
|
||||
let el = document.getElementById('{{idnumber}}');
|
||||
let progressBar = document.getElementById('{{idnumber}}_bar');
|
||||
let statusIndicator = document.getElementById('{{idnumber}}_status');
|
||||
let estimateIndicator = document.getElementById('{{idnumber}}_estimate');
|
||||
let percentageIndicator = document.getElementById('{{idnumber}}_percentage');
|
||||
|
||||
// Change background colour to red if there was an error.
|
||||
if ({{error}} == 1) {
|
||||
el.querySelector('.progress-bar').style.background = 'red';
|
||||
}
|
||||
|
||||
el.addEventListener('update', function(e) {
|
||||
var msg = e.detail.message,
|
||||
percent = e.detail.percent,
|
||||
estimate = e.detail.estimate;
|
||||
estimate = e.detail.estimate
|
||||
error = e.detail.error;
|
||||
|
||||
statusIndicator.textContent = msg;
|
||||
progressBar.style.width = percent.toFixed(1) + '%';
|
||||
progressBar.setAttribute('value', percent.toFixed(1));
|
||||
if (percent === 100) {
|
||||
|
||||
if (error) {
|
||||
progressBar.classList.add('bg-danger');
|
||||
progressBar.classList.remove('bg-success');
|
||||
estimateIndicator.textContent = '';
|
||||
} else if (percent === 100) {
|
||||
progressBar.classList.add('bg-success');
|
||||
progressBar.classList.remove('progress-bar-striped');
|
||||
progressBar.classList.remove('progress-bar-animated');
|
||||
|
@ -1530,6 +1530,60 @@ EOD;
|
||||
return $DB->get_record('user_lastaccess', ['id' => $recordid], '*', MUST_EXIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a stored_progress record and return the ID.
|
||||
*
|
||||
* All fields are optional, required fields will be generated if not supplied.
|
||||
*
|
||||
* @param ?string $idnumber The unique ID Number for this stored progress.
|
||||
* @param ?int $timestart The time progress was started, defaults to now.
|
||||
* @param ?int $lastupdate The time the progress was last updated.
|
||||
* @param float $percent The percentage progress so far.
|
||||
* @param ?string $message An error message.
|
||||
* @param ?bool $haserrored Whether the process has encountered an error.
|
||||
* @return stdClass The record including the inserted id.
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public function create_stored_progress(
|
||||
?string $idnumber = null,
|
||||
?int $timestart = null,
|
||||
?int $lastupdate = null,
|
||||
float $percent = 0.00,
|
||||
?string $message = null,
|
||||
?bool $haserrored = false,
|
||||
): stdClass {
|
||||
global $DB;
|
||||
$record = (object)[
|
||||
'idnumber' => $idnumber ?? random_string(),
|
||||
'timestart' => $timestart ?? time(),
|
||||
'lastupdate' => $lastupdate,
|
||||
'percentcompleted' => $percent,
|
||||
'message' => $message,
|
||||
'haserrored' => $haserrored,
|
||||
];
|
||||
$record->id = $DB->insert_record('stored_progress', $record);
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a stored progress record from an array of fields.
|
||||
*
|
||||
* For use as a behat createable entity. Use {@see self::create_stored_progress()} if calling directly.
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function create_stored_progress_bar(array $data): void {
|
||||
$this->create_stored_progress(
|
||||
$data['idnumber'] ?? null,
|
||||
$data['timestart'] ?? null,
|
||||
$data['lastupdate'] ?? null,
|
||||
$data['percent'] ?? 0.00,
|
||||
$data['message'] ?? null,
|
||||
$data['haserrored'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a default generator for a given component.
|
||||
*
|
||||
|
@ -2489,6 +2489,23 @@ EOF;
|
||||
} else {
|
||||
throw new \Behat\Mink\Exception\ExpectationException('Invalid state for switch: ' . $state, $this->getSession());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a stored progress bar.
|
||||
*
|
||||
* @Given I set the stored progress bar :idnumber to :percent
|
||||
* @param string $idnumber The unique idnumber of the stored progress bar.
|
||||
* @param float $percent The value to update the progress bar to.
|
||||
*/
|
||||
public function i_set_the_stored_progress_bar_to(string $idnumber, float $percent): void {
|
||||
$progress = \core\output\stored_progress_bar::get_by_idnumber($idnumber);
|
||||
if (!$progress) {
|
||||
throw new invalid_parameter_exception('No progress bar with idnumber ' . $idnumber . 'found.');
|
||||
}
|
||||
$progress->auto_update(false);
|
||||
$progress->update_full($percent, '');
|
||||
}
|
||||
|
||||
/**
|
||||
|
65
lib/tests/external/output/poll_stored_progress_test.php
vendored
Normal file
65
lib/tests/external/output/poll_stored_progress_test.php
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\external\output;
|
||||
|
||||
/**
|
||||
* Unit tests for poll_stored_progress
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core\external\poll_stored_progress_test
|
||||
*/
|
||||
final class poll_stored_progress_test extends \advanced_testcase {
|
||||
/**
|
||||
* Throw an exception if the wrong data type is passed for an ID.
|
||||
*/
|
||||
public function test_execute_invalid_id(): void {
|
||||
$debuginfo = 'Invalid external api parameter: the value is "foo", the server was expecting "int" type';
|
||||
$pollstoredprogress = new poll_stored_progress();
|
||||
$this->expectExceptionObject(new \invalid_parameter_exception($debuginfo));
|
||||
$pollstoredprogress->execute(['foo']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Passing a list of IDs returns a corresponding list of records.
|
||||
*/
|
||||
public function test_execute(): void {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
$progress1 = $generator->create_stored_progress();
|
||||
$progress2 = $generator->create_stored_progress();
|
||||
$falseid = $progress2->id + 1;
|
||||
|
||||
$ids = [
|
||||
$progress1->id,
|
||||
$progress2->id,
|
||||
$falseid,
|
||||
];
|
||||
|
||||
$pollstoredprogress = new poll_stored_progress();
|
||||
$result = $pollstoredprogress->execute($ids);
|
||||
|
||||
$this->assertEquals($progress1->id, $result[$progress1->id]['id']);
|
||||
$this->assertEquals($progress1->idnumber, $result[$progress1->id]['uniqueid']);
|
||||
$this->assertEquals($progress2->id, $result[$progress2->id]['id']);
|
||||
$this->assertEquals($progress2->idnumber, $result[$progress2->id]['uniqueid']);
|
||||
$this->assertEquals($falseid, $result[$falseid]['id']);
|
||||
$this->assertEmpty($result[$falseid]['uniqueid']); // Empty when no matching record is found.
|
||||
}
|
||||
}
|
195
lib/tests/stored_progress_bar_test.php
Normal file
195
lib/tests/stored_progress_bar_test.php
Normal file
@ -0,0 +1,195 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core;
|
||||
|
||||
use core\output\stored_progress_bar;
|
||||
|
||||
/**
|
||||
* Unit tests for \core\output\stored_progress_bar
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core\output\stored_progress_bar
|
||||
*/
|
||||
final class stored_progress_bar_test extends \advanced_testcase {
|
||||
/**
|
||||
* Test the progress bar initialisation.
|
||||
*
|
||||
* Creating a new stored progress bar object should set the idnumber,
|
||||
* and not generate any output.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_init(): void {
|
||||
$idnumber = random_string();
|
||||
$progress = new stored_progress_bar($idnumber);
|
||||
$this->assertEquals($idnumber, $progress->get_id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling get_by_idnumber() fetches the correct record.
|
||||
*
|
||||
* @return void
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public function test_get_by_idnumber(): void {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
$progress1 = $generator->create_stored_progress(message: 'progress1');
|
||||
$progress2 = $generator->create_stored_progress(message: 'progress2');
|
||||
$progress3 = $generator->create_stored_progress(message: 'progress3');
|
||||
|
||||
$progressbar = stored_progress_bar::get_by_idnumber($progress2->idnumber);
|
||||
$this->assertEquals('progress2', $progressbar->get_message());
|
||||
$progressbar = stored_progress_bar::get_by_idnumber($progress1->idnumber);
|
||||
$this->assertEquals('progress1', $progressbar->get_message());
|
||||
$progressbar = stored_progress_bar::get_by_idnumber($progress3->idnumber);
|
||||
$this->assertEquals('progress3', $progressbar->get_message());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling get_by_id() fetches the correct record.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_by_id(): void {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
$progress1 = $generator->create_stored_progress();
|
||||
$progress2 = $generator->create_stored_progress();
|
||||
$progress3 = $generator->create_stored_progress();
|
||||
|
||||
$progressbar = stored_progress_bar::get_by_id($progress2->id);
|
||||
$this->assertEquals($progress2->idnumber, $progressbar->get_id());
|
||||
$progressbar = stored_progress_bar::get_by_id($progress1->id);
|
||||
$this->assertEquals($progress1->idnumber, $progressbar->get_id());
|
||||
$progressbar = stored_progress_bar::get_by_id($progress3->id);
|
||||
$this->assertEquals($progress3->idnumber, $progressbar->get_id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling error() method updates the record with the new message and haserrored = true.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_error(): void {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
$progress = $generator->create_stored_progress();
|
||||
|
||||
$originalprogressbar = stored_progress_bar::get_by_id($progress->id);
|
||||
$originalprogressbar->auto_update(false);
|
||||
$this->assertEmpty($originalprogressbar->get_message());
|
||||
$this->assertFalse($originalprogressbar->get_haserrored());
|
||||
|
||||
$message = 'There was an error';
|
||||
$originalprogressbar->error($message);
|
||||
|
||||
$updatedprogressbar = stored_progress_bar::get_by_id($progress->id);
|
||||
$this->assertEquals($message, $updatedprogressbar->get_message());
|
||||
$this->assertTrue($updatedprogressbar->get_haserrored());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling start() replaces the existing record with a new one for the same idnumber.
|
||||
*/
|
||||
public function test_start(): void {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
$originalprogress = $generator->create_stored_progress();
|
||||
|
||||
$progressbar = stored_progress_bar::get_by_id($originalprogress->id);
|
||||
$this->assertNotNull($progressbar);
|
||||
$this->assertEquals($originalprogress->idnumber, $progressbar->get_id());
|
||||
|
||||
$newid = $progressbar->start();
|
||||
|
||||
$oldprogressbar = stored_progress_bar::get_by_id($originalprogress->id);
|
||||
$this->assertNull($oldprogressbar);
|
||||
|
||||
$newprogressbar = stored_progress_bar::get_by_id($newid);
|
||||
$this->assertNotNull($newprogressbar);
|
||||
$this->assertEquals($originalprogress->idnumber, $newprogressbar->get_id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling convert_to_idnumber() returns a valid idnumber.
|
||||
*
|
||||
* Leading backslashes are stripped from the class name, and any disallowed characters
|
||||
* (any except lower-case letters, numbers and underscores) are replaced with underscores.
|
||||
* The result is then concatenated with an underscore and the id argument.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_convert_to_idnumber(): void {
|
||||
$classname = '\\foo\\bar\\class-1_Name';
|
||||
$id = rand(1, 10);
|
||||
|
||||
$idnumber = stored_progress_bar::convert_to_idnumber($classname, $id);
|
||||
$this->assertEquals('foo_bar_class_1__ame_' . $id, $idnumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling get_timeout() returns the global progresspollinterval setting, or 5 by default.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_timeout(): void {
|
||||
global $CFG;
|
||||
$this->resetAfterTest();
|
||||
|
||||
$this->assertEquals(5, stored_progress_bar::get_timeout());
|
||||
$progresspollinterval = rand(10, 20);
|
||||
$CFG->progresspollinterval = $progresspollinterval;
|
||||
$this->assertEquals($progresspollinterval, stored_progress_bar::get_timeout());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling export_for_template() returns the current values for rendering the progress bar.
|
||||
*/
|
||||
public function test_export_for_template(): void {
|
||||
global $PAGE;
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
$timenow = time();
|
||||
$progress = $generator->create_stored_progress(
|
||||
'foo_bar_123',
|
||||
$timenow - 10,
|
||||
$timenow - 1,
|
||||
50.00,
|
||||
'error',
|
||||
true
|
||||
);
|
||||
|
||||
$progressbar = stored_progress_bar::get_by_id($progress->id);
|
||||
|
||||
$templatecontext = $progressbar->export_for_template($PAGE->get_renderer('core'));
|
||||
|
||||
$this->assertEquals([
|
||||
'id' => $progress->id,
|
||||
'idnumber' => $progress->idnumber,
|
||||
'width' => 0,
|
||||
'class' => 'stored-progress-bar',
|
||||
'value' => $progress->percentcompleted,
|
||||
'message' => $progress->message,
|
||||
'error' => $progress->haserrored,
|
||||
], $templatecontext);
|
||||
}
|
||||
}
|
51
lib/tests/task/stored_progress_bar_cleanup_task_test.php
Normal file
51
lib/tests/task/stored_progress_bar_cleanup_task_test.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle 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 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\task;
|
||||
|
||||
use core\output\stored_progress_bar;
|
||||
|
||||
/**
|
||||
* Unit tests for stored_progress_bar_cleanup
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core\task\stored_progress_bar_cleanup_task_test
|
||||
*/
|
||||
final class stored_progress_bar_cleanup_task_test extends \advanced_testcase {
|
||||
/**
|
||||
* Clean up stored_progress records that were last updated over 24 hours ago.
|
||||
*/
|
||||
public function test_execute(): void {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
$neverupdated = $generator->create_stored_progress();
|
||||
$updatednow = $generator->create_stored_progress(lastupdate: time());
|
||||
$updated23hours = $generator->create_stored_progress(lastupdate: time() - (HOURSECS * 23));
|
||||
$updated24hours = $generator->create_stored_progress(lastupdate: time() - DAYSECS - 1);
|
||||
|
||||
$task = new stored_progress_bar_cleanup_task();
|
||||
$this->expectOutputString('Deleted old stored_progress records' . PHP_EOL);
|
||||
$task->execute();
|
||||
|
||||
$this->assertNotNull(stored_progress_bar::get_by_id($neverupdated->id));
|
||||
$this->assertNotNull(stored_progress_bar::get_by_id($updatednow->id));
|
||||
$this->assertNotNull(stored_progress_bar::get_by_id($updated23hours->id));
|
||||
$this->assertNull(stored_progress_bar::get_by_id($updated24hours->id));
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2024071900.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2024071900.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '4.5dev (Build: 20240719)'; // Human-friendly version name
|
||||
|
Loading…
x
Reference in New Issue
Block a user