This commit is contained in:
Jun Pataleta 2024-07-23 17:59:30 +08:00
commit bc5a65e444
No known key found for this signature in database
GPG Key ID: F83510526D99E2C7
29 changed files with 1404 additions and 60 deletions

View File

@ -0,0 +1,5 @@
issueNumber: MDL-70854
notes:
core:
- message: Added stored progress bars
type: improved

View File

@ -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 '-';
}
}
}

View File

@ -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"

View File

@ -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!!!

View File

@ -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
View 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

View 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"}

View 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;
}
};

View File

@ -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;

View 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;
}
}

View File

@ -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]));
}
/**

View File

@ -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 = '';

View File

@ -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.

View 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;
}
}

View File

@ -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");
}
}

View 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');
}
}

View 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();
}
}

View File

@ -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>

View File

@ -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(

View File

@ -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,
],
);

View File

@ -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;
}

View File

@ -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', {

View File

@ -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">&nbsp;</div>
<div id="{{idnumber}}_status" class="text-truncate">&nbsp;</div>
</div>
<div class="text-right pl-3" style="flex: 0 0 content">
<span id="{{id}}_estimate" class="">&nbsp;</span>
<span id="{{id}}_percentage" class="d-inline-block" style="width: 3em">0%</span>
<span id="{{idnumber}}_estimate" class="">&nbsp;</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');

View File

@ -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.
*

View File

@ -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, '');
}
/**

View 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.
}
}

View 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);
}
}

View 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));
}
}

View File

@ -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