// 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/>.
/**
* This module updates the UI during an asynchronous
* backup or restore process.
*
* @module core_backup/async_backup
* @copyright 2018 Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.7
*/
define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates'],
function($, ajax, Str, notification, Templates) {
/**
* Module level constants.
*
* Using var instead of const as ES6 isn't fully supported yet.
*/
var STATUS_EXECUTING = 800;
var STATUS_FINISHED_ERR = 900;
var STATUS_FINISHED_OK = 1000;
/**
* Module level variables.
*/
var Asyncbackup = {};
var checkdelayoriginal = 15000; // This is the default time to use.
var checkdelay = 15000; // How often we should check for progress updates.
var checkdelaymultipler = 1.5; // If a request fails this multiplier will be used to increase the checkdelay value
var backupid; // The backup id to get the progress for.
var contextid; // The course this backup progress is for.
var restoreurl; // The URL to view course restores.
var typeid; // The type of operation backup or restore.
var backupintervalid; // The id of the setInterval function.
var allbackupintervalid; // The id of the setInterval function.
var allcopyintervalid; // The id of the setInterval function.
var timeout = 2000; // Timeout for ajax requests.
/**
* Helper function to update UI components.
*
* @param {string} backupid The id to match elements on.
* @param {string} type The type of operation, backup or restore.
* @param {number} percentage The completion percentage to apply.
*/
function updateElement(backupid, type, percentage) {
var percentagewidth = Math.round(percentage) + '%';
var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0];
var percentagetext = percentage.toFixed(2) + '%';
// Set progress bar percentage indicators
elementbar.setAttribute('aria-valuenow', percentagewidth);
elementbar.style.width = percentagewidth;
elementbar.innerHTML = percentagetext;
}
/**
* Updates the interval we use to check for backup progress.
*
* @param {Number} intervalid The id of the interval
* @param {Function} callback The function to use in setInterval
* @param {Number} value The specified interval (in milliseconds)
* @returns {Number}
*/
function updateInterval(intervalid, callback, value) {
clearInterval(intervalid);
return setInterval(callback, value);
}
/**
* Update backup table row when an async backup completes.
*
* @param {string} backupid The id to match elements on.
*/
function updateBackupTableRow(backupid) {
var statuscell = $('#' + backupid + '_bar').parent().parent();
var tablerow = statuscell.parent();
var cellsiblings = statuscell.siblings();
var timecell = cellsiblings[1];
var timevalue = $(timecell).text();
var filenamecell = cellsiblings[0];
var filename = $(filenamecell).text();
ajax.call([{
// Get the table data via webservice.
methodname: 'core_backup_get_async_backup_links_backup',
args: {
'filename': filename,
'contextid': contextid,
'backupid': backupid
},
}])[0].done(function(response) {
// We have the data now update the UI.
var context = {
filename: filename,
time: timevalue,
size: response.filesize,
fileurl: response.fileurl,
restoreurl: response.restoreurl
};
Templates.render('core/async_backup_progress_row', context).then(function(html, js) {
Templates.replaceNodeContents(tablerow, html, js);
return;
}).fail(function() {
notification.exception(new Error('Failed to load table row'));
return;
});
});
}
/**
* Update restore table row when an async restore completes.
*
* @param {string} backupid The id to match elements on.
*/
function updateRestoreTableRow(backupid) {
var statuscell = $('#' + backupid + '_bar').parent().parent();
var tablerow = statuscell.parent();
var cellsiblings = statuscell.siblings();
var coursecell = cellsiblings[0];
var timecell = cellsiblings[1];
var timevalue = $(timecell).text();
ajax.call([{
// Get the table data via webservice.
methodname: 'core_backup_get_async_backup_links_restore',
args: {
'backupid': backupid,
'contextid': contextid
},
}])[0].done(function(response) {
// We have the data now update the UI.
var resourcename = $(coursecell).text();
var context = {
resourcename: resourcename,
restoreurl: response.restoreurl,
time: timevalue
};
Templates.render('core/async_restore_progress_row', context).then(function(html, js) {
Templates.replaceNodeContents(tablerow, html, js);
return;
}).fail(function() {
notification.exception(new Error('Failed to load table row'));
return;
});
});
}
/**
* Update copy table row when an course copy completes.
*
* @param {string} backupid The id to match elements on.
*/
function updateCopyTableRow(backupid) {
var elementbar = document.querySelectorAll("[data-restoreid=" + CSS.escape(backupid) + "]")[0];
var restorecourse = elementbar.closest('tr').children[1];
var coursename = restorecourse.innerHTML;
var courselink = document.createElement('a');
var elementbarparent = elementbar.closest('td');
var operation = elementbarparent.previousElementSibling;
// Replace the prgress bar.
Str.get_string('complete').then(function(content) {
operation.innerHTML = content;
return;
}).catch(function() {
notification.exception(new Error('Failed to load string: complete'));
return;
});
Templates.render('core/async_copy_complete_cell', {}).then(function(html, js) {
Templates.replaceNodeContents(elementbarparent, html, js);
return;
}).fail(function() {
notification.exception(new Error('Failed to load table cell'));
return;
});
// Update the destination course name to a link to that course.
ajax.call([{
methodname: 'core_backup_get_async_backup_links_restore',
args: {
'backupid': backupid,
'contextid': 0
},
}])[0].done(function(response) {
courselink.setAttribute('href', response.restoreurl);
courselink.innerHTML = coursename;
restorecourse.innerHTML = null;
restorecourse.appendChild(courselink);
return;
}).fail(function() {
notification.exception(new Error('Failed to update table row'));
return;
});
}
/**
* Update the Moodle user interface with the progress of
* the backup process.
*
* @param {object} progress The progress and status of the process.
*/
function updateProgress(progress) {
var percentage = progress.progress * 100;
var type = 'backup';
var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0];
var elementstatus = $('#' + backupid + '_status');
var elementdetail = $('#' + backupid + '_detail');
var elementbutton = $('#' + backupid + '_button');
var stringRequests;
if (progress.status == STATUS_EXECUTING) {
// Process is in progress.
// Add in progress class color to bar.
elementbar.classList.add('bg-success');
updateElement(backupid, type, percentage);
// Change heading.
var strProcessing = 'async' + typeid + 'processing';
Str.get_string(strProcessing, 'backup').then(function(title) {
elementstatus.text(title);
return;
}).catch(function() {
notification.exception(new Error('Failed to load string: backup ' + strProcessing));
});
} else if (progress.status == STATUS_FINISHED_ERR) {
// Process completed with error.
// Add in fail class color to bar.
elementbar.classList.add('bg-danger');
// Remove in progress class color to bar.
elementbar.classList.remove('bg-success');
updateElement(backupid, type, 100);
// Change heading and text.
var strStatus = 'async' + typeid + 'error';
var strStatusDetail = 'async' + typeid + 'errordetail';
stringRequests = [
{key: strStatus, component: 'backup'},
{key: strStatusDetail, component: 'backup'}
];
Str.get_strings(stringRequests).then(function(strings) {
elementstatus.text(strings[0]);
elementdetail.text(strings[1]);
return;
})
.catch(function() {
notification.exception(new Error('Failed to load string'));
return;
});
$('.backup_progress').children('span').removeClass('backup_stage_current');
$('.backup_progress').children('span').last().addClass('backup_stage_current');
// Stop checking when we either have an error or a completion.
clearInterval(backupintervalid);
} else if (progress.status == STATUS_FINISHED_OK) {
// Process completed successfully.
// Add in progress class color to bar
elementbar.classList.add('bg-success');
updateElement(backupid, type, 100);
// Change heading and text
var strComplete = 'async' + typeid + 'complete';
Str.get_string(strComplete, 'backup').then(function(title) {
elementstatus.text(title);
return;
}).catch(function() {
notification.exception(new Error('Failed to load string: backup ' + strComplete));
});
if (typeid == 'restore') {
ajax.call([{
// Get the table data via webservice.
methodname: 'core_backup_get_async_backup_links_restore',
args: {
'backupid': backupid,
'contextid': contextid
},
}])[0].done(function(response) {
var strDetail = 'async' + typeid + 'completedetail';
var strButton = 'async' + typeid + 'completebutton';
var stringRequests = [
{key: strDetail, component: 'backup', param: response.restoreurl},
{key: strButton, component: 'backup'}
];
Str.get_strings(stringRequests).then(function(strings) {
elementdetail.html(strings[0]);
elementbutton.text(strings[1]);
elementbutton.attr('href', response.restoreurl);
return;
})
.catch(function() {
notification.exception(new Error('Failed to load string'));
return;
});
});
} else {
var strDetail = 'async' + typeid + 'completedetail';
var strButton = 'async' + typeid + 'completebutton';
stringRequests = [
{key: strDetail, component: 'backup', param: restoreurl},
{key: strButton, component: 'backup'}
];
Str.get_strings(stringRequests).then(function(strings) {
elementdetail.html(strings[0]);
elementbutton.text(strings[1]);
elementbutton.attr('href', restoreurl);
return;
})
.catch(function() {
notification.exception(new Error('Failed to load string'));
return;
});
}
$('.backup_progress').children('span').removeClass('backup_stage_current');
$('.backup_progress').children('span').last().addClass('backup_stage_current');
// Stop checking when we either have an error or a completion.
clearInterval(backupintervalid);
}
}
/**
* Update the Moodle user interface with the progress of
* all the pending processes for backup and restore operations.
*
* @param {object} progress The progress and status of the process.
*/
function updateProgressAll(progress) {
progress.forEach(function(element) {
var percentage = element.progress * 100;
var backupid = element.backupid;
var type = element.operation;
var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0];
if (element.status == STATUS_EXECUTING) {
// Process is in element.
// Add in element class color to bar
elementbar.classList.add('bg-success');
updateElement(backupid, type, percentage);
} else if (element.status == STATUS_FINISHED_ERR) {
// Process completed with error.
// Add in fail class color to bar
elementbar.classList.add('bg-danger');
elementbar.classList.add('complete');
// Remove in element class color to bar
elementbar.classList.remove('bg-success');
updateElement(backupid, type, 100);
} else if (element.status == STATUS_FINISHED_OK) {
// Process completed successfully.
// Add in element class color to bar
elementbar.classList.add('bg-success');
elementbar.classList.add('complete');
updateElement(backupid, type, 100);
// We have a successful backup. Update the UI with download and file details.
if (type == 'backup') {
updateBackupTableRow(backupid);
} else {
updateRestoreTableRow(backupid);
}
}
});
}
/**
* Update the Moodle user interface with the progress of
* all the pending processes for copy operations.
*
* @param {object} progress The progress and status of the process.
*/
function updateProgressCopy(progress) {
progress.forEach(function(element) {
var percentage = element.progress * 100;
var backupid = element.backupid;
var type = element.operation;
var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0];
if (type == 'restore') {
let restorecell = elementbar.closest('tr').children[3];
Str.get_string('restore').then(function(content) {
restorecell.innerHTML = content;
return;
}).catch(function() {
notification.exception(new Error('Failed to load string: restore'));
});
}
if (element.status == STATUS_EXECUTING) {
// Process is in element.
// Add in element class color to bar
elementbar.classList.add('bg-success');
updateElement(backupid, type, percentage);
} else if (element.status == STATUS_FINISHED_ERR) {
// Process completed with error.
// Add in fail class color to bar
elementbar.classList.add('bg-danger');
elementbar.classList.add('complete');
// Remove in element class color to bar
elementbar.classList.remove('bg-success');
updateElement(backupid, type, 100);
} else if ((element.status == STATUS_FINISHED_OK) && (type == 'restore')) {
// Process completed successfully.
// Add in element class color to bar
elementbar.classList.add('bg-success');
elementbar.classList.add('complete');
updateElement(backupid, type, 100);
// We have a successful copy. Update the UI link to copied course.
updateCopyTableRow(backupid);
}
});
}
/**
* Get the progress of the backup process via ajax.
*/
function getBackupProgress() {
ajax.call([{
// Get the backup progress via webservice.
methodname: 'core_backup_get_async_backup_progress',
args: {
'backupids': [backupid],
'contextid': contextid
},
}], true, true, false, timeout)[0].done(function(response) {
// We have the progress now update the UI.
updateProgress(response[0]);
checkdelay = checkdelayoriginal;
backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelayoriginal);
}).fail(function() {
checkdelay = checkdelay * checkdelaymultipler;
backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelay);
});
}
/**
* Get the progress of all backup processes via ajax.
*/
function getAllBackupProgress() {
var backupids = [];
var progressbars = $('.progress').find('.progress-bar').not('.complete');
progressbars.each(function() {
backupids.push((this.id).substring(0, 32));
});
if (backupids.length > 0) {
ajax.call([{
// Get the backup progress via webservice.
methodname: 'core_backup_get_async_backup_progress',
args: {
'backupids': backupids,
'contextid': contextid
},
}], true, true, false, timeout)[0].done(function(response) {
updateProgressAll(response);
checkdelay = checkdelayoriginal;
allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelayoriginal);
}).fail(function() {
checkdelay = checkdelay * checkdelaymultipler;
allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelay);
});
} else {
clearInterval(allbackupintervalid); // No more progress bars to update, stop checking.
}
}
/**
* Get the progress of all copy processes via ajax.
*/
function getAllCopyProgress() {
var copyids = [];
var progressbars = $('.progress').find('.progress-bar[data-operation][data-backupid][data-restoreid]').not('.complete');
progressbars.each(function() {
let progressvars = {
'backupid': this.dataset.backupid,
'restoreid': this.dataset.restoreid,
'operation': this.dataset.operation,
};
copyids.push(progressvars);
});
if (copyids.length > 0) {
ajax.call([{
// Get the copy progress via webservice.
methodname: 'core_backup_get_copy_progress',
args: {
'copies': copyids
},
}], true, true, false, timeout)[0].done(function(response) {
updateProgressCopy(response);
checkdelay = checkdelayoriginal;
allcopyintervalid = updateInterval(allcopyintervalid, getAllCopyProgress, checkdelayoriginal);
}).fail(function() {
checkdelay = checkdelay * checkdelaymultipler;
allcopyintervalid = updateInterval(allcopyintervalid, getAllCopyProgress, checkdelay);
});
} else {
clearInterval(allcopyintervalid); // No more progress bars to update, stop checking.
}
}
/**
* Get status updates for all backups.
*
* @public
* @param {number} context The context id.
*/
Asyncbackup.asyncBackupAllStatus = function(context) {
contextid = context;
allbackupintervalid = setInterval(getAllBackupProgress, checkdelay);
};
/**
* Get status updates for all course copies.
*
* @public
*/
Asyncbackup.asyncCopyAllStatus = function() {
allcopyintervalid = setInterval(getAllCopyProgress, checkdelay);
};
/**
* Get status updates for backup.
*
* @public
* @param {string} backup The backup record id.
* @param {number} context The context id.
* @param {string} restore The restore link.
* @param {string} type The operation type (backup or restore).
*/
Asyncbackup.asyncBackupStatus = function(backup, context, restore, type) {
backupid = backup;
contextid = context;
restoreurl = restore;
if (type == 'backup') {
typeid = 'backup';
} else {
typeid = 'restore';
}
// Remove the links from the progress bar, no going back now.
$('.backup_progress').children('a').removeAttr('href');
// Periodically check for progress updates and update the UI as required.
backupintervalid = setInterval(getBackupProgress, checkdelay);
};
return Asyncbackup;
});