diff --git a/backup/backup.class.php b/backup/backup.class.php index 260058d650e30..034c1bf46f5da 100644 --- a/backup/backup.class.php +++ b/backup/backup.class.php @@ -85,6 +85,15 @@ abstract class backup implements checksumable { */ const MODE_ASYNC = 70; + /** + * This mode is for course copies. + * It is similar to async, but identifies back up and restore tasks + * as course copies. + * + * These copies will run via adhoc scheduled tasks. + */ + const MODE_COPY = 80; + // Target (new/existing/current/adding/deleting) const TARGET_CURRENT_DELETING = 0; const TARGET_CURRENT_ADDING = 1; diff --git a/backup/controller/backup_controller.class.php b/backup/controller/backup_controller.class.php index c2c7fdd3dfc9d..781e8cbcd69d3 100644 --- a/backup/controller/backup_controller.class.php +++ b/backup/controller/backup_controller.class.php @@ -70,6 +70,12 @@ class backup_controller extends base_controller { protected $checksum; // Cache @checksumable results for lighter @is_checksum_correct() uses + /** + * The role ids to keep in a copy operation. + * @var array + */ + protected $keptroles = array(); + /** * Constructor for the backup controller class. * @@ -97,7 +103,7 @@ public function __construct($type, $id, $format, $interactive, $mode, $userid, $ $this->checksum = ''; // Set execution based on backup mode. - if ($mode == backup::MODE_ASYNC) { + if ($mode == backup::MODE_ASYNC || $mode == backup::MODE_COPY) { $this->execution = backup::EXECUTION_DELAYED; } else { $this->execution = backup::EXECUTION_INMEDIATE; @@ -291,7 +297,7 @@ protected function get_include_files_default() : bool { // When a backup is intended for the same site, we don't need to include the files. // Note, this setting is only used for duplication of an entire course. - if ($this->get_mode() === backup::MODE_SAMESITE) { + if ($this->get_mode() === backup::MODE_SAMESITE || $this->get_mode() === backup::MODE_COPY) { $includefiles = false; } @@ -352,6 +358,22 @@ public function get_plan() { return $this->plan; } + /** + * Sets the user roles that should be kept in the destination course + * for a course copy operation. + * + * @param array $roleids + * @throws backup_controller_exception + */ + public function set_kept_roles(array $roleids): void { + // Only allow of keeping user roles when controller is in copy mode. + if ($this->mode != backup::MODE_COPY) { + throw new backup_controller_exception('cannot_set_keep_roles_wrong_mode'); + } + + $this->keptroles = $roleids; + } + /** * Executes the backup * @return void Throws and exception of completes @@ -379,6 +401,12 @@ public function execute_plan() { $this->log('notifying plan about excluded activities by type', backup::LOG_DEBUG); $this->plan->set_excluding_activities(); } + + // Handle copy operation specific settings. + if ($this->mode == backup::MODE_COPY) { + $this->plan->set_kept_roles($this->keptroles); + } + return $this->plan->execute(); } diff --git a/backup/controller/base_controller.class.php b/backup/controller/base_controller.class.php index 32aa06cc5b474..8e8d0d10d2b41 100644 --- a/backup/controller/base_controller.class.php +++ b/backup/controller/base_controller.class.php @@ -36,6 +36,13 @@ abstract class base_controller extends backup implements loggable { /** @var bool Whether this backup should release the session. */ protected $releasesession = backup::RELEASESESSION_NO; + /** + * Holds the relevant destination information for course copy operations. + * + * @var \stdClass. + */ + protected $copy; + /** * Gets the progress reporter, which can be used to report progress within * the backup or restore process. @@ -95,4 +102,30 @@ public function log($message, $level, $a = null, $depth = null, $display = false public function get_releasesession() { return $this->releasesession; } + + /** + * Store extra data for course copy operations. + * + * For a course copying these is data required to be passed to the restore step. + * We store this data in its own section of the backup controller + * + * @param \stdClass $data The course copy data. + * @throws backup_controller_exception + */ + public function set_copy(\stdClass $data): void { + // Only allow setting of copy data when controller is in copy mode. + if ($this->mode != backup::MODE_COPY) { + throw new backup_controller_exception('cannot_set_copy_vars_wrong_mode'); + } + $this->copy = $data; + } + + /** + * Get the course copy data. + * + * @return \stdClass + */ + public function get_copy(): \stdClass { + return $this->copy; + } } diff --git a/backup/controller/restore_controller.class.php b/backup/controller/restore_controller.class.php index cf37e552dc517..39c411606a2be 100644 --- a/backup/controller/restore_controller.class.php +++ b/backup/controller/restore_controller.class.php @@ -116,7 +116,7 @@ public function __construct($tempdir, $courseid, $interactive, $mode, $userid, $ $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->restoreid); // Set execution based on backup mode. - if ($mode == backup::MODE_ASYNC) { + if ($mode == backup::MODE_ASYNC || $mode == backup::MODE_COPY) { $this->execution = backup::EXECUTION_DELAYED; } else { $this->execution = backup::EXECUTION_INMEDIATE; @@ -529,6 +529,30 @@ public function convert() { $this->progress->end_progress(); } + /** + * Do the necessary copy preparation actions. + * This method should only be called once the backup of a copy operation is completed. + * + * @throws restore_controller_exception + */ + public function prepare_copy(): void { + // Check that we are in the correct mode. + if ($this->mode != backup::MODE_COPY) { + throw new restore_controller_exception('cannot_prepare_copy_wrong_mode'); + } + + $this->progress->start_progress('Prepare Copy'); + + // If no exceptions were thrown, then we are in the proper format. + $this->format = backup::FORMAT_MOODLE; + + // Load plan, apply security and set status based on interactivity. + $this->load_plan(); + + $this->set_status(backup::STATUS_NEED_PRECHECK); + $this->progress->end_progress(); + } + // Protected API starts here protected function calculate_restoreid() { diff --git a/backup/controller/tests/controller_test.php b/backup/controller/tests/controller_test.php index 0488185998b0c..f638be031aa29 100644 --- a/backup/controller/tests/controller_test.php +++ b/backup/controller/tests/controller_test.php @@ -60,6 +60,20 @@ protected function setUp() { $CFG->backup_file_logger_level_extra = backup::LOG_NONE; } + /** + * Test set copy method. + */ + public function test_base_controller_set_copy() { + $this->expectException(\backup_controller_exception::class); + $copy = new \stdClass(); + + // Set up controller as a non-copy operation. + $bc = new \backup_controller(backup::TYPE_1COURSE, $this->courseid, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid, backup::RELEASESESSION_YES); + + $bc->set_copy($copy); + } + /* * test base_setting class */ @@ -103,6 +117,19 @@ public function test_backup_controller_include_files() { $this->assertEquals($bc->get_include_files(), 0); } + /** + * Test set kept roles method. + */ + public function test_backup_controller_set_kept_roles() { + $this->expectException(\backup_controller_exception::class); + + // Set up controller as a non-copy operation. + $bc = new \backup_controller(backup::TYPE_1COURSE, $this->courseid, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid, backup::RELEASESESSION_YES); + + $bc->set_kept_roles(array(1, 3, 5)); + } + /** * Tests the restore_controller. */ @@ -150,6 +177,29 @@ public function test_restore_controller_is_executing() { $this->assertTrue($alltrue); } + /** + * Test prepare copy method. + */ + public function test_restore_controller_prepare_copy() { + $this->expectException(\restore_controller_exception::class); + + global $CFG; + + // Make a backup. + make_backup_temp_directory(''); + $bc = new backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_IMPORT, $this->userid); + $backupid = $bc->get_backupid(); + $bc->execute_plan(); + $bc->destroy(); + + // Set up restore. + $rc = new restore_controller($backupid, $this->courseid, + backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $this->userid, + backup::TARGET_EXISTING_ADDING); + $rc->prepare_copy(); + } + /** * Test restore of deadlock causing backup. */ diff --git a/backup/copy.php b/backup/copy.php new file mode 100644 index 0000000000000..6243090dbe536 --- /dev/null +++ b/backup/copy.php @@ -0,0 +1,95 @@ +. + +/** + * This script is used to configure and execute the course copy proccess. + * + * @package core_backup + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); + +defined('MOODLE_INTERNAL') || die(); + +$courseid = required_param('id', PARAM_INT); +$returnto = optional_param('returnto', 'course', PARAM_ALPHANUM); // Generic navigation return page switch. +$returnurl = optional_param('returnurl', '', PARAM_LOCALURL); // A return URL. returnto must also be set to 'url'. + +$url = new moodle_url('/backup/copy.php', array('id' => $courseid)); +$course = get_course($courseid); +$coursecontext = context_course::instance($course->id); + +// Security and access checks. +require_login($course, false); +$copycaps = \core_course\management\helper::get_course_copy_capabilities(); +require_all_capabilities($copycaps, $coursecontext); + +if ($returnurl != '') { + $returnurl = new moodle_url($returnurl); +} else if ($returnto == 'catmanage') { + // Redirect to category management page. + $returnurl = new moodle_url('/course/management.php', array('categoryid' => $course->category)); +} else { + // Redirect back to course page if we came from there. + $returnurl = new moodle_url('/course/view.php', array('id' => $courseid)); +} + +// Setup the page. +$title = get_string('copycoursetitle', 'backup', $course->shortname); +$heading = get_string('copycourseheading', 'backup'); +$PAGE->set_url($url); +$PAGE->set_pagelayout('admin'); +$PAGE->set_title($title); +$PAGE->set_heading($heading); + +// Get data ready for mform. +$mform = new \core_backup\output\copy_form( + $url, + array('course' => $course, 'returnto' => $returnto, 'returnurl' => $returnurl)); + +if ($mform->is_cancelled()) { + // The form has been cancelled, take them back to what ever the return to is. + redirect($returnurl); + +} else if ($mdata = $mform->get_data()) { + + // Process the form and create the copy task. + $backupcopy = new \core_backup\copy\copy($mdata); + $backupcopy->create_copy(); + + if (!empty($mdata->submitdisplay)) { + // Redirect to the copy progress overview. + $progressurl = new moodle_url('/backup/copyprogress.php', array('id' => $courseid)); + redirect($progressurl); + } else { + // Redirect to the course view page. + $coursesurl = new moodle_url('/course/view.php', array('id' => $courseid)); + redirect($coursesurl); + } + +} else { + // This branch is executed if the form is submitted but the data doesn't validate, + // or on the first display of the form. + + // Build the page output. + echo $OUTPUT->header(); + echo $OUTPUT->heading($title); + $mform->display(); + echo $OUTPUT->footer(); +} diff --git a/backup/copyprogress.php b/backup/copyprogress.php new file mode 100644 index 0000000000000..d43db0251e041 --- /dev/null +++ b/backup/copyprogress.php @@ -0,0 +1,59 @@ +. + +/** + * This script is used to configure and execute the course copy proccess. + * + * @package core_backup + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + +defined('MOODLE_INTERNAL') || die(); + +$courseid = required_param('id', PARAM_INT); + +$url = new moodle_url('/backup/copyprogress.php', array('id' => $courseid)); +$course = get_course($courseid); +$coursecontext = context_course::instance($course->id); + +// Security and access checks. +require_login($course, false); +$copycaps = \core_course\management\helper::get_course_copy_capabilities(); +require_all_capabilities($copycaps, $coursecontext); + +// Setup the page. +$title = get_string('copyprogresstitle', 'backup'); +$PAGE->set_url($url); +$PAGE->set_pagelayout('admin'); +$PAGE->set_title($title); +$PAGE->set_heading($title); +$PAGE->requires->js_call_amd('core_backup/async_backup', 'asyncCopyAllStatus'); + +// Build the page output. +echo $OUTPUT->header(); +echo $OUTPUT->heading_with_help(get_string('copyprogressheading', 'backup'), 'copyprogressheading', 'backup'); +echo $OUTPUT->container_start(); +$renderer = $PAGE->get_renderer('core', 'backup'); +echo $renderer->copy_progress_viewer($USER->id, $courseid); +echo $OUTPUT->container_end(); + +echo $OUTPUT->footer(); diff --git a/backup/externallib.php b/backup/externallib.php index dd7091025a379..e05b9db3d98f2 100644 --- a/backup/externallib.php +++ b/backup/externallib.php @@ -28,6 +28,7 @@ require_once("$CFG->libdir/externallib.php"); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Backup external functions. @@ -67,10 +68,6 @@ public static function get_async_backup_progress_parameters() { * @since Moodle 3.7 */ public static function get_async_backup_progress($backupids, $contextid) { - global $CFG; - require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); - require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); - // Release session lock. \core\session\manager::write_close(); @@ -224,7 +221,12 @@ public static function get_async_backup_links_restore($backupid, $contextid) { ); // Context validation. - $context = context::instance_by_id($contextid); + if ($contextid == 0) { + $copyrec = \async_helper::get_backup_record($backupid); + $context = context_course::instance($copyrec->itemid); + } else { + $context = context::instance_by_id($contextid); + } self::validate_context($context); require_capability('moodle/restore:restorecourse', $context); @@ -245,4 +247,163 @@ public static function get_async_backup_links_restore_returns() { 'restoreurl' => new external_value(PARAM_URL, 'Restore url'), ), 'Table row data.'); } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 3.9 + */ + public static function get_copy_progress_parameters() { + return new external_function_parameters( + array( + 'copies' => new external_multiple_structure( + new external_single_structure( + array( + 'backupid' => new external_value(PARAM_ALPHANUM, 'Backup id'), + 'restoreid' => new external_value(PARAM_ALPHANUM, 'Restore id'), + 'operation' => new external_value(PARAM_ALPHANUM, 'Operation type'), + ), 'Copy data' + ), 'Copy data' + ), + ) + ); + } + + /** + * Get the data to be used when generating the table row for a course copy, + * the table row updates via ajax when copy is complete. + * + * @param array $copies Array of ids. + * @return array $results The array of results. + * @since Moodle 3.9 + */ + public static function get_copy_progress($copies) { + // Release session lock. + \core\session\manager::write_close(); + + // Parameter validation. + self::validate_parameters( + self::get_copy_progress_parameters(), + array('copies' => $copies) + ); + + $results = array(); + + foreach ($copies as $copy) { + + if ($copy['operation'] == \backup::OPERATION_BACKUP) { + $copyid = $copy['backupid']; + } else { + $copyid = $copy['restoreid']; + } + + $copyrec = \async_helper::get_backup_record($copyid); + $context = context_course::instance($copyrec->itemid); + self::validate_context($context); + + $copycaps = \core_course\management\helper::get_course_copy_capabilities(); + require_all_capabilities($copycaps, $context); + + if ($copy['operation'] == \backup::OPERATION_BACKUP) { + $result = \backup_controller_dbops::get_progress($copyid); + if ($result['status'] == \backup::STATUS_FINISHED_OK) { + $copyid = $copy['restoreid']; + } + } + + $results[] = \backup_controller_dbops::get_progress($copyid); + } + + return $results; + } + + /** + * Returns description of method result value. + * + * @return external_description + * @since Moodle 3.9 + */ + public static function get_copy_progress_returns() { + return new external_multiple_structure( + new external_single_structure( + array( + 'status' => new external_value(PARAM_INT, 'Copy Status'), + 'progress' => new external_value(PARAM_FLOAT, 'Copy progress'), + 'backupid' => new external_value(PARAM_ALPHANUM, 'Copy id'), + 'operation' => new external_value(PARAM_ALPHANUM, 'Operation type'), + ), 'Copy completion status' + ), 'Copy data' + ); + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 3.9 + */ + public static function submit_copy_form_parameters() { + return new external_function_parameters( + array( + 'jsonformdata' => new external_value(PARAM_RAW, 'The data from the create copy form, encoded as a json array') + ) + ); + } + + /** + * Submit the course group form. + * + * @param string $jsonformdata The data from the form, encoded as a json array. + * @return int new group id. + */ + public static function submit_copy_form($jsonformdata) { + + // Release session lock. + \core\session\manager::write_close(); + + // We always must pass webservice params through validate_parameters. + $params = self::validate_parameters( + self::submit_copy_form_parameters(), + array('jsonformdata' => $jsonformdata) + ); + + $formdata = json_decode($params['jsonformdata']); + + $data = array(); + parse_str($formdata, $data); + + $context = context_course::instance($data['courseid']); + self::validate_context($context); + $copycaps = \core_course\management\helper::get_course_copy_capabilities(); + require_all_capabilities($copycaps, $context); + + // Submit the form data. + $course = get_course($data['courseid']); + $mform = new \core_backup\output\copy_form( + null, + array('course' => $course, 'returnto' => '', 'returnurl' => ''), + 'post', '', ['class' => 'ignoredirty'], true, $data); + $mdata = $mform->get_data(); + + if ($mdata) { + // Create the copy task. + $backupcopy = new \core_backup\copy\copy($mdata); + $copyids = $backupcopy->create_copy(); + } else { + throw new moodle_exception('copyformfail', 'backup'); + } + + return json_encode($copyids); + } + + /** + * Returns description of method result value. + * + * @return external_description + * @since Moodle 3.9 + */ + public static function submit_copy_form_returns() { + return new external_value(PARAM_RAW, 'JSON response.'); + } } diff --git a/backup/moodle2/backup_final_task.class.php b/backup/moodle2/backup_final_task.class.php index 6f69c59f2b99b..affe6bf30c985 100644 --- a/backup/moodle2/backup_final_task.class.php +++ b/backup/moodle2/backup_final_task.class.php @@ -69,14 +69,14 @@ public function build() { // This step also ensures that the contexts for all the users exist, so next // step can be safely executed (join between users and contexts) // Not executed if backup is without users of anonymized - if ($this->get_setting_value('users') && !$this->get_setting_value('anonymize')) { + if (($this->get_setting_value('users') || !empty($this->get_kept_roles())) && !$this->get_setting_value('anonymize')) { $this->add_step(new backup_annotate_all_user_files('user_files')); } // Generate the users file (conditionally) with the final annotated users // including custom profile fields, preferences, tags, role assignments and // overrides - if ($this->get_setting_value('users')) { + if ($this->get_setting_value('users') || !empty($this->get_kept_roles())) { $this->add_step(new backup_users_structure_step('users', 'users.xml')); } diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index ac8c91711e3fa..b62fc70c9de4a 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -510,9 +510,11 @@ protected function execute_condition() { } protected function define_structure() { + global $DB; // To know if we are including users $users = $this->get_setting_value('users'); + $keptroles = $this->task->get_kept_roles(); // Define each element separated @@ -545,10 +547,28 @@ protected function define_structure() { // Define sources - the instances are restored using the same sortorder, we do not need to store it in xml and deal with it afterwards. $enrol->set_source_table('enrol', array('courseid' => backup::VAR_COURSEID), 'sortorder ASC'); - // User enrolments only added only if users included - if ($users) { + // User enrolments only added only if users included. + if (empty($keptroles) && $users) { $enrolment->set_source_table('user_enrolments', array('enrolid' => backup::VAR_PARENTID)); $enrolment->annotate_ids('user', 'userid'); + } else if (!empty($keptroles)) { + list($insql, $inparams) = $DB->get_in_or_equal($keptroles); + $params = array( + backup::VAR_CONTEXTID, + backup::VAR_PARENTID + ); + foreach ($inparams as $inparam) { + $params[] = backup_helper::is_sqlparam($inparam); + } + $enrolment->set_source_sql( + "SELECT ue.* + FROM {user_enrolments} ue + INNER JOIN {role_assignments} ra ON ue.userid = ra.userid + WHERE ra.contextid = ? + AND ue.enrolid = ? + AND ra.roleid $insql", + $params); + $enrolment->annotate_ids('user', 'userid'); } $enrol->annotate_ids('role', 'roleid'); @@ -1451,7 +1471,6 @@ protected function define_structure() { // Define id annotations (as final) $override->annotate_ids('rolefinal', 'roleid'); } - // Return root element (users) return $users; } diff --git a/backup/tests/course_copy_test.php b/backup/tests/course_copy_test.php new file mode 100644 index 0000000000000..906dbfe51c4b2 --- /dev/null +++ b/backup/tests/course_copy_test.php @@ -0,0 +1,636 @@ +. + +/** + * Course copy tests. + * + * @package core_backup + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); +require_once($CFG->libdir . '/completionlib.php'); + +/** + * Course copy tests. + * + * @package core_backup + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_backup_course_copy_testcase extends advanced_testcase { + + /** + * + * @var \stdClass Course used for testing. + */ + protected $course; + + /** + * + * @var int User used to perform backups. + */ + protected $userid; + + /** + * + * @var array Ids of users in test course. + */ + protected $courseusers; + + /** + * + * @var array Names of the created activities. + */ + protected $activitynames; + + /** + * Set up tasks for all tests. + */ + protected function setUp() { + global $DB, $CFG, $USER; + + $this->resetAfterTest(true); + + $CFG->enableavailability = true; + $CFG->enablecompletion = true; + + // Create a course with some availability data set. + $generator = $this->getDataGenerator(); + $course = $generator->create_course( + array('format' => 'topics', 'numsections' => 3, + 'enablecompletion' => COMPLETION_ENABLED), + array('createsections' => true)); + $forum = $generator->create_module('forum', array( + 'course' => $course->id)); + $forum2 = $generator->create_module('forum', array( + 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); + + // We need a grade, easiest is to add an assignment. + $assignrow = $generator->create_module('assign', array( + 'course' => $course->id)); + $assign = new assign(context_module::instance($assignrow->cmid), false, false); + $item = $assign->get_grade_item(); + + // Make a test grouping as well. + $grouping = $generator->create_grouping(array('courseid' => $course->id, + 'name' => 'Grouping!')); + + // Create some users. + $user1 = $generator->create_user(); + $user2 = $generator->create_user(); + $user3 = $generator->create_user(); + $user4 = $generator->create_user(); + $this->courseusers = array( + $user1->id, $user2->id, $user3->id, $user4->id + ); + + // Enrol users into the course. + $generator->enrol_user($user1->id, $course->id, 'student'); + $generator->enrol_user($user2->id, $course->id, 'editingteacher'); + $generator->enrol_user($user3->id, $course->id, 'manager'); + $generator->enrol_user($user4->id, $course->id, 'editingteacher'); + $generator->enrol_user($user4->id, $course->id, 'manager'); + + $availability = '{"op":"|","show":false,"c":[' . + '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . + '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . + '{"type":"grouping","id":' . $grouping->id . '}' . + ']}'; + $DB->set_field('course_modules', 'availability', $availability, array( + 'id' => $forum->cmid)); + $DB->set_field('course_sections', 'availability', $availability, array( + 'course' => $course->id, 'section' => 1)); + + // Add some user data to the course. + $discussion = $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id, + 'forum' => $forum->id, 'userid' => $user1->id, 'timemodified' => time(), + 'name' => 'Frog']); + $generator->get_plugin_generator('mod_forum')->create_post(['discussion' => $discussion->id, 'userid' => $user1->id]); + + $this->course = $course; + $this->userid = $USER->id; // Admin. + $this->activitynames = array( + $forum->name, + $forum2->name, + $assignrow->name + ); + + // Set the user doing the backup to be a manager in the course. + // By default Managers can restore courses AND users, teachers can only do users. + $this->setUser($user3); + + // Disable all loggers. + $CFG->backup_error_log_logger_level = backup::LOG_NONE; + $CFG->backup_output_indented_logger_level = backup::LOG_NONE; + $CFG->backup_file_logger_level = backup::LOG_NONE; + $CFG->backup_database_logger_level = backup::LOG_NONE; + $CFG->backup_file_logger_level_extra = backup::LOG_NONE; + } + + /** + * Test creating a course copy. + */ + public function test_create_copy() { + + // Mock up the form data. + $formdata = new \stdClass; + $formdata->courseid = $this->course->id; + $formdata->fullname = 'foo'; + $formdata->shortname = 'bar'; + $formdata->category = 1; + $formdata->visible = 1; + $formdata->startdate = 1582376400; + $formdata->enddate = 0; + $formdata->idnumber = 123; + $formdata->userdata = 1; + $formdata->role_1 = 1; + $formdata->role_3 = 3; + $formdata->role_5 = 5; + + $coursecopy = new \core_backup\copy\copy($formdata); + $result = $coursecopy->create_copy(); + + // Load the controllers, to extract the data we need. + $bc = \backup_controller::load_controller($result['backupid']); + $rc = \restore_controller::load_controller($result['restoreid']); + + // Check the backup controller. + $this->assertEquals($result, $bc->get_copy()->copyids); + $this->assertEquals(backup::MODE_COPY, $bc->get_mode()); + $this->assertEquals($this->course->id, $bc->get_courseid()); + $this->assertEquals(backup::TYPE_1COURSE, $bc->get_type()); + + // Check the restore controller. + $newcourseid = $rc->get_courseid(); + $newcourse = get_course($newcourseid); + + $this->assertEquals($result, $rc->get_copy()->copyids); + $this->assertEquals(get_string('copyingcourse', 'backup'), $newcourse->fullname); + $this->assertEquals(get_string('copyingcourseshortname', 'backup'), $newcourse->shortname); + $this->assertEquals(backup::MODE_COPY, $rc->get_mode()); + $this->assertEquals($newcourseid, $rc->get_courseid()); + + // Check the created ad-hoc task. + $now = time(); + $task = \core\task\manager::get_next_adhoc_task($now); + + $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); + $this->assertEquals($result, (array)$task->get_custom_data()); + $this->assertFalse($task->is_blocking()); + + \core\task\manager::adhoc_task_complete($task); + } + + /** + * Test getting the current copies. + */ + public function test_get_copies() { + global $USER; + + // Mock up the form data. + $formdata = new \stdClass; + $formdata->courseid = $this->course->id; + $formdata->fullname = 'foo'; + $formdata->shortname = 'bar'; + $formdata->category = 1; + $formdata->visible = 1; + $formdata->startdate = 1582376400; + $formdata->enddate = 0; + $formdata->idnumber = ''; + $formdata->userdata = 1; + $formdata->role_1 = 1; + $formdata->role_3 = 3; + $formdata->role_5 = 5; + + $formdata2 = clone($formdata); + $formdata2->shortname = 'tree'; + + // Create some copies. + $coursecopy = new \core_backup\copy\copy($formdata); + $result = $coursecopy->create_copy(); + + // Backup, awaiting. + $copies = \core_backup\copy\copy::get_copies($USER->id); + $this->assertEquals($result['backupid'], $copies[0]->backupid); + $this->assertEquals($result['restoreid'], $copies[0]->restoreid); + $this->assertEquals(\backup::STATUS_AWAITING, $copies[0]->status); + $this->assertEquals(\backup::OPERATION_BACKUP, $copies[0]->operation); + + $bc = \backup_controller::load_controller($result['backupid']); + + // Backup, in progress. + $bc->set_status(\backup::STATUS_EXECUTING); + $copies = \core_backup\copy\copy::get_copies($USER->id); + $this->assertEquals($result['backupid'], $copies[0]->backupid); + $this->assertEquals($result['restoreid'], $copies[0]->restoreid); + $this->assertEquals(\backup::STATUS_EXECUTING, $copies[0]->status); + $this->assertEquals(\backup::OPERATION_BACKUP, $copies[0]->operation); + + // Restore, ready to process. + $bc->set_status(\backup::STATUS_FINISHED_OK); + $copies = \core_backup\copy\copy::get_copies($USER->id); + $this->assertEquals($result['backupid'], $copies[0]->backupid); + $this->assertEquals($result['restoreid'], $copies[0]->restoreid); + $this->assertEquals(\backup::STATUS_REQUIRE_CONV, $copies[0]->status); + $this->assertEquals(\backup::OPERATION_RESTORE, $copies[0]->operation); + + // No records. + $bc->set_status(\backup::STATUS_FINISHED_ERR); + $copies = \core_backup\copy\copy::get_copies($USER->id); + $this->assertEmpty($copies); + + $coursecopy2 = new \core_backup\copy\copy($formdata2); + $result2 = $coursecopy2->create_copy(); + // Set the second copy to be complete. + $bc = \backup_controller::load_controller($result2['backupid']); + $bc->set_status(\backup::STATUS_FINISHED_OK); + // Set the restore to be finished. + $rc = \backup_controller::load_controller($result2['restoreid']); + $rc->set_status(\backup::STATUS_FINISHED_OK); + + // No records. + $copies = \core_backup\copy\copy::get_copies($USER->id); + $this->assertEmpty($copies); + } + + /** + * Test getting the current copies for specific course. + */ + public function test_get_copies_course() { + global $USER; + + // Mock up the form data. + $formdata = new \stdClass; + $formdata->courseid = $this->course->id; + $formdata->fullname = 'foo'; + $formdata->shortname = 'bar'; + $formdata->category = 1; + $formdata->visible = 1; + $formdata->startdate = 1582376400; + $formdata->enddate = 0; + $formdata->idnumber = ''; + $formdata->userdata = 1; + $formdata->role_1 = 1; + $formdata->role_3 = 3; + $formdata->role_5 = 5; + + // Create some copies. + $coursecopy = new \core_backup\copy\copy($formdata); + $coursecopy->create_copy(); + + // No copies match this course id. + $copies = \core_backup\copy\copy::get_copies($USER->id, ($this->course->id + 1)); + $this->assertEmpty($copies); + } + + /** + * Test getting the current copies if course has been deleted. + */ + public function test_get_copies_course_deleted() { + global $USER; + + // Mock up the form data. + $formdata = new \stdClass; + $formdata->courseid = $this->course->id; + $formdata->fullname = 'foo'; + $formdata->shortname = 'bar'; + $formdata->category = 1; + $formdata->visible = 1; + $formdata->startdate = 1582376400; + $formdata->enddate = 0; + $formdata->idnumber = ''; + $formdata->userdata = 1; + $formdata->role_1 = 1; + $formdata->role_3 = 3; + $formdata->role_5 = 5; + + // Create some copies. + $coursecopy = new \core_backup\copy\copy($formdata); + $coursecopy->create_copy(); + + delete_course($this->course->id, false); + + // No copies match this course id as it has been deleted. + $copies = \core_backup\copy\copy::get_copies($USER->id, ($this->course->id)); + $this->assertEmpty($copies); + } + + /* + * Test course copy. + */ + public function test_course_copy() { + global $DB; + + // Mock up the form data. + $formdata = new \stdClass; + $formdata->courseid = $this->course->id; + $formdata->fullname = 'copy course'; + $formdata->shortname = 'copy course short'; + $formdata->category = 1; + $formdata->visible = 0; + $formdata->startdate = 1582376400; + $formdata->enddate = 1582386400; + $formdata->idnumber = 123; + $formdata->userdata = 1; + $formdata->role_1 = 1; + $formdata->role_3 = 3; + $formdata->role_5 = 5; + + // Create the course copy records and associated ad-hoc task. + $coursecopy = new \core_backup\copy\copy($formdata); + $copyids = $coursecopy->create_copy(); + + $courseid = $this->course->id; + + // We are expecting trace output during this test. + $this->expectOutputRegex("/$courseid/"); + + // Execute adhoc task. + $now = time(); + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); + $task->execute(); + \core\task\manager::adhoc_task_complete($task); + + $postbackuprec = $DB->get_record('backup_controllers', array('backupid' => $copyids['backupid'])); + $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); + + // Check backup was completed successfully. + $this->assertEquals(backup::STATUS_FINISHED_OK, $postbackuprec->status); + $this->assertEquals(1.0, $postbackuprec->progress); + + // Check restore was completed successfully. + $this->assertEquals(backup::STATUS_FINISHED_OK, $postrestorerec->status); + $this->assertEquals(1.0, $postrestorerec->progress); + + // Check the restored course itself. + $coursecontext = context_course::instance($postrestorerec->itemid); + $users = get_enrolled_users($coursecontext); + + $modinfo = get_fast_modinfo($postrestorerec->itemid); + $forums = $modinfo->get_instances_of('forum'); + $forum = reset($forums); + $discussions = forum_get_discussions($forum); + $course = $modinfo->get_course(); + + $this->assertEquals($formdata->startdate, $course->startdate); + $this->assertEquals($formdata->enddate, $course->enddate); + $this->assertEquals('copy course', $course->fullname); + $this->assertEquals('copy course short', $course->shortname); + $this->assertEquals(0, $course->visible); + $this->assertEquals(123, $course->idnumber); + + foreach ($modinfo->get_cms() as $cm) { + $this->assertContains($cm->get_formatted_name(), $this->activitynames); + } + + foreach ($this->courseusers as $user) { + $this->assertEquals($user, $users[$user]->id); + } + + $this->assertEquals(count($this->courseusers), count($users)); + $this->assertEquals(2, count($discussions)); + } + + /* + * Test course copy, not including any users (or data). + */ + public function test_course_copy_no_users() { + global $DB; + + // Mock up the form data. + $formdata = new \stdClass; + $formdata->courseid = $this->course->id; + $formdata->fullname = 'copy course'; + $formdata->shortname = 'copy course short'; + $formdata->category = 1; + $formdata->visible = 0; + $formdata->startdate = 1582376400; + $formdata->enddate = 1582386400; + $formdata->idnumber = 123; + $formdata->userdata = 1; + $formdata->role_1 = 0; + $formdata->role_3 = 0; + $formdata->role_5 = 0; + + // Create the course copy records and associated ad-hoc task. + $coursecopy = new \core_backup\copy\copy($formdata); + $copyids = $coursecopy->create_copy(); + + $courseid = $this->course->id; + + // We are expecting trace output during this test. + $this->expectOutputRegex("/$courseid/"); + + // Execute adhoc task. + $now = time(); + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); + $task->execute(); + \core\task\manager::adhoc_task_complete($task); + + $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); + + // Check the restored course itself. + $coursecontext = context_course::instance($postrestorerec->itemid); + $users = get_enrolled_users($coursecontext); + + $modinfo = get_fast_modinfo($postrestorerec->itemid); + $forums = $modinfo->get_instances_of('forum'); + $forum = reset($forums); + $discussions = forum_get_discussions($forum); + $course = $modinfo->get_course(); + + $this->assertEquals($formdata->startdate, $course->startdate); + $this->assertEquals($formdata->enddate, $course->enddate); + $this->assertEquals('copy course', $course->fullname); + $this->assertEquals('copy course short', $course->shortname); + $this->assertEquals(0, $course->visible); + $this->assertEquals(123, $course->idnumber); + + foreach ($modinfo->get_cms() as $cm) { + $this->assertContains($cm->get_formatted_name(), $this->activitynames); + } + + // Should be no discussions as the user that made them wasn't included. + $this->assertEquals(0, count($discussions)); + + // There should only be one user in the new course, and that's the user who did the copy. + $this->assertEquals(1, count($users)); + $this->assertEquals($this->courseusers[2], $users[$this->courseusers[2]]->id); + + } + + /* + * Test course copy, including students and their data. + */ + public function test_course_copy_students_data() { + global $DB; + + // Mock up the form data. + $formdata = new \stdClass; + $formdata->courseid = $this->course->id; + $formdata->fullname = 'copy course'; + $formdata->shortname = 'copy course short'; + $formdata->category = 1; + $formdata->visible = 0; + $formdata->startdate = 1582376400; + $formdata->enddate = 1582386400; + $formdata->idnumber = 123; + $formdata->userdata = 1; + $formdata->role_1 = 0; + $formdata->role_3 = 0; + $formdata->role_5 = 5; + + // Create the course copy records and associated ad-hoc task. + $coursecopy = new \core_backup\copy\copy($formdata); + $copyids = $coursecopy->create_copy(); + + $courseid = $this->course->id; + + // We are expecting trace output during this test. + $this->expectOutputRegex("/$courseid/"); + + // Execute adhoc task. + $now = time(); + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); + $task->execute(); + \core\task\manager::adhoc_task_complete($task); + + $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); + + // Check the restored course itself. + $coursecontext = context_course::instance($postrestorerec->itemid); + $users = get_enrolled_users($coursecontext); + + $modinfo = get_fast_modinfo($postrestorerec->itemid); + $forums = $modinfo->get_instances_of('forum'); + $forum = reset($forums); + $discussions = forum_get_discussions($forum); + $course = $modinfo->get_course(); + + $this->assertEquals($formdata->startdate, $course->startdate); + $this->assertEquals($formdata->enddate, $course->enddate); + $this->assertEquals('copy course', $course->fullname); + $this->assertEquals('copy course short', $course->shortname); + $this->assertEquals(0, $course->visible); + $this->assertEquals(123, $course->idnumber); + + foreach ($modinfo->get_cms() as $cm) { + $this->assertContains($cm->get_formatted_name(), $this->activitynames); + } + + // Should be no discussions as the user that made them wasn't included. + $this->assertEquals(2, count($discussions)); + + // There should only be two users in the new course. The copier and one student. + $this->assertEquals(2, count($users)); + $this->assertEquals($this->courseusers[2], $users[$this->courseusers[2]]->id); + $this->assertEquals($this->courseusers[0], $users[$this->courseusers[0]]->id); + } + + /* + * Test course copy, not including any users (or data). + */ + public function test_course_copy_no_data() { + global $DB; + + // Mock up the form data. + $formdata = new \stdClass; + $formdata->courseid = $this->course->id; + $formdata->fullname = 'copy course'; + $formdata->shortname = 'copy course short'; + $formdata->category = 1; + $formdata->visible = 0; + $formdata->startdate = 1582376400; + $formdata->enddate = 1582386400; + $formdata->idnumber = 123; + $formdata->userdata = 0; + $formdata->role_1 = 1; + $formdata->role_3 = 3; + $formdata->role_5 = 5; + + // Create the course copy records and associated ad-hoc task. + $coursecopy = new \core_backup\copy\copy($formdata); + $copyids = $coursecopy->create_copy(); + + $courseid = $this->course->id; + + // We are expecting trace output during this test. + $this->expectOutputRegex("/$courseid/"); + + // Execute adhoc task. + $now = time(); + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); + $task->execute(); + \core\task\manager::adhoc_task_complete($task); + + $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); + + // Check the restored course itself. + $coursecontext = context_course::instance($postrestorerec->itemid); + $users = get_enrolled_users($coursecontext); + + get_fast_modinfo($postrestorerec->itemid, 0, true); + $modinfo = get_fast_modinfo($postrestorerec->itemid); + $forums = $modinfo->get_instances_of('forum'); + $forum = reset($forums); + $discussions = forum_get_discussions($forum); + $course = $modinfo->get_course(); + + $this->assertEquals($formdata->startdate, $course->startdate); + $this->assertEquals($formdata->enddate, $course->enddate); + $this->assertEquals('copy course', $course->fullname); + $this->assertEquals('copy course short', $course->shortname); + $this->assertEquals(0, $course->visible); + $this->assertEquals(123, $course->idnumber); + + foreach ($modinfo->get_cms() as $cm) { + $this->assertContains($cm->get_formatted_name(), $this->activitynames); + } + + // Should be no discussions as the user data wasn't included. + $this->assertEquals(0, count($discussions)); + + // There should only be all users in the new course. + $this->assertEquals(count($this->courseusers), count($users)); + } + + /* + * Test instantiation with incomplete formdata. + */ + public function test_malformed_instantiation() { + // Mock up the form data, missing things so we get an exception. + $formdata = new \stdClass; + $formdata->courseid = $this->course->id; + $formdata->fullname = 'copy course'; + $formdata->shortname = 'copy course short'; + $formdata->category = 1; + + // Expect and exception as form data is incomplete. + $this->expectException(\moodle_exception::class); + new \core_backup\copy\copy($formdata); + } +} \ No newline at end of file diff --git a/backup/tests/externallib_test.php b/backup/tests/externallib_test.php new file mode 100644 index 0000000000000..8cb6e4d3a6a73 --- /dev/null +++ b/backup/tests/externallib_test.php @@ -0,0 +1,182 @@ +. + +/** + * Backup webservice tests. + * + * @package core_backup + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); +require_once($CFG->dirroot . '/backup/externallib.php'); + +/** + * Backup webservice tests. + * + * @package core_backup + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_external_testcase extends externallib_advanced_testcase { + + /** + * Set up tasks for all tests. + */ + protected function setUp() { + global $CFG; + + $this->resetAfterTest(true); + + // Disable all loggers. + $CFG->backup_error_log_logger_level = backup::LOG_NONE; + $CFG->backup_output_indented_logger_level = backup::LOG_NONE; + $CFG->backup_file_logger_level = backup::LOG_NONE; + $CFG->backup_database_logger_level = backup::LOG_NONE; + $CFG->backup_file_logger_level_extra = backup::LOG_NONE; + } + + /** + * Test getting course copy progress. + */ + public function test_get_copy_progress() { + global $USER; + + $this->setAdminUser(); + + // Create a course with some availability data set. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $courseid = $course->id; + + // Mock up the form data for use in tests. + $formdata = new \stdClass; + $formdata->courseid = $courseid; + $formdata->fullname = 'foo'; + $formdata->shortname = 'bar'; + $formdata->category = 1; + $formdata->visible = 1; + $formdata->startdate = 1582376400; + $formdata->enddate = 0; + $formdata->idnumber = 123; + $formdata->userdata = 1; + $formdata->role_1 = 1; + $formdata->role_3 = 3; + $formdata->role_5 = 5; + + $coursecopy = new \core_backup\copy\copy($formdata); + $copydetails = $coursecopy->create_copy(); + $copydetails['operation'] = \backup::OPERATION_BACKUP; + + $params = array('copies' => $copydetails); + $returnvalue = core_backup_external::get_copy_progress($params); + + // We need to execute the return values cleaning process to simulate the web service server. + $returnvalue = external_api::clean_returnvalue(core_backup_external::get_copy_progress_returns(), $returnvalue); + + $this->assertEquals(\backup::STATUS_AWAITING, $returnvalue[0]['status']); + $this->assertEquals(0, $returnvalue[0]['progress']); + $this->assertEquals($copydetails['backupid'], $returnvalue[0]['backupid']); + $this->assertEquals(\backup::OPERATION_BACKUP, $returnvalue[0]['operation']); + + // We are expecting trace output during this test. + $this->expectOutputRegex("/$courseid/"); + + // Execute adhoc task and create the copy. + $now = time(); + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); + $task->execute(); + \core\task\manager::adhoc_task_complete($task); + + // Check the copy progress now. + $params = array('copies' => $copydetails); + $returnvalue = core_backup_external::get_copy_progress($params); + + $returnvalue = external_api::clean_returnvalue(core_backup_external::get_copy_progress_returns(), $returnvalue); + + $this->assertEquals(\backup::STATUS_FINISHED_OK, $returnvalue[0]['status']); + $this->assertEquals(1, $returnvalue[0]['progress']); + $this->assertEquals($copydetails['restoreid'], $returnvalue[0]['backupid']); + $this->assertEquals(\backup::OPERATION_RESTORE, $returnvalue[0]['operation']); + + } + + /** + * Test ajax submission of course copy process. + */ + public function test_submit_copy_form() { + global $DB; + + $this->setAdminUser(); + + // Create a course with some availability data set. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $courseid = $course->id; + + // Moodle form requires this for validation. + $sesskey = sesskey(); + $_POST['sesskey'] = $sesskey; + + // Mock up the form data for use in tests. + $formdata = new \stdClass; + $formdata->courseid = $courseid; + $formdata->returnto = ''; + $formdata->returnurl = ''; + $formdata->sesskey = $sesskey; + $formdata->_qf__core_backup_output_copy_form = 1; + $formdata->fullname = 'foo'; + $formdata->shortname = 'bar'; + $formdata->category = 1; + $formdata->visible = 1; + $formdata->startdate = array('day' => 5, 'month' => 5, 'year' => 2020, 'hour' => 0, 'minute' => 0); + $formdata->idnumber = 123; + $formdata->userdata = 1; + $formdata->role_1 = 1; + $formdata->role_3 = 3; + $formdata->role_5 = 5; + + $urlform = http_build_query($formdata, '', '&'); // Take the form data and url encode it. + $jsonformdata = json_encode($urlform); // Take form string and JSON encode. + + $returnvalue = core_backup_external::submit_copy_form($jsonformdata); + + $returnjson = external_api::clean_returnvalue(core_backup_external::submit_copy_form_returns(), $returnvalue); + $copyids = json_decode($returnjson, true); + + $backuprec = $DB->get_record('backup_controllers', array('backupid' => $copyids['backupid'])); + $restorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); + + // Check backup was completed successfully. + $this->assertEquals(backup::STATUS_AWAITING, $backuprec->status); + $this->assertEquals(0, $backuprec->progress); + $this->assertEquals('backup', $backuprec->operation); + + // Check restore was completed successfully. + $this->assertEquals(backup::STATUS_REQUIRE_CONV, $restorerec->status); + $this->assertEquals(0, $restorerec->progress); + $this->assertEquals('restore', $restorerec->operation); + } +} \ No newline at end of file diff --git a/backup/util/helper/async_helper.class.php b/backup/util/helper/async_helper.class.php index 309c478b0c5aa..b57c4b5eb2879 100644 --- a/backup/util/helper/async_helper.class.php +++ b/backup/util/helper/async_helper.class.php @@ -64,7 +64,7 @@ class async_helper { public function __construct($type, $id) { $this->type = $type; $this->backupid = $id; - $this->backuprec = $this->get_backup_record($id); + $this->backuprec = self::get_backup_record($id); $this->user = $this->get_user(); } @@ -76,7 +76,7 @@ public function __construct($type, $id) { * @param int $id The backup id to get. * @return object $backuprec The backup controller record. */ - private function get_backup_record($id) { + static public function get_backup_record($id) { global $DB; $backuprec = $DB->get_record('backup_controllers', array('backupid' => $id), '*', MUST_EXIST); @@ -215,18 +215,21 @@ public static function is_async_pending($id, $type, $operation) { require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); require_once($CFG->dirroot . '/backup/backup.class.php'); - if (self::is_async_enabled()) { - $select = 'userid = ? AND itemid = ? AND type = ? AND operation = ? AND execution = ? AND status < ? AND status > ?'; - $params = array( - $USER->id, - $id, - $type, - $operation, - backup::EXECUTION_DELAYED, - backup::STATUS_FINISHED_ERR, - backup::STATUS_NEED_PRECHECK - ); - $asyncpending = $DB->record_exists_select('backup_controllers', $select, $params); + $select = 'userid = ? AND itemid = ? AND type = ? AND operation = ? AND execution = ? AND status < ? AND status > ?'; + $params = array( + $USER->id, + $id, + $type, + $operation, + backup::EXECUTION_DELAYED, + backup::STATUS_FINISHED_ERR, + backup::STATUS_NEED_PRECHECK + ); + + $asyncrecord= $DB->get_record_select('backup_controllers', $select, $params); + + if ((self::is_async_enabled() && $asyncrecord) || ($asyncrecord && $asyncrecord->purpose == backup::MODE_COPY)) { + $asyncpending = true; } return $asyncpending; } diff --git a/backup/util/helper/tests/async_helper_test.php b/backup/util/helper/tests/async_helper_test.php index 70b50a2078a68..c27ccf2caa98d 100644 --- a/backup/util/helper/tests/async_helper_test.php +++ b/backup/util/helper/tests/async_helper_test.php @@ -145,4 +145,88 @@ public function test_get_async_backups() { $this->assertEquals(1, count($result)); $this->assertEquals('backup.mbz', $result[0][0]); } + + /** + * Tests getting the backup record. + */ + public function test_get_backup_record() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + + // Create the initial backupcontoller. + $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE, + \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES); + $backupid = $bc->get_backupid(); + $copyrec = \async_helper::get_backup_record($backupid); + + $this->assertEquals($backupid, $copyrec->backupid); + + } + + /** + * Tests is async pending conditions. + */ + public function test_is_async_pending() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + + set_config('enableasyncbackup', '0'); + $ispending = async_helper::is_async_pending($course->id, 'course', 'backup'); + + // Should be false as there are no backups and async backup is false. + $this->assertFalse($ispending); + + // Create the initial backupcontoller. + new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE, + \backup::INTERACTIVE_NO, \backup::MODE_ASYNC, $USER->id, \backup::RELEASESESSION_YES); + $ispending = async_helper::is_async_pending($course->id, 'course', 'backup'); + + // Should be false as there as async backup is false. + $this->assertFalse($ispending); + + set_config('enableasyncbackup', '1'); + // Should be true as there as async backup is true and there is a pending backup. + $this->assertFalse($ispending); + } + + /** + * Tests is async pending conditions for course copies. + */ + public function test_is_async_pending_copy() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + + set_config('enableasyncbackup', '0'); + $ispending = async_helper::is_async_pending($course->id, 'course', 'backup'); + + // Should be false as there are no copies and async backup is false. + $this->assertFalse($ispending); + + // Create the initial backupcontoller. + new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE, + \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES); + $ispending = async_helper::is_async_pending($course->id, 'course', 'backup'); + + // Should be True as this a copy operation. + $this->assertTrue($ispending); + + set_config('enableasyncbackup', '1'); + $ispending = async_helper::is_async_pending($course->id, 'course', 'backup'); + + // Should be true as there as async backup is true and there is a pending copy. + $this->assertTrue($ispending); + } + } diff --git a/backup/util/includes/backup_includes.php b/backup/util/includes/backup_includes.php index 0080a0e3b19a6..9df19647e16f2 100644 --- a/backup/util/includes/backup_includes.php +++ b/backup/util/includes/backup_includes.php @@ -97,6 +97,7 @@ require_once($CFG->dirroot . '/backup/util/ui/backup_ui.class.php'); require_once($CFG->dirroot . '/backup/util/ui/backup_ui_stage.class.php'); require_once($CFG->dirroot . '/backup/util/ui/backup_ui_setting.class.php'); +require_once($CFG->dirroot . '/backup/util/ui/classes/copy/copy.php'); // And some moodle stuff too require_once($CFG->dirroot.'/course/lib.php'); diff --git a/backup/util/plan/backup_plan.class.php b/backup/util/plan/backup_plan.class.php index e3db201acada8..a3dc2b0fdbcdb 100644 --- a/backup/util/plan/backup_plan.class.php +++ b/backup/util/plan/backup_plan.class.php @@ -33,6 +33,12 @@ class backup_plan extends base_plan implements loggable { protected $basepath; // Fullpath to dir where backup is created protected $excludingdactivities; + /** + * The role ids to keep in a copy operation. + * @var array + */ + protected $keptroles = array(); + /** * Constructor - instantiates one object of this class */ @@ -104,6 +110,26 @@ public function set_excluding_activities() { $this->excludingdactivities = true; } + /** + * Sets the user roles that should be kept in the destination course + * for a course copy operation. + * + * @param array $roleids + */ + public function set_kept_roles(array $roleids): void { + $this->keptroles = $roleids; + } + + /** + * Get the user roles that should be kept in the destination course + * for a course copy operation. + * + * @return array + */ + public function get_kept_roles(): array { + return $this->keptroles; + } + public function log($message, $level, $a = null, $depth = null, $display = false) { backup_helper::log($message, $level, $a, $depth, $display, $this->get_logger()); } diff --git a/backup/util/plan/backup_task.class.php b/backup/util/plan/backup_task.class.php index d6313a1f2b619..818b315bcf2b2 100644 --- a/backup/util/plan/backup_task.class.php +++ b/backup/util/plan/backup_task.class.php @@ -46,6 +46,16 @@ public function get_backupid() { public function is_excluding_activities() { return $this->plan->is_excluding_activities(); } + + /** + * Get the user roles that should be kept in the destination course + * for a course copy operation. + * + * @return array + */ + public function get_kept_roles(): array { + return $this->plan->get_kept_roles(); + } } /* diff --git a/backup/util/ui/amd/build/async_backup.min.js b/backup/util/ui/amd/build/async_backup.min.js index 202c88c45784d..15d852a7e99e4 100644 --- a/backup/util/ui/amd/build/async_backup.min.js +++ b/backup/util/ui/amd/build/async_backup.min.js @@ -1,2 +1,2 @@ -define ("core_backup/async_backup",["jquery","core/ajax","core/str","core/notification","core/templates"],function(a,b,c,d,e){var n=900,o=1e3,p={},q=15e3,r=15e3,s=1.5,t,u,v,w,x,y,z=2e3;function f(b,c){var d=Math.round(c)+"%",e=a("#"+b+"_bar"),f=c.toFixed(2)+"%";e.attr("aria-valuenow",d);e.css("width",d);e.text(f)}function g(a,b,c){clearInterval(a);return setInterval(b,c)}function h(c){var f=a("#"+c+"_bar").parent().parent(),g=f.parent(),h=f.siblings(),i=h[1],j=a(i).text(),k=h[0],l=a(k).text();b.call([{methodname:"core_backup_get_async_backup_links_backup",args:{filename:l,contextid:u}}])[0].done(function(a){var b={filename:l,time:j,size:a.filesize,fileurl:a.fileurl,restoreurl:a.restoreurl};e.render("core/async_backup_progress_row",b).then(function(a,b){e.replaceNodeContents(g,a,b)}).fail(function(){d.exception(new Error("Failed to load table row"))})})}function i(c){var f=a("#"+c+"_bar").parent().parent(),g=f.parent(),h=f.siblings(),i=h[0],j=h[1],k=a(j).text();b.call([{methodname:"core_backup_get_async_backup_links_restore",args:{backupid:c,contextid:u}}])[0].done(function(b){var c=a(i).text(),f={resourcename:c,restoreurl:b.restoreurl,time:k};e.render("core/async_restore_progress_row",f).then(function(a,b){e.replaceNodeContents(g,a,b)}).fail(function(){d.exception(new Error("Failed to load table row"))})})}function j(e){var g=100*e.progress,h=a("#"+t+"_bar"),i=a("#"+t+"_status"),j=a("#"+t+"_detail"),k=a("#"+t+"_button"),l;if(e.status==800){h.addClass("bg-success");f(t,g);var m="async"+w+"processing";c.get_string(m,"backup").then(function(a){i.text(a);return a}).catch(function(){d.exception(new Error("Failed to load string: backup "+m))})}else if(e.status==n){h.addClass("bg-danger");h.removeClass("bg-success");f(t,100);var p="async"+w+"error",q="async"+w+"errordetail";l=[{key:p,component:"backup"},{key:q,component:"backup"}];c.get_strings(l).then(function(a){i.text(a[0]);j.text(a[1]);return a}).catch(function(){d.exception(new Error("Failed to load string"))});a(".backup_progress").children("span").removeClass("backup_stage_current");a(".backup_progress").children("span").last().addClass("backup_stage_current");clearInterval(x)}else if(e.status==o){h.addClass("bg-success");f(t,100);var r="async"+w+"complete";c.get_string(r,"backup").then(function(a){i.text(a);return a}).catch(function(){d.exception(new Error("Failed to load string: backup "+r))});if("restore"==w){b.call([{methodname:"core_backup_get_async_backup_links_restore",args:{backupid:t,contextid:u}}])[0].done(function(a){var b="async"+w+"completedetail",e="async"+w+"completebutton",f=[{key:b,component:"backup",param:a.restoreurl},{key:e,component:"backup"}];c.get_strings(f).then(function(b){j.html(b[0]);k.text(b[1]);k.attr("href",a.restoreurl);return b}).catch(function(){d.exception(new Error("Failed to load string"))})})}else{var s="async"+w+"completedetail",y="async"+w+"completebutton";l=[{key:s,component:"backup",param:v},{key:y,component:"backup"}];c.get_strings(l).then(function(a){j.html(a[0]);k.text(a[1]);k.attr("href",v);return a}).catch(function(){d.exception(new Error("Failed to load string"))})}a(".backup_progress").children("span").removeClass("backup_stage_current");a(".backup_progress").children("span").last().addClass("backup_stage_current");clearInterval(x)}}function k(b){b.forEach(function(b){var c=100*b.progress,d=b.backupid,e=a("#"+d+"_bar"),g=b.operation;if(b.status==800){e.addClass("bg-success");f(d,c)}else if(b.status==n){e.addClass("bg-danger");e.addClass("complete");a("#"+d+"_bar").removeClass("bg-success");f(d,100)}else if(b.status==o){e.addClass("bg-success");e.addClass("complete");f(d,100);if("backup"==g){h(d)}else{i(d)}}})}function l(){b.call([{methodname:"core_backup_get_async_backup_progress",args:{backupids:[t],contextid:u}}],!0,!0,!1,z)[0].done(function(a){j(a[0]);r=q;x=g(x,l,q)}).fail(function(){r=r*s;x=g(x,l,r)})}function m(){var c=[],d=a(".progress").find(".progress-bar").not(".complete");d.each(function(){c.push(this.id.substring(0,32))});if(0.\n\n/**\n * This module updates the UI during an asynchronous\n * backup or restore process.\n *\n * @module backup/util/async_backup\n * @package core\n * @copyright 2018 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.7\n */\ndefine(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates'],\n function($, ajax, Str, notification, Templates) {\n\n /**\n * Module level constants.\n *\n * Using var instead of const as ES6 isn't fully supported yet.\n */\n var STATUS_EXECUTING = 800;\n var STATUS_FINISHED_ERR = 900;\n var STATUS_FINISHED_OK = 1000;\n\n /**\n * Module level variables.\n */\n var Asyncbackup = {};\n var checkdelayoriginal = 15000; // This is the default time to use.\n var checkdelay = 15000; // How often we should check for progress updates.\n var checkdelaymultipler = 1.5; // If a request fails this multiplier will be used to increase the checkdelay value\n var backupid; // The backup id to get the progress for.\n var contextid; // The course this backup progress is for.\n var restoreurl; // The URL to view course restores.\n var typeid; // The type of operation backup or restore.\n var backupintervalid; // The id of the setInterval function.\n var allbackupintervalid; // The id of the setInterval function.\n var timeout = 2000; // Timeout for ajax requests.\n\n /**\n * Helper function to update UI components.\n *\n * @param {string} backupid The id to match elements on.\n * @param {number} percentage The completion percentage to apply.\n */\n function updateElement(backupid, percentage) {\n var percentagewidth = Math.round(percentage) + '%';\n var elementbar = $('#' + backupid + '_bar');\n var percentagetext = percentage.toFixed(2) + '%';\n\n // Set progress bar percentage indicators\n elementbar.attr('aria-valuenow', percentagewidth);\n elementbar.css('width', percentagewidth);\n elementbar.text(percentagetext);\n }\n\n /**\n * Updates the interval we use to check for backup progress.\n *\n * @param {Number} intervalid The id of the interval\n * @param {Function} callback The function to use in setInterval\n * @param {Number} value The specified interval (in milliseconds)\n * @returns {Number}\n */\n function updateInterval(intervalid, callback, value) {\n clearInterval(intervalid);\n return setInterval(callback, value);\n }\n\n /**\n * Update backup table row when an async backup completes.\n *\n * @param {string} backupid The id to match elements on.\n */\n function updateBackupTableRow(backupid) {\n var statuscell = $('#' + backupid + '_bar').parent().parent();\n var tablerow = statuscell.parent();\n var cellsiblings = statuscell.siblings();\n var timecell = cellsiblings[1];\n var timevalue = $(timecell).text();\n var filenamecell = cellsiblings[0];\n var filename = $(filenamecell).text();\n\n ajax.call([{\n // Get the table data via webservice.\n methodname: 'core_backup_get_async_backup_links_backup',\n args: {\n 'filename': filename,\n 'contextid': contextid\n },\n }])[0].done(function(response) {\n // We have the data now update the UI.\n var context = {\n filename: filename,\n time: timevalue,\n size: response.filesize,\n fileurl: response.fileurl,\n restoreurl: response.restoreurl\n };\n\n Templates.render('core/async_backup_progress_row', context).then(function(html, js) {\n Templates.replaceNodeContents(tablerow, html, js);\n return;\n }).fail(function() {\n notification.exception(new Error('Failed to load table row'));\n return;\n });\n });\n }\n\n /**\n * Update restore table row when an async restore completes.\n *\n * @param {string} backupid The id to match elements on.\n */\n function updateRestoreTableRow(backupid) {\n var statuscell = $('#' + backupid + '_bar').parent().parent();\n var tablerow = statuscell.parent();\n var cellsiblings = statuscell.siblings();\n var coursecell = cellsiblings[0];\n var timecell = cellsiblings[1];\n var timevalue = $(timecell).text();\n\n ajax.call([{\n // Get the table data via webservice.\n methodname: 'core_backup_get_async_backup_links_restore',\n args: {\n 'backupid': backupid,\n 'contextid': contextid\n },\n }])[0].done(function(response) {\n // We have the data now update the UI.\n var resourcename = $(coursecell).text();\n var context = {\n resourcename: resourcename,\n restoreurl: response.restoreurl,\n time: timevalue\n };\n\n Templates.render('core/async_restore_progress_row', context).then(function(html, js) {\n Templates.replaceNodeContents(tablerow, html, js);\n return;\n }).fail(function() {\n notification.exception(new Error('Failed to load table row'));\n return;\n });\n });\n }\n\n /**\n * Update the Moodle user interface with the progress of\n * the backup process.\n *\n * @param {object} progress The progress and status of the process.\n */\n function updateProgress(progress) {\n var percentage = progress.progress * 100;\n var elementbar = $('#' + backupid + '_bar');\n var elementstatus = $('#' + backupid + '_status');\n var elementdetail = $('#' + backupid + '_detail');\n var elementbutton = $('#' + backupid + '_button');\n var stringRequests;\n\n if (progress.status == STATUS_EXECUTING) {\n // Process is in progress.\n // Add in progress class color to bar\n elementbar.addClass('bg-success');\n\n updateElement(backupid, percentage);\n\n // Change heading\n var strProcessing = 'async' + typeid + 'processing';\n Str.get_string(strProcessing, 'backup').then(function(title) {\n elementstatus.text(title);\n return title;\n }).catch(function() {\n notification.exception(new Error('Failed to load string: backup ' + strProcessing));\n });\n\n } else if (progress.status == STATUS_FINISHED_ERR) {\n // Process completed with error.\n\n // Add in fail class color to bar\n elementbar.addClass('bg-danger');\n\n // Remove in progress class color to bar\n elementbar.removeClass('bg-success');\n\n updateElement(backupid, 100);\n\n // Change heading and text\n var strStatus = 'async' + typeid + 'error';\n var strStatusDetail = 'async' + typeid + 'errordetail';\n stringRequests = [\n {key: strStatus, component: 'backup'},\n {key: strStatusDetail, component: 'backup'}\n ];\n Str.get_strings(stringRequests).then(function(strings) {\n elementstatus.text(strings[0]);\n elementdetail.text(strings[1]);\n\n return strings;\n })\n .catch(function() {\n notification.exception(new Error('Failed to load string'));\n return;\n });\n\n $('.backup_progress').children('span').removeClass('backup_stage_current');\n $('.backup_progress').children('span').last().addClass('backup_stage_current');\n\n // Stop checking when we either have an error or a completion.\n clearInterval(backupintervalid);\n\n } else if (progress.status == STATUS_FINISHED_OK) {\n // Process completed successfully.\n\n // Add in progress class color to bar\n elementbar.addClass('bg-success');\n\n updateElement(backupid, 100);\n\n // Change heading and text\n var strComplete = 'async' + typeid + 'complete';\n Str.get_string(strComplete, 'backup').then(function(title) {\n elementstatus.text(title);\n return title;\n }).catch(function() {\n notification.exception(new Error('Failed to load string: backup ' + strComplete));\n });\n\n if (typeid == 'restore') {\n ajax.call([{\n // Get the table data via webservice.\n methodname: 'core_backup_get_async_backup_links_restore',\n args: {\n 'backupid': backupid,\n 'contextid': contextid\n },\n }])[0].done(function(response) {\n var strDetail = 'async' + typeid + 'completedetail';\n var strButton = 'async' + typeid + 'completebutton';\n var stringRequests = [\n {key: strDetail, component: 'backup', param: response.restoreurl},\n {key: strButton, component: 'backup'}\n ];\n Str.get_strings(stringRequests).then(function(strings) {\n elementdetail.html(strings[0]);\n elementbutton.text(strings[1]);\n elementbutton.attr('href', response.restoreurl);\n\n return strings;\n })\n .catch(function() {\n notification.exception(new Error('Failed to load string'));\n return;\n });\n\n });\n } else {\n var strDetail = 'async' + typeid + 'completedetail';\n var strButton = 'async' + typeid + 'completebutton';\n stringRequests = [\n {key: strDetail, component: 'backup', param: restoreurl},\n {key: strButton, component: 'backup'}\n ];\n Str.get_strings(stringRequests).then(function(strings) {\n elementdetail.html(strings[0]);\n elementbutton.text(strings[1]);\n elementbutton.attr('href', restoreurl);\n\n return strings;\n })\n .catch(function() {\n notification.exception(new Error('Failed to load string'));\n return;\n });\n\n }\n\n $('.backup_progress').children('span').removeClass('backup_stage_current');\n $('.backup_progress').children('span').last().addClass('backup_stage_current');\n\n // Stop checking when we either have an error or a completion.\n clearInterval(backupintervalid);\n }\n }\n\n /**\n * Update the Moodle user interface with the progress of\n * all the pending processes.\n *\n * @param {object} progress The progress and status of the process.\n */\n function updateProgressAll(progress) {\n progress.forEach(function(element) {\n var percentage = element.progress * 100;\n var backupid = element.backupid;\n var elementbar = $('#' + backupid + '_bar');\n var type = element.operation;\n\n if (element.status == STATUS_EXECUTING) {\n // Process is in element.\n\n // Add in element class color to bar\n elementbar.addClass('bg-success');\n\n updateElement(backupid, percentage);\n\n } else if (element.status == STATUS_FINISHED_ERR) {\n // Process completed with error.\n\n // Add in fail class color to bar\n elementbar.addClass('bg-danger');\n elementbar.addClass('complete');\n\n // Remove in element class color to bar\n $('#' + backupid + '_bar').removeClass('bg-success');\n\n updateElement(backupid, 100);\n\n } else if (element.status == STATUS_FINISHED_OK) {\n // Process completed successfully.\n\n // Add in element class color to bar\n elementbar.addClass('bg-success');\n elementbar.addClass('complete');\n\n updateElement(backupid, 100);\n\n // We have a successful backup. Update the UI with download and file details.\n if (type == 'backup') {\n updateBackupTableRow(backupid);\n } else {\n updateRestoreTableRow(backupid);\n }\n\n }\n\n });\n }\n\n /**\n * Get the progress of the backup process via ajax.\n */\n function getBackupProgress() {\n ajax.call([{\n // Get the backup progress via webservice.\n methodname: 'core_backup_get_async_backup_progress',\n args: {\n 'backupids': [backupid],\n 'contextid': contextid\n },\n }], true, true, false, timeout)[0].done(function(response) {\n // We have the progress now update the UI.\n updateProgress(response[0]);\n checkdelay = checkdelayoriginal;\n backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelayoriginal);\n }).fail(function() {\n checkdelay = checkdelay * checkdelaymultipler;\n backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelay);\n });\n }\n\n /**\n * Get the progress of all backup processes via ajax.\n */\n function getAllBackupProgress() {\n var backupids = [];\n var progressbars = $('.progress').find('.progress-bar').not('.complete');\n\n progressbars.each(function() {\n backupids.push((this.id).substring(0, 32));\n });\n\n if (backupids.length > 0) {\n ajax.call([{\n // Get the backup progress via webservice.\n methodname: 'core_backup_get_async_backup_progress',\n args: {\n 'backupids': backupids,\n 'contextid': contextid\n },\n }], true, true, false, timeout)[0].done(function(response) {\n updateProgressAll(response);\n checkdelay = checkdelayoriginal;\n allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelayoriginal);\n }).fail(function() {\n checkdelay = checkdelay * checkdelaymultipler;\n allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelay);\n });\n } else {\n clearInterval(allbackupintervalid); // No more progress bars to update, stop checking.\n }\n }\n\n /**\n * Get status updates for all backups.\n *\n * @public\n * @param {number} context The context id.\n */\n Asyncbackup.asyncBackupAllStatus = function(context) {\n contextid = context;\n allbackupintervalid = setInterval(getAllBackupProgress, checkdelay);\n };\n\n /**\n * Get status updates for backup.\n *\n * @public\n * @param {string} backup The backup record id.\n * @param {number} context The context id.\n * @param {string} restore The restore link.\n * @param {string} type The operation type (backup or restore).\n */\n Asyncbackup.asyncBackupStatus = function(backup, context, restore, type) {\n backupid = backup;\n contextid = context;\n restoreurl = restore;\n\n if (type == 'backup') {\n typeid = 'backup';\n } else {\n typeid = 'restore';\n }\n\n // Remove the links from the progress bar, no going back now.\n $('.backup_progress').children('a').removeAttr('href');\n\n // Periodically check for progress updates and update the UI as required.\n backupintervalid = setInterval(getBackupProgress, checkdelay);\n\n };\n\n return Asyncbackup;\n});\n"],"file":"async_backup.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/async_backup.js"],"names":["define","$","ajax","Str","notification","Templates","STATUS_FINISHED_ERR","STATUS_FINISHED_OK","Asyncbackup","checkdelayoriginal","checkdelay","checkdelaymultipler","backupid","contextid","restoreurl","typeid","backupintervalid","allbackupintervalid","allcopyintervalid","timeout","updateElement","type","percentage","percentagewidth","Math","round","elementbar","document","querySelectorAll","CSS","escape","percentagetext","toFixed","setAttribute","style","width","innerHTML","updateInterval","intervalid","callback","value","clearInterval","setInterval","updateBackupTableRow","statuscell","parent","tablerow","cellsiblings","siblings","timecell","timevalue","text","filenamecell","filename","call","methodname","args","done","response","context","time","size","filesize","fileurl","render","then","html","js","replaceNodeContents","fail","exception","Error","updateRestoreTableRow","coursecell","resourcename","updateCopyTableRow","restorecourse","closest","children","coursename","courselink","createElement","elementbarparent","operation","previousElementSibling","get_string","content","catch","appendChild","updateProgress","progress","elementstatus","elementdetail","elementbutton","stringRequests","status","classList","add","strProcessing","title","remove","strStatus","strStatusDetail","key","component","get_strings","strings","removeClass","last","addClass","strComplete","strDetail","strButton","param","attr","updateProgressAll","forEach","element","updateProgressCopy","restorecell","getBackupProgress","getAllBackupProgress","backupids","progressbars","find","not","each","push","id","substring","length","getAllCopyProgress","copyids","progressvars","dataset","restoreid","asyncBackupAllStatus","asyncCopyAllStatus","asyncBackupStatus","backup","restore","removeAttr"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module updates the UI during an asynchronous\n * backup or restore process.\n *\n * @module backup/util/async_backup\n * @package core\n * @copyright 2018 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.7\n */\ndefine(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates'],\n function($, ajax, Str, notification, Templates) {\n\n /**\n * Module level constants.\n *\n * Using var instead of const as ES6 isn't fully supported yet.\n */\n var STATUS_EXECUTING = 800;\n var STATUS_FINISHED_ERR = 900;\n var STATUS_FINISHED_OK = 1000;\n\n /**\n * Module level variables.\n */\n var Asyncbackup = {};\n var checkdelayoriginal = 15000; // This is the default time to use.\n var checkdelay = 15000; // How often we should check for progress updates.\n var checkdelaymultipler = 1.5; // If a request fails this multiplier will be used to increase the checkdelay value\n var backupid; // The backup id to get the progress for.\n var contextid; // The course this backup progress is for.\n var restoreurl; // The URL to view course restores.\n var typeid; // The type of operation backup or restore.\n var backupintervalid; // The id of the setInterval function.\n var allbackupintervalid; // The id of the setInterval function.\n var allcopyintervalid; // The id of the setInterval function.\n var timeout = 2000; // Timeout for ajax requests.\n\n /**\n * Helper function to update UI components.\n *\n * @param {string} backupid The id to match elements on.\n * @param {string} type The type of operation, backup or restore.\n * @param {number} percentage The completion percentage to apply.\n */\n function updateElement(backupid, type, percentage) {\n var percentagewidth = Math.round(percentage) + '%';\n var elementbar = document.querySelectorAll(\"[data-\" + type + \"id=\" + CSS.escape(backupid) + \"]\")[0];\n var percentagetext = percentage.toFixed(2) + '%';\n\n // Set progress bar percentage indicators\n elementbar.setAttribute('aria-valuenow', percentagewidth);\n elementbar.style.width = percentagewidth;\n elementbar.innerHTML = percentagetext;\n }\n\n /**\n * Updates the interval we use to check for backup progress.\n *\n * @param {Number} intervalid The id of the interval\n * @param {Function} callback The function to use in setInterval\n * @param {Number} value The specified interval (in milliseconds)\n * @returns {Number}\n */\n function updateInterval(intervalid, callback, value) {\n clearInterval(intervalid);\n return setInterval(callback, value);\n }\n\n /**\n * Update backup table row when an async backup completes.\n *\n * @param {string} backupid The id to match elements on.\n */\n function updateBackupTableRow(backupid) {\n var statuscell = $('#' + backupid + '_bar').parent().parent();\n var tablerow = statuscell.parent();\n var cellsiblings = statuscell.siblings();\n var timecell = cellsiblings[1];\n var timevalue = $(timecell).text();\n var filenamecell = cellsiblings[0];\n var filename = $(filenamecell).text();\n\n ajax.call([{\n // Get the table data via webservice.\n methodname: 'core_backup_get_async_backup_links_backup',\n args: {\n 'filename': filename,\n 'contextid': contextid\n },\n }])[0].done(function(response) {\n // We have the data now update the UI.\n var context = {\n filename: filename,\n time: timevalue,\n size: response.filesize,\n fileurl: response.fileurl,\n restoreurl: response.restoreurl\n };\n\n Templates.render('core/async_backup_progress_row', context).then(function(html, js) {\n Templates.replaceNodeContents(tablerow, html, js);\n return;\n }).fail(function() {\n notification.exception(new Error('Failed to load table row'));\n return;\n });\n });\n }\n\n /**\n * Update restore table row when an async restore completes.\n *\n * @param {string} backupid The id to match elements on.\n */\n function updateRestoreTableRow(backupid) {\n var statuscell = $('#' + backupid + '_bar').parent().parent();\n var tablerow = statuscell.parent();\n var cellsiblings = statuscell.siblings();\n var coursecell = cellsiblings[0];\n var timecell = cellsiblings[1];\n var timevalue = $(timecell).text();\n\n ajax.call([{\n // Get the table data via webservice.\n methodname: 'core_backup_get_async_backup_links_restore',\n args: {\n 'backupid': backupid,\n 'contextid': contextid\n },\n }])[0].done(function(response) {\n // We have the data now update the UI.\n var resourcename = $(coursecell).text();\n var context = {\n resourcename: resourcename,\n restoreurl: response.restoreurl,\n time: timevalue\n };\n\n Templates.render('core/async_restore_progress_row', context).then(function(html, js) {\n Templates.replaceNodeContents(tablerow, html, js);\n return;\n }).fail(function() {\n notification.exception(new Error('Failed to load table row'));\n return;\n });\n });\n }\n\n /**\n * Update copy table row when an course copy completes.\n *\n * @param {string} backupid The id to match elements on.\n */\n function updateCopyTableRow(backupid) {\n var elementbar = document.querySelectorAll(\"[data-restoreid=\" + CSS.escape(backupid) + \"]\")[0];\n var restorecourse = elementbar.closest('tr').children[1];\n var coursename = restorecourse.innerHTML;\n var courselink = document.createElement('a');\n var elementbarparent = elementbar.closest('td');\n var operation = elementbarparent.previousElementSibling;\n\n // Replace the prgress bar.\n Str.get_string('complete').then(function(content) {\n operation.innerHTML = content;\n return;\n }).catch(function() {\n notification.exception(new Error('Failed to load string: complete'));\n return;\n });\n\n Templates.render('core/async_copy_complete_cell', {}).then(function(html, js) {\n Templates.replaceNodeContents(elementbarparent, html, js);\n return;\n }).fail(function() {\n notification.exception(new Error('Failed to load table cell'));\n return;\n });\n\n // Update the destination course name to a link to that course.\n ajax.call([{\n methodname: 'core_backup_get_async_backup_links_restore',\n args: {\n 'backupid': backupid,\n 'contextid': 0\n },\n }])[0].done(function(response) {\n courselink.setAttribute('href', response.restoreurl);\n courselink.innerHTML = coursename;\n restorecourse.innerHTML = null;\n restorecourse.appendChild(courselink);\n\n return;\n }).fail(function() {\n notification.exception(new Error('Failed to update table row'));\n return;\n });\n }\n\n /**\n * Update the Moodle user interface with the progress of\n * the backup process.\n *\n * @param {object} progress The progress and status of the process.\n */\n function updateProgress(progress) {\n var percentage = progress.progress * 100;\n var type = 'backup';\n var elementbar = document.querySelectorAll(\"[data-\" + type + \"id=\" + CSS.escape(backupid) + \"]\")[0];\n var elementstatus = $('#' + backupid + '_status');\n var elementdetail = $('#' + backupid + '_detail');\n var elementbutton = $('#' + backupid + '_button');\n var stringRequests;\n\n if (progress.status == STATUS_EXECUTING) {\n // Process is in progress.\n // Add in progress class color to bar.\n elementbar.classList.add('bg-success');\n\n updateElement(backupid, type, percentage);\n\n // Change heading.\n var strProcessing = 'async' + typeid + 'processing';\n Str.get_string(strProcessing, 'backup').then(function(title) {\n elementstatus.text(title);\n return;\n }).catch(function() {\n notification.exception(new Error('Failed to load string: backup ' + strProcessing));\n });\n\n } else if (progress.status == STATUS_FINISHED_ERR) {\n // Process completed with error.\n\n // Add in fail class color to bar.\n elementbar.classList.add('bg-danger');\n\n // Remove in progress class color to bar.\n elementbar.classList.remove('bg-success');\n\n updateElement(backupid, type, 100);\n\n // Change heading and text.\n var strStatus = 'async' + typeid + 'error';\n var strStatusDetail = 'async' + typeid + 'errordetail';\n stringRequests = [\n {key: strStatus, component: 'backup'},\n {key: strStatusDetail, component: 'backup'}\n ];\n Str.get_strings(stringRequests).then(function(strings) {\n elementstatus.text(strings[0]);\n elementdetail.text(strings[1]);\n\n return;\n })\n .catch(function() {\n notification.exception(new Error('Failed to load string'));\n return;\n });\n\n $('.backup_progress').children('span').removeClass('backup_stage_current');\n $('.backup_progress').children('span').last().addClass('backup_stage_current');\n\n // Stop checking when we either have an error or a completion.\n clearInterval(backupintervalid);\n\n } else if (progress.status == STATUS_FINISHED_OK) {\n // Process completed successfully.\n\n // Add in progress class color to bar\n elementbar.classList.add('bg-success');\n\n updateElement(backupid, type, 100);\n\n // Change heading and text\n var strComplete = 'async' + typeid + 'complete';\n Str.get_string(strComplete, 'backup').then(function(title) {\n elementstatus.text(title);\n return;\n }).catch(function() {\n notification.exception(new Error('Failed to load string: backup ' + strComplete));\n });\n\n if (typeid == 'restore') {\n ajax.call([{\n // Get the table data via webservice.\n methodname: 'core_backup_get_async_backup_links_restore',\n args: {\n 'backupid': backupid,\n 'contextid': contextid\n },\n }])[0].done(function(response) {\n var strDetail = 'async' + typeid + 'completedetail';\n var strButton = 'async' + typeid + 'completebutton';\n var stringRequests = [\n {key: strDetail, component: 'backup', param: response.restoreurl},\n {key: strButton, component: 'backup'}\n ];\n Str.get_strings(stringRequests).then(function(strings) {\n elementdetail.html(strings[0]);\n elementbutton.text(strings[1]);\n elementbutton.attr('href', response.restoreurl);\n\n return;\n })\n .catch(function() {\n notification.exception(new Error('Failed to load string'));\n return;\n });\n\n });\n } else {\n var strDetail = 'async' + typeid + 'completedetail';\n var strButton = 'async' + typeid + 'completebutton';\n stringRequests = [\n {key: strDetail, component: 'backup', param: restoreurl},\n {key: strButton, component: 'backup'}\n ];\n Str.get_strings(stringRequests).then(function(strings) {\n elementdetail.html(strings[0]);\n elementbutton.text(strings[1]);\n elementbutton.attr('href', restoreurl);\n\n return;\n })\n .catch(function() {\n notification.exception(new Error('Failed to load string'));\n return;\n });\n\n }\n\n $('.backup_progress').children('span').removeClass('backup_stage_current');\n $('.backup_progress').children('span').last().addClass('backup_stage_current');\n\n // Stop checking when we either have an error or a completion.\n clearInterval(backupintervalid);\n }\n }\n\n /**\n * Update the Moodle user interface with the progress of\n * all the pending processes for backup and restore operations.\n *\n * @param {object} progress The progress and status of the process.\n */\n function updateProgressAll(progress) {\n progress.forEach(function(element) {\n var percentage = element.progress * 100;\n var backupid = element.backupid;\n var type = element.operation;\n var elementbar = document.querySelectorAll(\"[data-\" + type + \"id=\" + CSS.escape(backupid) + \"]\")[0];\n\n if (element.status == STATUS_EXECUTING) {\n // Process is in element.\n\n // Add in element class color to bar\n elementbar.classList.add('bg-success');\n\n updateElement(backupid, type, percentage);\n\n } else if (element.status == STATUS_FINISHED_ERR) {\n // Process completed with error.\n\n // Add in fail class color to bar\n elementbar.classList.add('bg-danger');\n elementbar.classList.add('complete');\n\n // Remove in element class color to bar\n elementbar.classList.remove('bg-success');\n\n updateElement(backupid, type, 100);\n\n } else if (element.status == STATUS_FINISHED_OK) {\n // Process completed successfully.\n\n // Add in element class color to bar\n elementbar.classList.add('bg-success');\n elementbar.classList.add('complete');\n\n updateElement(backupid, type, 100);\n\n // We have a successful backup. Update the UI with download and file details.\n if (type == 'backup') {\n updateBackupTableRow(backupid);\n } else {\n updateRestoreTableRow(backupid);\n }\n\n }\n\n });\n }\n\n /**\n * Update the Moodle user interface with the progress of\n * all the pending processes for copy operations.\n *\n * @param {object} progress The progress and status of the process.\n */\n function updateProgressCopy(progress) {\n progress.forEach(function(element) {\n var percentage = element.progress * 100;\n var backupid = element.backupid;\n var type = element.operation;\n var elementbar = document.querySelectorAll(\"[data-\" + type + \"id=\" + CSS.escape(backupid) + \"]\")[0];\n\n if (type == 'restore') {\n let restorecell = elementbar.closest('tr').children[3];\n Str.get_string('restore').then(function(content) {\n restorecell.innerHTML = content;\n return;\n }).catch(function() {\n notification.exception(new Error('Failed to load string: restore'));\n });\n }\n\n if (element.status == STATUS_EXECUTING) {\n // Process is in element.\n\n // Add in element class color to bar\n elementbar.classList.add('bg-success');\n\n updateElement(backupid, type, percentage);\n\n } else if (element.status == STATUS_FINISHED_ERR) {\n // Process completed with error.\n\n // Add in fail class color to bar\n elementbar.classList.add('bg-danger');\n elementbar.classList.add('complete');\n\n // Remove in element class color to bar\n elementbar.classList.remove('bg-success');\n\n updateElement(backupid, type, 100);\n\n } else if ((element.status == STATUS_FINISHED_OK) && (type == 'restore')) {\n // Process completed successfully.\n\n // Add in element class color to bar\n elementbar.classList.add('bg-success');\n elementbar.classList.add('complete');\n\n updateElement(backupid, type, 100);\n\n // We have a successful copy. Update the UI link to copied course.\n updateCopyTableRow(backupid);\n }\n\n });\n }\n\n /**\n * Get the progress of the backup process via ajax.\n */\n function getBackupProgress() {\n ajax.call([{\n // Get the backup progress via webservice.\n methodname: 'core_backup_get_async_backup_progress',\n args: {\n 'backupids': [backupid],\n 'contextid': contextid\n },\n }], true, true, false, timeout)[0].done(function(response) {\n // We have the progress now update the UI.\n updateProgress(response[0]);\n checkdelay = checkdelayoriginal;\n backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelayoriginal);\n }).fail(function() {\n checkdelay = checkdelay * checkdelaymultipler;\n backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelay);\n });\n }\n\n /**\n * Get the progress of all backup processes via ajax.\n */\n function getAllBackupProgress() {\n var backupids = [];\n var progressbars = $('.progress').find('.progress-bar').not('.complete');\n\n progressbars.each(function() {\n backupids.push((this.id).substring(0, 32));\n });\n\n if (backupids.length > 0) {\n ajax.call([{\n // Get the backup progress via webservice.\n methodname: 'core_backup_get_async_backup_progress',\n args: {\n 'backupids': backupids,\n 'contextid': contextid\n },\n }], true, true, false, timeout)[0].done(function(response) {\n updateProgressAll(response);\n checkdelay = checkdelayoriginal;\n allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelayoriginal);\n }).fail(function() {\n checkdelay = checkdelay * checkdelaymultipler;\n allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelay);\n });\n } else {\n clearInterval(allbackupintervalid); // No more progress bars to update, stop checking.\n }\n }\n\n /**\n * Get the progress of all copy processes via ajax.\n */\n function getAllCopyProgress() {\n var copyids = [];\n var progressbars = $('.progress').find('.progress-bar').not('.complete');\n\n progressbars.each(function() {\n let progressvars = {\n 'backupid': this.dataset.backupid,\n 'restoreid': this.dataset.restoreid,\n 'operation': this.dataset.operation,\n };\n copyids.push(progressvars);\n });\n\n if (copyids.length > 0) {\n ajax.call([{\n // Get the copy progress via webservice.\n methodname: 'core_backup_get_copy_progress',\n args: {\n 'copies': copyids\n },\n }], true, true, false, timeout)[0].done(function(response) {\n updateProgressCopy(response);\n checkdelay = checkdelayoriginal;\n allcopyintervalid = updateInterval(allcopyintervalid, getAllCopyProgress, checkdelayoriginal);\n }).fail(function() {\n checkdelay = checkdelay * checkdelaymultipler;\n allcopyintervalid = updateInterval(allcopyintervalid, getAllCopyProgress, checkdelay);\n });\n } else {\n clearInterval(allcopyintervalid); // No more progress bars to update, stop checking.\n }\n }\n\n /**\n * Get status updates for all backups.\n *\n * @public\n * @param {number} context The context id.\n */\n Asyncbackup.asyncBackupAllStatus = function(context) {\n contextid = context;\n allbackupintervalid = setInterval(getAllBackupProgress, checkdelay);\n };\n\n /**\n * Get status updates for all course copies.\n *\n * @public\n */\n Asyncbackup.asyncCopyAllStatus = function() {\n allcopyintervalid = setInterval(getAllCopyProgress, checkdelay);\n };\n\n /**\n * Get status updates for backup.\n *\n * @public\n * @param {string} backup The backup record id.\n * @param {number} context The context id.\n * @param {string} restore The restore link.\n * @param {string} type The operation type (backup or restore).\n */\n Asyncbackup.asyncBackupStatus = function(backup, context, restore, type) {\n backupid = backup;\n contextid = context;\n restoreurl = restore;\n\n if (type == 'backup') {\n typeid = 'backup';\n } else {\n typeid = 'restore';\n }\n\n // Remove the links from the progress bar, no going back now.\n $('.backup_progress').children('a').removeAttr('href');\n\n // Periodically check for progress updates and update the UI as required.\n backupintervalid = setInterval(getBackupProgress, checkdelay);\n\n };\n\n return Asyncbackup;\n});\n"],"file":"async_backup.min.js"} \ No newline at end of file diff --git a/backup/util/ui/amd/src/async_backup.js b/backup/util/ui/amd/src/async_backup.js index b44e025a8ee76..59a4bc997ed00 100644 --- a/backup/util/ui/amd/src/async_backup.js +++ b/backup/util/ui/amd/src/async_backup.js @@ -48,23 +48,25 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' 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, percentage) { + function updateElement(backupid, type, percentage) { var percentagewidth = Math.round(percentage) + '%'; - var elementbar = $('#' + backupid + '_bar'); + var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0]; var percentagetext = percentage.toFixed(2) + '%'; // Set progress bar percentage indicators - elementbar.attr('aria-valuenow', percentagewidth); - elementbar.css('width', percentagewidth); - elementbar.text(percentagetext); + elementbar.setAttribute('aria-valuenow', percentagewidth); + elementbar.style.width = percentagewidth; + elementbar.innerHTML = percentagetext; } /** @@ -160,6 +162,56 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' }); } + /** + * 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. @@ -168,7 +220,8 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' */ function updateProgress(progress) { var percentage = progress.progress * 100; - var elementbar = $('#' + backupid + '_bar'); + 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'); @@ -176,16 +229,16 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' if (progress.status == STATUS_EXECUTING) { // Process is in progress. - // Add in progress class color to bar - elementbar.addClass('bg-success'); + // Add in progress class color to bar. + elementbar.classList.add('bg-success'); - updateElement(backupid, percentage); + updateElement(backupid, type, percentage); - // Change heading + // Change heading. var strProcessing = 'async' + typeid + 'processing'; Str.get_string(strProcessing, 'backup').then(function(title) { elementstatus.text(title); - return title; + return; }).catch(function() { notification.exception(new Error('Failed to load string: backup ' + strProcessing)); }); @@ -193,15 +246,15 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' } else if (progress.status == STATUS_FINISHED_ERR) { // Process completed with error. - // Add in fail class color to bar - elementbar.addClass('bg-danger'); + // Add in fail class color to bar. + elementbar.classList.add('bg-danger'); - // Remove in progress class color to bar - elementbar.removeClass('bg-success'); + // Remove in progress class color to bar. + elementbar.classList.remove('bg-success'); - updateElement(backupid, 100); + updateElement(backupid, type, 100); - // Change heading and text + // Change heading and text. var strStatus = 'async' + typeid + 'error'; var strStatusDetail = 'async' + typeid + 'errordetail'; stringRequests = [ @@ -212,7 +265,7 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' elementstatus.text(strings[0]); elementdetail.text(strings[1]); - return strings; + return; }) .catch(function() { notification.exception(new Error('Failed to load string')); @@ -229,15 +282,15 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' // Process completed successfully. // Add in progress class color to bar - elementbar.addClass('bg-success'); + elementbar.classList.add('bg-success'); - updateElement(backupid, 100); + 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 title; + return; }).catch(function() { notification.exception(new Error('Failed to load string: backup ' + strComplete)); }); @@ -262,7 +315,7 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' elementbutton.text(strings[1]); elementbutton.attr('href', response.restoreurl); - return strings; + return; }) .catch(function() { notification.exception(new Error('Failed to load string')); @@ -282,7 +335,7 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' elementbutton.text(strings[1]); elementbutton.attr('href', restoreurl); - return strings; + return; }) .catch(function() { notification.exception(new Error('Failed to load string')); @@ -301,7 +354,7 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' /** * Update the Moodle user interface with the progress of - * all the pending processes. + * all the pending processes for backup and restore operations. * * @param {object} progress The progress and status of the process. */ @@ -309,37 +362,37 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' progress.forEach(function(element) { var percentage = element.progress * 100; var backupid = element.backupid; - var elementbar = $('#' + backupid + '_bar'); 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.addClass('bg-success'); + elementbar.classList.add('bg-success'); - updateElement(backupid, percentage); + updateElement(backupid, type, percentage); } else if (element.status == STATUS_FINISHED_ERR) { // Process completed with error. // Add in fail class color to bar - elementbar.addClass('bg-danger'); - elementbar.addClass('complete'); + elementbar.classList.add('bg-danger'); + elementbar.classList.add('complete'); // Remove in element class color to bar - $('#' + backupid + '_bar').removeClass('bg-success'); + elementbar.classList.remove('bg-success'); - updateElement(backupid, 100); + updateElement(backupid, type, 100); } else if (element.status == STATUS_FINISHED_OK) { // Process completed successfully. // Add in element class color to bar - elementbar.addClass('bg-success'); - elementbar.addClass('complete'); + elementbar.classList.add('bg-success'); + elementbar.classList.add('complete'); - updateElement(backupid, 100); + updateElement(backupid, type, 100); // We have a successful backup. Update the UI with download and file details. if (type == 'backup') { @@ -353,6 +406,65 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' }); } + /** + * 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. */ @@ -407,6 +519,42 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' } } + /** + * Get the progress of all copy processes via ajax. + */ + function getAllCopyProgress() { + var copyids = []; + var progressbars = $('.progress').find('.progress-bar').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. * @@ -418,6 +566,15 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates' allbackupintervalid = setInterval(getAllBackupProgress, checkdelay); }; + /** + * Get status updates for all course copies. + * + * @public + */ + Asyncbackup.asyncCopyAllStatus = function() { + allcopyintervalid = setInterval(getAllCopyProgress, checkdelay); + }; + /** * Get status updates for backup. * diff --git a/backup/util/ui/classes/copy/copy.php b/backup/util/ui/classes/copy/copy.php new file mode 100644 index 0000000000000..3e2d269fd7046 --- /dev/null +++ b/backup/util/ui/classes/copy/copy.php @@ -0,0 +1,309 @@ +. + +/** + * Course copy class. + * + * Handles procesing data submitted by UI copy form + * and sets up the course copy process. + * + * @package core_backup + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_backup\copy; + +defined('MOODLE_INTERNAL') || die; + +global $CFG; +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + +/** + * Course copy class. + * + * Handles procesing data submitted by UI copy form + * and sets up the course copy process. + * + * @package core_backup + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class copy { + + /** + * The fields required for copy operations. + * + * @var array + */ + private $copyfields = array( + 'courseid', // Course id integer. + 'fullname', // Fullname of the destination course. + 'shortname', // Shortname of the destination course. + 'category', // Category integer ID that contains the destination course. + 'visible', // Integer to detrmine of the copied course will be visible. + 'startdate', // Integer timestamp of the start of the destination course. + 'enddate', // Integer timestamp of the end of the destination course. + 'idnumber', // ID of the destination course. + 'userdata', // Integer to determine if the copied course will contain user data. + ); + + /** + * Data required for course copy operations. + * + * @var array + */ + private $copydata = array(); + + /** + * List of role ids to keep enrolments for in the destination course. + * + * @var array + */ + private $roles = array(); + + /** + * Constructor for the class. + * + * @param \stdClass $formdata Data from the validated course copy form. + */ + public function __construct(\stdClass $formdata) { + $this->copydata = $this->get_copy_data($formdata); + $this->roles = $this->get_enrollment_roles($formdata); + } + + /** + * Extract the enrolment roles to keep in the copied course + * from the raw submitted form data. + * + * @param \stdClass $formdata Data from the validated course copy form. + * @return array $keptroles The roles to keep. + */ + private final function get_enrollment_roles(\stdClass $formdata): array { + $keptroles = array(); + + foreach ($formdata as $key => $value) { + if ((substr($key, 0, 5 ) === 'role_') && ($value != 0)) { + $keptroles[] = $value; + } + } + + return $keptroles; + } + + /** + * Take the validated form data and extract the required information for copy operations. + * + * @param \stdClass $formdata Data from the validated course copy form. + * @throws \moodle_exception + * @return \stdClass $copydata Data required for course copy operations. + */ + private final function get_copy_data(\stdClass $formdata): \stdClass { + $copydata = new \stdClass(); + + foreach ($this->copyfields as $field) { + if (isset($formdata->{$field})) { + $copydata->{$field} = $formdata->{$field}; + } else { + throw new \moodle_exception('copy_class_field_not_found'); + } + } + + return $copydata; + } + + /** + * Creates a course copy. + * Sets up relevant controllers and adhoc task. + * + * @return array $copyids THe backup and restore controller ids. + */ + public function create_copy(): array { + global $USER; + $copyids = array(); + + // Create the initial backupcontoller. + $bc = new \backup_controller(\backup::TYPE_1COURSE, $this->copydata->courseid, \backup::FORMAT_MOODLE, + \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES); + $copyids['backupid'] = $bc->get_backupid(); + + // Create the initial restore contoller. + list($fullname, $shortname) = \restore_dbops::calculate_course_names( + 0, get_string('copyingcourse', 'backup'), get_string('copyingcourseshortname', 'backup')); + $newcourseid = \restore_dbops::create_new_course($fullname, $shortname, $this->copydata->category); + $rc = new \restore_controller($copyids['backupid'], $newcourseid, + \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, + \backup::TARGET_NEW_COURSE); + $copyids['restoreid'] = $rc->get_restoreid(); + + // Configure the controllers based on the submitted data. + $copydata = $this->copydata; + $copydata->copyids = $copyids; + $copydata->keptroles = $this->roles; + $bc->set_copy($copydata); + $bc->set_status(\backup::STATUS_AWAITING); + $bc->get_status(); + + $rc->set_copy($copydata); + $rc->save_controller(); + + // Create the ad-hoc task to perform the course copy. + $asynctask = new \core\task\asynchronous_copy_task(); + $asynctask->set_blocking(false); + $asynctask->set_custom_data($copyids); + \core\task\manager::queue_adhoc_task($asynctask); + + // Clean up the controller. + $bc->destroy(); + + return $copyids; + } + + /** + * Filters an array of copy records by course ID. + * + * @param array $copyrecords + * @param int $courseid + * @return array $copies Filtered array of records. + */ + static private function filter_copies_course(array $copyrecords, int $courseid): array { + $copies = array(); + + foreach ($copyrecords as $copyrecord) { + if ($copyrecord->operation == \backup::OPERATION_RESTORE) { // Restore records. + if ($copyrecord->status == \backup::STATUS_FINISHED_OK + || $copyrecord->status == \backup::STATUS_FINISHED_ERR) { + continue; + } else { + $rc = \restore_controller::load_controller($copyrecord->restoreid); + if ($rc->get_copy()->courseid == $courseid) { + $copies[] = $copyrecord; + } + } + } else { // Backup records. + if ($copyrecord->itemid == $courseid) { + $copies[] = $copyrecord; + } + } + } + return $copies; + } + + /** + * Get the in progress course copy operations for a user. + * + * @param int $userid User id to get the course copies for. + * @param int $courseid The optional source course id to get copies for. + * @return array $copies Details of the inprogress copies. + */ + static public function get_copies(int $userid, int $courseid=0): array { + global $DB; + $copies = array(); + $params = array($userid, \backup::EXECUTION_DELAYED, \backup::MODE_COPY); + $sql = 'SELECT bc.backupid, bc.itemid, bc.operation, bc.status, bc.timecreated + FROM {backup_controllers} bc + INNER JOIN {course} c ON bc.itemid = c.id + WHERE bc.userid = ? + AND bc.execution = ? + AND bc.purpose = ? + ORDER BY bc.timecreated DESC'; + + $copyrecords = $DB->get_records_sql($sql, $params); + + foreach ($copyrecords as $copyrecord) { + $copy = new \stdClass(); + $copy->itemid = $copyrecord->itemid; + $copy->time = $copyrecord->timecreated; + $copy->operation = $copyrecord->operation; + $copy->status = $copyrecord->status; + $copy->backupid = null; + $copy->restoreid = null; + + if ($copyrecord->operation == \backup::OPERATION_RESTORE) { + $copy->restoreid = $copyrecord->backupid; + // If record is complete or complete with errors, it means the backup also completed. + // It also means there are no controllers. In this case just skip and move on. + if ($copyrecord->status == \backup::STATUS_FINISHED_OK + || $copyrecord->status == \backup::STATUS_FINISHED_ERR) { + continue; + } else if ($copyrecord->status > \backup::STATUS_REQUIRE_CONV) { + // If record is a restore and it's in progress (>200), it means the backup is finished. + // In this case return the restore. + $rc = \restore_controller::load_controller($copyrecord->backupid); + $course = get_course($rc->get_copy()->courseid); + + $copy->source = $course->shortname; + $copy->sourceid = $course->id; + $copy->destination = $rc->get_copy()->shortname; + $copy->backupid = $rc->get_copy()->copyids['backupid']; + $rc->destroy(); + + } else if ($copyrecord->status == \backup::STATUS_REQUIRE_CONV) { + // If record is a restore and it is waiting (=200), load the controller + // and check the status of the backup. + // If the backup has finished successfully we have and edge case. Process as per in progress restore. + // If the backup has any other code it will be handled by backup processing. + $rc = \restore_controller::load_controller($copyrecord->backupid); + $bcid = $rc->get_copy()->copyids['backupid']; + if (empty($copyrecords[$bcid])) { + continue; + } + $backuprecord = $copyrecords[$bcid]; + $backupstatus = $backuprecord->status; + if ($backupstatus == \backup::STATUS_FINISHED_OK) { + $course = get_course($rc->get_copy()->courseid); + + $copy->source = $course->shortname; + $copy->sourceid = $course->id; + $copy->destination = $rc->get_copy()->shortname; + $copy->backupid = $rc->get_copy()->copyids['backupid']; + } else { + continue; + } + } + } else { // Record is a backup. + $copy->backupid = $copyrecord->backupid; + if ($copyrecord->status == \backup::STATUS_FINISHED_OK + || $copyrecord->status == \backup::STATUS_FINISHED_ERR) { + // If successfully finished then skip it. Restore procesing will look after it. + // If it has errored then we can't go any further. + continue; + } else { + // If is in progress then process it. + $bc = \backup_controller::load_controller($copyrecord->backupid); + $course = get_course($bc->get_courseid()); + + $copy->source = $course->shortname; + $copy->sourceid = $course->id; + $copy->destination = $bc->get_copy()->shortname; + $copy->restoreid = $bc->get_copy()->copyids['restoreid']; + } + } + + $copies[] = $copy; + } + + // Extra processing to filter records for a given course. + if ($courseid != 0 ) { + $copies = self::filter_copies_course($copies, $courseid); + } + + return $copies; + } +} diff --git a/backup/util/ui/classes/output/copy_form.php b/backup/util/ui/classes/output/copy_form.php new file mode 100644 index 0000000000000..ff7a55dea6348 --- /dev/null +++ b/backup/util/ui/classes/output/copy_form.php @@ -0,0 +1,235 @@ +. + +/** + * Course copy form class. + * + * @package core_backup + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_backup\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->libdir/formslib.php"); + +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + +/** + * Course copy form class. + * + * @package core_backup + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class copy_form extends \moodleform { + + /** + * Build form for the course copy settings. + * + * {@inheritDoc} + * @see \moodleform::definition() + */ + public function definition() { + global $CFG, $OUTPUT, $USER; + + $mform = $this->_form; + $course = $this->_customdata['course']; + $coursecontext = \context_course::instance($course->id); + $courseconfig = get_config('moodlecourse'); + $returnto = $this->_customdata['returnto']; + $returnurl = $this->_customdata['returnurl']; + + if (empty($course->category)) { + $course->category = $course->categoryid; + } + + // Course ID. + $mform->addElement('hidden', 'courseid', $course->id); + $mform->setType('courseid', PARAM_INT); + + // Return to type. + $mform->addElement('hidden', 'returnto', null); + $mform->setType('returnto', PARAM_ALPHANUM); + $mform->setConstant('returnto', $returnto); + + // Notifications of current copies. + $copies = \core_backup\copy\copy::get_copies($USER->id, $course->id); + if (!empty($copies)) { + $progresslink = new \moodle_url('/backup/copyprogress.php?', array('id' => $course->id)); + $notificationmsg = get_string('copiesinprogress', 'backup', $progresslink->out()); + $notification = $OUTPUT->notification($notificationmsg, 'notifymessage'); + $mform->addElement('html', $notification); + } + + // Return to URL. + $mform->addElement('hidden', 'returnurl', null); + $mform->setType('returnurl', PARAM_LOCALURL); + $mform->setConstant('returnurl', $returnurl); + + // Form heading. + $mform->addElement('html', \html_writer::div(get_string('copycoursedesc', 'backup'), 'form-description mb-3')); + + // Course fullname. + $mform->addElement('text', 'fullname', get_string('fullnamecourse'), 'maxlength="254" size="50"'); + $mform->addHelpButton('fullname', 'fullnamecourse'); + $mform->addRule('fullname', get_string('missingfullname'), 'required', null, 'client'); + $mform->setType('fullname', PARAM_TEXT); + + // Course shortname. + $mform->addElement('text', 'shortname', get_string('shortnamecourse'), 'maxlength="100" size="20"'); + $mform->addHelpButton('shortname', 'shortnamecourse'); + $mform->addRule('shortname', get_string('missingshortname'), 'required', null, 'client'); + $mform->setType('shortname', PARAM_TEXT); + + // Course category. + $displaylist = \core_course_category::make_categories_list(\core_course\management\helper::get_course_copy_capabilities()); + if (!isset($displaylist[$course->category])) { + // Always keep current category. + $displaylist[$course->category] = \core_course_category::get($course->category, MUST_EXIST, true)->get_formatted_name(); + } + $mform->addElement('select', 'category', get_string('coursecategory'), $displaylist); + $mform->addHelpButton('category', 'coursecategory'); + + // Course visibility. + $choices = array(); + $choices['0'] = get_string('hide'); + $choices['1'] = get_string('show'); + $mform->addElement('select', 'visible', get_string('coursevisibility'), $choices); + $mform->addHelpButton('visible', 'coursevisibility'); + $mform->setDefault('visible', $courseconfig->visible); + if (!has_capability('moodle/course:visibility', $coursecontext)) { + $mform->hardFreeze('visible'); + $mform->setConstant('visible', $course->visible); + } + + // Course start date. + $mform->addElement('date_time_selector', 'startdate', get_string('startdate')); + $mform->addHelpButton('startdate', 'startdate'); + $date = (new \DateTime())->setTimestamp(usergetmidnight(time())); + $date->modify('+1 day'); + $mform->setDefault('startdate', $date->getTimestamp()); + + // Course enddate. + $mform->addElement('date_time_selector', 'enddate', get_string('enddate'), array('optional' => true)); + $mform->addHelpButton('enddate', 'enddate'); + + if (!empty($CFG->enablecourserelativedates)) { + $attributes = [ + 'aria-describedby' => 'relativedatesmode_warning' + ]; + if (!empty($course->id)) { + $attributes['disabled'] = true; + } + $relativeoptions = [ + 0 => get_string('no'), + 1 => get_string('yes'), + ]; + $relativedatesmodegroup = []; + $relativedatesmodegroup[] = $mform->createElement('select', 'relativedatesmode', get_string('relativedatesmode'), + $relativeoptions, $attributes); + $relativedatesmodegroup[] = $mform->createElement('html', \html_writer::span(get_string('relativedatesmode_warning'), + '', ['id' => 'relativedatesmode_warning'])); + $mform->addGroup($relativedatesmodegroup, 'relativedatesmodegroup', get_string('relativedatesmode'), null, false); + $mform->addHelpButton('relativedatesmodegroup', 'relativedatesmode'); + } + + // Course id number. + $mform->addElement('text', 'idnumber', get_string('idnumbercourse'), 'maxlength="100" size="10"'); + $mform->addHelpButton('idnumber', 'idnumbercourse'); + $mform->setType('idnumber', PARAM_RAW); + if (!empty($course->id) and !has_capability('moodle/course:changeidnumber', $coursecontext)) { + $mform->hardFreeze('idnumber'); + $mform->setConstants('idnumber', $course->idnumber); + } + + // Keep source course user data. + $requiredcapabilities = array( + 'moodle/restore:createuser', 'moodle/backup:userinfo', 'moodle/restore:userinfo' + ); + if (has_all_capabilities($requiredcapabilities, $coursecontext)) { + $dataarray = array(); + $dataarray[] = $mform->createElement('advcheckbox', 'userdata', + get_string('enable'), '', array('group' => 1), array(0, 1)); + $mform->addGroup($dataarray, 'dataarray', get_string('userdata', 'backup'), ' ', false); + $mform->addHelpButton('dataarray', 'userdata', 'backup'); + } + + // Keep manual enrolments. + // Only get roles actually used in this course. + $roles = role_fix_names(get_roles_used_in_context($coursecontext, false), $coursecontext); + + // Only add the option if there are roles in this course. + if (!empty($roles) && has_capability('moodle/restore:createuser', $coursecontext)) { + $rolearray = array(); + foreach ($roles as $role) { + $roleid = 'role_' . $role->id; + $rolearray[] = $mform->createElement('advcheckbox', $roleid, + $role->localname, '', array('group' => 2), array(0, $role->id)); + } + + $mform->addGroup($rolearray, 'rolearray', get_string('keptroles', 'backup'), ' ', false); + $mform->addHelpButton('rolearray', 'keptroles', 'backup'); + $this->add_checkbox_controller(2); + } + + $buttonarray = array(); + $buttonarray[] = $mform->createElement('submit', 'submitreturn', get_string('copyreturn', 'backup')); + $buttonarray[] = $mform->createElement('submit', 'submitdisplay', get_string('copyview', 'backup')); + $buttonarray[] = $mform->createElement('cancel'); + $mform->addGroup($buttonarray, 'buttonar', '', ' ', false); + + } + + /** + * Validation of the form. + * + * @param array $data + * @param array $files + * @return array the errors that were found + */ + public function validation($data, $files) { + global $DB; + $errors = parent::validation($data, $files); + + // Add field validation check for duplicate shortname. + $courseshortname = $DB->get_record('course', array('shortname' => $data['shortname']), 'fullname', IGNORE_MULTIPLE); + if ($courseshortname) { + $errors['shortname'] = get_string('shortnametaken', '', $courseshortname->fullname); + } + + // Add field validation check for duplicate idnumber. + if (!empty($data['idnumber'])) { + $courseidnumber = $DB->get_record('course', array('idnumber' => $data['idnumber']), 'fullname', IGNORE_MULTIPLE); + if ($courseidnumber) { + $errors['idnumber'] = get_string('courseidnumbertaken', 'error', $courseidnumber->fullname); + } + } + + // Validate the dates (make sure end isn't greater than start). + if ($errorcode = course_validate_dates($data)) { + $errors['enddate'] = get_string($errorcode, 'error'); + } + + return $errors; + } + +} diff --git a/backup/util/ui/renderer.php b/backup/util/ui/renderer.php index d9bdbe1fa325f..d5b44ce9189d4 100644 --- a/backup/util/ui/renderer.php +++ b/backup/util/ui/renderer.php @@ -572,11 +572,15 @@ public function backup_files_viewer(array $options = null) { * @param string $backupid The backup record id. * @return string|boolean $status The status indicator for the operation. */ - public function get_status_display($statuscode, $backupid) { - if ($statuscode == backup::STATUS_AWAITING || $statuscode == backup::STATUS_EXECUTING) { // Inprogress. + public function get_status_display($statuscode, $backupid, $restoreid=null, $operation='backup') { + if ($statuscode == backup::STATUS_AWAITING + || $statuscode == backup::STATUS_EXECUTING + || $statuscode == backup::STATUS_REQUIRE_CONV) { // In progress. $progresssetup = array( - 'backupid' => $backupid, - 'width' => '100' + 'backupid' => $backupid, + 'restoreid' => $restoreid, + 'operation' => $operation, + 'width' => '100' ); $status = $this->render_from_template('core/async_backup_progress', $progresssetup); } else if ($statuscode == backup::STATUS_FINISHED_ERR) { // Error. @@ -963,7 +967,7 @@ public function restore_progress_viewer ($userid, $context) { $restorename = \async_helper::get_restore_name($context); $timecreated = $restore->timecreated; - $status = $this->get_status_display($restore->status, $restore->backupid); + $status = $this->get_status_display($restore->status, $restore->backupid, $restore->backupid, null, 'restore'); $tablerow = array($restorename, userdate($timecreated), $status); $tabledata[] = $tablerow; @@ -974,6 +978,50 @@ public function restore_progress_viewer ($userid, $context) { return $html; } + + /** + * Get markup to render table for all of a users course copies. + * + * @param int $userid The Moodle user id. + * @param int $courseid The id of the course to get the backups for. + * @return string $html The table HTML. + */ + public function copy_progress_viewer(int $userid, int $courseid): string { + $tablehead = array( + get_string('copysource', 'backup'), + get_string('copydest', 'backup'), + get_string('time'), + get_string('copyop', 'backup'), + get_string('status', 'backup') + ); + + $table = new html_table(); + $table->attributes['class'] = 'backup-files-table generaltable'; + $table->head = $tablehead; + + $tabledata = array(); + + // Get all in progress course copies for this user. + $copies = \core_backup\copy\copy::get_copies($userid, $courseid); + + foreach ($copies as $copy) { + $sourceurl = new \moodle_url('/course/view.php', array('id' => $copy->sourceid)); + + $tablerow = array( + html_writer::link($sourceurl, $copy->source), + $copy->destination, + userdate($copy->time), + get_string($copy->operation), + $this->get_status_display($copy->status, $copy->backupid, $copy->restoreid, $copy->operation) + ); + $tabledata[] = $tablerow; + } + + $table->data = $tabledata; + $html = html_writer::table($table); + + return $html; + } } /** diff --git a/course/amd/build/copy_modal.min.js b/course/amd/build/copy_modal.min.js new file mode 100644 index 0000000000000..e67d350bffc73 --- /dev/null +++ b/course/amd/build/copy_modal.min.js @@ -0,0 +1,2 @@ +define ("core_course/copy_modal",["jquery","core/str","core/modal_factory","core/modal_events","core/ajax","core/fragment","core/notification","core/config"],function(a,b,c,d,f,g,h,i){var m={},n,o,p,q="

";function j(){b.get_string("loading").then(function(a){c.create({type:c.types.DEFAULT,title:a,body:q,large:!0}).done(function(a){p=a;p.getRoot().on("click","#id_submitreturn",l);p.getRoot().on("click","#id_submitdisplay",function(a){a.formredirect=!0;l(a)});p.getRoot().on("click","#id_cancel",function(a){a.preventDefault();p.setBody(q);p.hide()})})}).catch(function(){h.exception(new Error("Failed to load string: loading"))})}function k(a){if("undefined"==typeof a){a={}}var c={jsonformdata:JSON.stringify(a),courseid:o.id};p.setBody(q);b.get_string("copycoursetitle","backup",o.shortname).then(function(a){p.setTitle(a);p.setBody(g.loadFragment("course","new_base_form",n,c))}).catch(function(){h.exception(new Error("Failed to load string: copycoursetitle"))})}function l(b){b.preventDefault();var c=p.getRoot().find("form").serialize(),d=JSON.stringify(c),e=a.merge(p.getRoot().find("[aria-invalid=\"true\"]"),p.getRoot().find(".error"));if(e.length){e.first().focus();return}f.call([{methodname:"core_backup_submit_copy_form",args:{jsonformdata:d}}])[0].done(function(){p.setBody(q);p.hide();if(!0==b.formredirect){var a=i.wwwroot+"/backup/copyprogress.php?id="+o.id;window.location.assign(a)}}).fail(function(){k(c)})}m.init=function(b){n=b;j();a(".action-copy").on("click",function(a){a.preventDefault();var b=new URL(this.getAttribute("href")),c=new URLSearchParams(b.search),d=c.get("id");f.call([{methodname:"core_course_get_courses",args:{options:{ids:[d]}}}])[0].done(function(a){o=a[0];k()}).fail(function(){h.exception(new Error("Failed to load course"))});p.show()})};return m}); +//# sourceMappingURL=copy_modal.min.js.map diff --git a/course/amd/build/copy_modal.min.js.map b/course/amd/build/copy_modal.min.js.map new file mode 100644 index 0000000000000..3ec3b14aeeac8 --- /dev/null +++ b/course/amd/build/copy_modal.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/copy_modal.js"],"names":["define","$","Str","ModalFactory","ModalEvents","ajax","Fragment","Notification","Config","CopyModal","contextid","course","modalObj","spinner","createModal","get_string","then","title","create","type","types","DEFAULT","body","large","done","modal","getRoot","on","processModalForm","e","formredirect","preventDefault","setBody","hide","catch","exception","Error","updateModalBody","formdata","params","JSON","stringify","id","shortname","setTitle","loadFragment","copyform","find","serialize","formjson","invalid","merge","length","first","focus","call","methodname","args","jsonformdata","redirect","wwwroot","window","location","assign","fail","init","context","url","URL","getAttribute","URLSearchParams","search","courseid","get","response","show"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module provides the course copy modal from the course and\n * category management screen.\n *\n * @module course\n * @package core\n * @copyright 2020 onward The Moodle Users Association \n * @author Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.9\n */\n\ndefine(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events',\n 'core/ajax', 'core/fragment', 'core/notification', 'core/config'],\n function($, Str, ModalFactory, ModalEvents, ajax, Fragment, Notification, Config) {\n\n /**\n * Module level variables.\n */\n var CopyModal = {};\n var contextid;\n var course;\n var modalObj;\n var spinner = '

'\n + ''\n + '

';\n\n /**\n * Creates the modal for the course copy form\n *\n * @private\n */\n function createModal() {\n // Get the Title String.\n Str.get_string('loading').then(function(title) {\n // Create the Modal.\n ModalFactory.create({\n type: ModalFactory.types.DEFAULT,\n title: title,\n body: spinner,\n large: true\n })\n .done(function(modal) {\n modalObj = modal;\n // Explicitly handle form click events.\n modalObj.getRoot().on('click', '#id_submitreturn', processModalForm);\n modalObj.getRoot().on('click', '#id_submitdisplay', function(e) {\n e.formredirect = true;\n processModalForm(e);\n\n });\n modalObj.getRoot().on('click', '#id_cancel', function(e) {\n e.preventDefault();\n modalObj.setBody(spinner);\n modalObj.hide();\n });\n });\n return;\n }).catch(function() {\n Notification.exception(new Error('Failed to load string: loading'));\n });\n }\n\n /**\n * Updates the body of the modal window.\n *\n * @param {Object} formdata\n * @private\n */\n function updateModalBody(formdata) {\n if (typeof formdata === \"undefined\") {\n formdata = {};\n }\n\n var params = {\n 'jsonformdata': JSON.stringify(formdata),\n 'courseid': course.id\n };\n\n modalObj.setBody(spinner);\n Str.get_string('copycoursetitle', 'backup', course.shortname).then(function(title) {\n modalObj.setTitle(title);\n modalObj.setBody(Fragment.loadFragment('course', 'new_base_form', contextid, params));\n return;\n }).catch(function() {\n Notification.exception(new Error('Failed to load string: copycoursetitle'));\n });\n }\n\n /**\n * Updates Moodle form with selected information.\n *\n * @param {Object} e\n * @private\n */\n function processModalForm(e) {\n e.preventDefault(); // Stop modal from closing.\n\n // Form data.\n var copyform = modalObj.getRoot().find('form').serialize();\n var formjson = JSON.stringify(copyform);\n\n // Handle invalid form fields for better UX.\n var invalid = $.merge(\n modalObj.getRoot().find('[aria-invalid=\"true\"]'),\n modalObj.getRoot().find('.error')\n );\n\n if (invalid.length) {\n invalid.first().focus();\n return;\n }\n\n // Submit form via ajax.\n ajax.call([{\n methodname: 'core_backup_submit_copy_form',\n args: {\n jsonformdata: formjson\n },\n }])[0].done(function() {\n // For submission succeeded.\n modalObj.setBody(spinner);\n modalObj.hide();\n\n if (e.formredirect == true) {\n // We are redirecting to copy progress display.\n let redirect = Config.wwwroot + \"/backup/copyprogress.php?id=\" + course.id;\n window.location.assign(redirect);\n }\n\n }).fail(function() {\n // Form submission failed server side, redisplay with errors.\n updateModalBody(copyform);\n });\n }\n\n /**\n * Initialise the class.\n *\n * @param {Object} context\n * @public\n */\n CopyModal.init = function(context) {\n contextid = context;\n // Setup the initial Modal.\n createModal();\n\n // Setup the click handlers on the copy buttons.\n $('.action-copy').on('click', function(e) {\n e.preventDefault(); // Stop. Hammer time.\n let url = new URL(this.getAttribute('href'));\n let params = new URLSearchParams(url.search);\n let courseid = params.get('id');\n\n ajax.call([{ // Get the course information.\n methodname: 'core_course_get_courses',\n args: {\n 'options': {'ids': [courseid]},\n },\n }])[0].done(function(response) {\n // We have the course info get the modal content.\n course = response[0];\n updateModalBody();\n\n }).fail(function() {\n Notification.exception(new Error('Failed to load course'));\n });\n\n modalObj.show();\n });\n\n };\n\n return CopyModal;\n});\n"],"file":"copy_modal.min.js"} \ No newline at end of file diff --git a/course/amd/src/copy_modal.js b/course/amd/src/copy_modal.js new file mode 100644 index 0000000000000..dc1644c5977f1 --- /dev/null +++ b/course/amd/src/copy_modal.js @@ -0,0 +1,190 @@ +// 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 . + +/** + * This module provides the course copy modal from the course and + * category management screen. + * + * @module course + * @package core + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 3.9 + */ + +define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', + 'core/ajax', 'core/fragment', 'core/notification', 'core/config'], + function($, Str, ModalFactory, ModalEvents, ajax, Fragment, Notification, Config) { + + /** + * Module level variables. + */ + var CopyModal = {}; + var contextid; + var course; + var modalObj; + var spinner = '

' + + '' + + '

'; + + /** + * Creates the modal for the course copy form + * + * @private + */ + function createModal() { + // Get the Title String. + Str.get_string('loading').then(function(title) { + // Create the Modal. + ModalFactory.create({ + type: ModalFactory.types.DEFAULT, + title: title, + body: spinner, + large: true + }) + .done(function(modal) { + modalObj = modal; + // Explicitly handle form click events. + modalObj.getRoot().on('click', '#id_submitreturn', processModalForm); + modalObj.getRoot().on('click', '#id_submitdisplay', function(e) { + e.formredirect = true; + processModalForm(e); + + }); + modalObj.getRoot().on('click', '#id_cancel', function(e) { + e.preventDefault(); + modalObj.setBody(spinner); + modalObj.hide(); + }); + }); + return; + }).catch(function() { + Notification.exception(new Error('Failed to load string: loading')); + }); + } + + /** + * Updates the body of the modal window. + * + * @param {Object} formdata + * @private + */ + function updateModalBody(formdata) { + if (typeof formdata === "undefined") { + formdata = {}; + } + + var params = { + 'jsonformdata': JSON.stringify(formdata), + 'courseid': course.id + }; + + modalObj.setBody(spinner); + Str.get_string('copycoursetitle', 'backup', course.shortname).then(function(title) { + modalObj.setTitle(title); + modalObj.setBody(Fragment.loadFragment('course', 'new_base_form', contextid, params)); + return; + }).catch(function() { + Notification.exception(new Error('Failed to load string: copycoursetitle')); + }); + } + + /** + * Updates Moodle form with selected information. + * + * @param {Object} e + * @private + */ + function processModalForm(e) { + e.preventDefault(); // Stop modal from closing. + + // Form data. + var copyform = modalObj.getRoot().find('form').serialize(); + var formjson = JSON.stringify(copyform); + + // Handle invalid form fields for better UX. + var invalid = $.merge( + modalObj.getRoot().find('[aria-invalid="true"]'), + modalObj.getRoot().find('.error') + ); + + if (invalid.length) { + invalid.first().focus(); + return; + } + + // Submit form via ajax. + ajax.call([{ + methodname: 'core_backup_submit_copy_form', + args: { + jsonformdata: formjson + }, + }])[0].done(function() { + // For submission succeeded. + modalObj.setBody(spinner); + modalObj.hide(); + + if (e.formredirect == true) { + // We are redirecting to copy progress display. + let redirect = Config.wwwroot + "/backup/copyprogress.php?id=" + course.id; + window.location.assign(redirect); + } + + }).fail(function() { + // Form submission failed server side, redisplay with errors. + updateModalBody(copyform); + }); + } + + /** + * Initialise the class. + * + * @param {Object} context + * @public + */ + CopyModal.init = function(context) { + contextid = context; + // Setup the initial Modal. + createModal(); + + // Setup the click handlers on the copy buttons. + $('.action-copy').on('click', function(e) { + e.preventDefault(); // Stop. Hammer time. + let url = new URL(this.getAttribute('href')); + let params = new URLSearchParams(url.search); + let courseid = params.get('id'); + + ajax.call([{ // Get the course information. + methodname: 'core_course_get_courses', + args: { + 'options': {'ids': [courseid]}, + }, + }])[0].done(function(response) { + // We have the course info get the modal content. + course = response[0]; + updateModalBody(); + + }).fail(function() { + Notification.exception(new Error('Failed to load course')); + }); + + modalObj.show(); + }); + + }; + + return CopyModal; +}); diff --git a/course/classes/management/helper.php b/course/classes/management/helper.php index d055ddc178206..c4b9535eec469 100644 --- a/course/classes/management/helper.php +++ b/course/classes/management/helper.php @@ -375,6 +375,14 @@ public static function get_course_listitem_actions(\core_course_category $catego 'attributes' => array('class' => 'action-edit') ); } + // Copy. + if (self::can_copy_course($course->id)) { + $actions[] = array( + 'url' => new \moodle_url('/backup/copy.php', array('id' => $course->id, 'returnto' => 'catmanage')), + 'icon' => new \pix_icon('t/copy', \get_string('copycourse')), + 'attributes' => array('class' => 'action-copy') + ); + } // Delete. if ($course->can_delete()) { $actions[] = array( @@ -996,4 +1004,24 @@ public static function get_expanded_categories($withpath = null) { return array($parent); } } + + /** + * Get an array of the capabilities required to copy a course. + * + * @return array + */ + public static function get_course_copy_capabilities(): array { + return array('moodle/backup:backupcourse', 'moodle/restore:restorecourse', 'moodle/course:view', 'moodle/course:create'); + } + + /** + * Returns true if the current user can copy this course. + * + * @param int $courseid + * @return bool + */ + public static function can_copy_course(int $courseid): bool { + $coursecontext = \context_course::instance($courseid); + return has_all_capabilities(self::get_course_copy_capabilities(), $coursecontext); + } } diff --git a/course/lib.php b/course/lib.php index 7268824761a4d..5e783d56e46b7 100644 --- a/course/lib.php +++ b/course/lib.php @@ -4028,7 +4028,6 @@ function course_get_user_administration_options($course, $context) { $isfrontpage = $course->id == SITEID; $completionenabled = $CFG->enablecompletion && $course->enablecompletion; $hascompletiontabs = count(core_completion\manager::get_available_completion_tabs($course, $context)) > 0; - $options = new stdClass; $options->update = has_capability('moodle/course:update', $context); $options->editcompletion = $CFG->enablecompletion && @@ -4039,6 +4038,7 @@ function course_get_user_administration_options($course, $context) { $options->reports = has_capability('moodle/site:viewreports', $context); $options->backup = has_capability('moodle/backup:backupcourse', $context); $options->restore = has_capability('moodle/restore:restorecourse', $context); + $options->copy = \core_course\management\helper::can_copy_course($course->id); $options->files = ($course->legacyfiles == 2 && has_capability('moodle/course:managefiles', $context)); if (!$isfrontpage) { @@ -4944,3 +4944,40 @@ function course_get_course_dates_for_user_ids(stdClass $course, array $userids): function course_get_course_dates_for_user_id(stdClass $course, int $userid): array { return (course_get_course_dates_for_user_ids($course, [$userid]))[$userid]; } + +/** + * Renders the course copy form for the modal on the course management screen. + * + * @param array $args + * @return string $o Form HTML. + */ +function course_output_fragment_new_base_form($args) { + + $serialiseddata = json_decode($args['jsonformdata'], true); + $formdata = []; + if (!empty($serialiseddata)) { + parse_str($serialiseddata, $formdata); + } + + $context = context_course::instance($args['courseid']); + $copycaps = \core_course\management\helper::get_course_copy_capabilities(); + require_all_capabilities($copycaps, $context); + + $course = get_course($args['courseid']); + $mform = new \core_backup\output\copy_form( + null, + array('course' => $course, 'returnto' => '', 'returnurl' => ''), + 'post', '', ['class' => 'ignoredirty'], true, $formdata); + + if (!empty($serialiseddata)) { + // If we were passed non-empty form data we want the mform to call validation functions and show errors. + $mform->is_validated(); + } + + ob_start(); + $mform->display(); + $o = ob_get_contents(); + ob_end_clean(); + + return $o; +} diff --git a/course/management.php b/course/management.php index ad74a3e2f3b3d..39ad5a77b7b75 100644 --- a/course/management.php +++ b/course/management.php @@ -107,6 +107,7 @@ $PAGE->set_pagelayout('admin'); $PAGE->set_title($strmanagement); $PAGE->set_heading($pageheading); +$PAGE->requires->js_call_amd('core_course/copy_modal', 'init', array($context->id)); // This is a system level page that operates on other contexts. require_login(); diff --git a/course/tests/externallib_test.php b/course/tests/externallib_test.php index c1877c1c3d029..753544a0f2ba0 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -2283,7 +2283,7 @@ public function test_get_user_administration_options() { $adminoptions->{$option['name']} = $option['available']; } if ($course['id'] == SITEID) { - $this->assertCount(16, $course['options']); + $this->assertCount(17, $course['options']); $this->assertFalse($adminoptions->update); $this->assertFalse($adminoptions->filters); $this->assertFalse($adminoptions->reports); @@ -2298,8 +2298,9 @@ public function test_get_user_administration_options() { $this->assertFalse($adminoptions->reset); $this->assertFalse($adminoptions->roles); $this->assertFalse($adminoptions->editcompletion); + $this->assertFalse($adminoptions->copy); } else { - $this->assertCount(14, $course['options']); + $this->assertCount(15, $course['options']); $this->assertFalse($adminoptions->update); $this->assertFalse($adminoptions->filters); $this->assertFalse($adminoptions->reports); @@ -2314,6 +2315,7 @@ public function test_get_user_administration_options() { $this->assertFalse($adminoptions->reset); $this->assertFalse($adminoptions->roles); $this->assertFalse($adminoptions->editcompletion); + $this->assertFalse($adminoptions->copy); } } } diff --git a/course/view.php b/course/view.php index 6d65bc9c59409..611ed6c54b358 100644 --- a/course/view.php +++ b/course/view.php @@ -243,7 +243,7 @@ $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); - if ($USER->editing == 1 && !empty($CFG->enableasyncbackup)) { + if ($USER->editing == 1) { // MDL-65321 The backup libraries are quite heavy, only require the bare minimum. require_once($CFG->dirroot . '/backup/util/helper/async_helper.class.php'); diff --git a/lang/en/backup.php b/lang/en/backup.php index 0c08c50eb0a7d..1e363e565e2c6 100644 --- a/lang/en/backup.php +++ b/lang/en/backup.php @@ -160,6 +160,21 @@ $string['confirmcancelno'] = 'Do not cancel'; $string['confirmnewcoursecontinue'] = 'New course warning'; $string['confirmnewcoursecontinuequestion'] = 'A temporary (hidden) course will be created by the course restoration process. To abort restoration click cancel. Do not close the browser while restoring.'; +$string['copiesinprogress'] = 'This course has copies in progress. View in progress copies.'; +$string['copycoursedesc'] = 'This course will be duplicated and put into the given course category.'; +$string['copycourseheading'] = 'Copy a course'; +$string['copycoursetitle'] = 'Copy course: {$a}'; +$string['copydest'] = 'Destination'; +$string['copyingcourse'] = 'Course copying in progress'; +$string['copyingcourseshortname'] = 'copying'; +$string['copyformfail'] = 'Ajax submission of course copy form has failed.'; +$string['copyop'] = 'Current operation'; +$string['copyprogressheading'] = 'Course copies in progress'; +$string['copyprogressheading_help'] = 'This table shows the status of all of your unfinished course copies.'; +$string['copyprogresstitle'] = 'Course copy progress'; +$string['copyreturn'] = 'Copy and return'; +$string['copysource'] = 'Source'; +$string['copyview'] = 'Copy and view'; $string['coursecategory'] = 'Category the course will be restored into'; $string['courseid'] = 'Original ID'; $string['coursesettings'] = 'Course settings'; @@ -253,6 +268,8 @@ $string['lockedbyhierarchy'] = 'Locked by dependencies'; $string['loglifetime'] = 'Keep logs for'; $string['managefiles'] = 'Manage backup files'; +$string['keptroles'] = 'Keep enrolments of role'; +$string['keptroles_help'] = 'Select which roles and the users with those roles in the source course that you want to keep enrolments for in the new course. Any users with those roles will be copied into the new course.'; $string['missingfilesinpool'] = 'Some files could not be saved during the backup, and so it will not be possible to restore them.'; $string['moodleversion'] = 'Moodle version'; $string['moreresults'] = 'There are too many results, enter a more specific search.'; @@ -368,6 +385,7 @@ $string['skipmodifprevhelp'] = 'Choose whether to skip courses that have not been modified since the last automatic backup. This requires logging to be enabled.'; $string['status'] = 'Status'; $string['successful'] = 'Backup successful'; +$string['successfulcopy'] = 'Copy successful'; $string['successfulrestore'] = 'Restore successful'; $string['timetaken'] = 'Time taken'; $string['title'] = 'Title'; @@ -375,6 +393,8 @@ $string['totalcoursesearchresults'] = 'Total courses: {$a}'; $string['undefinedrolemapping'] = 'Role mapping undefined for \'{$a}\' archetype.'; $string['unnamedsection'] = 'Unnamed section'; +$string['userdata'] = 'Keep user data'; +$string['userdata_help'] = 'When enabled user generated data in the source course will be copied into the new course. This includes forum posts, assignment submissions, etc.'; $string['userinfo'] = 'Userinfo'; $string['module'] = 'Module'; $string['morecoursesearchresults'] = 'More than {$a} courses found, showing first {$a} results'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 4885cc4598ee3..6ba40104038fc 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -289,6 +289,7 @@ $string['cookiesnotenabled'] = 'Unfortunately, cookies are currently not enabled in your browser'; $string['copy'] = 'copy'; $string['copyasnoun'] = 'copy'; +$string['copycourse'] = 'Copy course'; $string['copyingcoursefiles'] = 'Copying course files'; $string['copyingsitefiles'] = 'Copying site files used in course'; $string['copyinguserfiles'] = 'Copying user files'; diff --git a/lib/accesslib.php b/lib/accesslib.php index 1e56b09d14570..eaefd6b8dac17 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -861,6 +861,34 @@ function require_capability($capability, context $context, $userid = null, $doan } } +/** + * A convenience function that tests has_capability for a list of capabilities, and displays an error if + * the user does not have that capability. + * + * This is just a utility method that calls has_capability in a loop. Try to put + * the capabilities that fewest users are likely to have first in the list for best + * performance. + * + * @category access + * @see has_capability() + * + * @param array $capabilities an array of capability names. + * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance(). + * @param int $userid A user id. By default (null) checks the permissions of the current user. + * @param bool $doanything If false, ignore effect of admin role assignment + * @param string $errormessage The error string to to user. Defaults to 'nopermissions'. + * @param string $stringfile The language file to load the error string from. Defaults to 'error'. + * @return void terminates with an error if the user does not have the given capability. + */ +function require_all_capabilities(array $capabilities, context $context, $userid = null, $doanything = true, + $errormessage = 'nopermissions', $stringfile = ''): void { + foreach ($capabilities as $capability) { + if (!has_capability($capability, $context, $userid, $doanything)) { + throw new required_capability_exception($context, $capability, $errormessage, $stringfile); + } + } +} + /** * Return a nested array showing all role assignments for the user. * [ra] => [contextpath][roleid] = roleid diff --git a/lib/classes/task/asynchronous_copy_task.php b/lib/classes/task/asynchronous_copy_task.php new file mode 100644 index 0000000000000..6504d408cdc45 --- /dev/null +++ b/lib/classes/task/asynchronous_copy_task.php @@ -0,0 +1,211 @@ +. + +/** + * Adhoc task that performs asynchronous course copies. + * + * @package core + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\task; + +use async_helper; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php'); +require_once($CFG->libdir . '/externallib.php'); + +/** + * Adhoc task that performs asynchronous course copies. + * + * @package core + * @copyright 2020 onward The Moodle Users Association + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class asynchronous_copy_task extends adhoc_task { + + /** + * Run the adhoc task and preform the backup. + */ + public function execute() { + global $CFG, $DB; + $started = time(); + + $backupid = $this->get_custom_data()->backupid; + $restoreid = $this->get_custom_data()->restoreid; + $backuprecord = $DB->get_record('backup_controllers', array('backupid' => $backupid), 'id, itemid', MUST_EXIST); + $restorerecord = $DB->get_record('backup_controllers', array('backupid' => $restoreid), 'id, itemid', MUST_EXIST); + + // First backup the course. + mtrace('Course copy: Processing asynchronous course copy for course id: ' . $backuprecord->itemid); + try { + $bc = \backup_controller::load_controller($backupid); // Get the backup controller by backup id. + } catch (\backup_dbops_exception $e) { + mtrace('Course copy: Can not load backup controller for copy, marking job as failed'); + delete_course($restorerecord->itemid, false); // Clean up partially created destination course. + return; // Return early as we can't continue. + } + $bc->set_progress(new \core\progress\db_updater($backuprecord->id, 'backup_controllers', 'progress')); + $copyinfo = $bc->get_copy(); + $backupplan = $bc->get_plan(); + + $keepuserdata = (bool)$copyinfo->userdata; + $keptroles = $copyinfo->keptroles; + + $backupplan->get_setting('users')->set_value('1'); + $bc->set_kept_roles($keptroles); + + // If we are not keeping user data don't include users or data in the backup. + // In this case we'll add the user enrolments at the end. + // Also if we have no roles to keep don't backup users. + if (empty($keptroles) || !$keepuserdata) { + $backupplan->get_setting('users')->set_value('0'); + } + + // Do some preflight checks on the backup. + $status = $bc->get_status(); + $execution = $bc->get_execution(); + // Check that the backup is in the correct status and + // that is set for asynchronous execution. + if ($status == \backup::STATUS_AWAITING && $execution == \backup::EXECUTION_DELAYED) { + // Execute the backup. + mtrace('Course copy: Backing up course, id: ' . $backuprecord->itemid); + $bc->execute_plan(); + + } else { + // If status isn't 700, it means the process has failed. + // Retrying isn't going to fix it, so marked operation as failed. + mtrace('Course copy: Bad backup controller status, is: ' . $status . ' should be 700, marking job as failed.'); + $bc->set_status(\backup::STATUS_FINISHED_ERR); + delete_course($restorerecord->itemid, false); // Clean up partially created destination course. + $bc->destroy(); + return; // Return early as we can't continue. + + } + + $results = $bc->get_results(); + $backupbasepath = $backupplan->get_basepath(); + $file = $results['backup_destination']; + $file->extract_to_pathname(get_file_packer('application/vnd.moodle.backup'), $backupbasepath); + // Start the restore process. + $rc = \restore_controller::load_controller($restoreid); // Get the restore controller by restore id. + $rc->set_progress(new \core\progress\db_updater($restorerecord->id, 'backup_controllers', 'progress')); + $rc->prepare_copy(); + + // Set the course settings we can do now (the remaining settings will be done after restore completes). + $plan = $rc->get_plan(); + + $startdate = $plan->get_setting('course_startdate'); + $startdate->set_value($copyinfo->startdate); + $fullname = $plan->get_setting('course_fullname'); + $fullname->set_value($copyinfo->fullname); + $shortname = $plan->get_setting('course_shortname'); + $shortname->set_value($copyinfo->shortname); + + // Do some preflight checks on the restore. + $rc->execute_precheck(); + $status = $rc->get_status(); + $execution = $rc->get_execution(); + + // Check that the restore is in the correct status and + // that is set for asynchronous execution. + if ($status == \backup::STATUS_AWAITING && $execution == \backup::EXECUTION_DELAYED) { + // Execute the restore. + mtrace('Course copy: Restoring into course, id: ' . $restorerecord->itemid); + $rc->execute_plan(); + + } else { + // If status isn't 700, it means the process has failed. + // Retrying isn't going to fix it, so marked operation as failed. + mtrace('Course copy: Bad backup controller status, is: ' . $status . ' should be 700, marking job as failed.'); + $rc->set_status(\backup::STATUS_FINISHED_ERR); + delete_course($restorerecord->itemid, false); // Clean up partially created destination course. + $file->delete(); + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + $rc->destroy(); + return; // Return early as we can't continue. + + } + + // Copy user enrolments from source course to destination. + if (!empty($keptroles) && !$keepuserdata) { + mtrace('Course copy: Creating user enrolments in destination course.'); + $context = \context_course::instance($backuprecord->itemid); + + $enrol = enrol_get_plugin('manual'); + $instance = null; + $enrolinstances = enrol_get_instances($restorerecord->itemid, true); + foreach ($enrolinstances as $courseenrolinstance) { + if ($courseenrolinstance->enrol == 'manual') { + $instance = $courseenrolinstance; + break; + } + } + + // Abort if there enrolment plugin problems. + if (empty($enrol) || empty($instance)) { + mtrace('Course copy: Could not enrol users in course.');; + delete_course($restorerecord->itemid, false); + return; + } + + // Enrol the users from the source course to the destination. + foreach ($keptroles as $roleid) { + $sourceusers = get_role_users($roleid, $context); + foreach ($sourceusers as $sourceuser) { + $enrol->enrol_user($instance, $sourceuser->id, $roleid); + } + } + } + + // Set up remaining course settings. + $course = $DB->get_record('course', array('id' => $restorerecord->itemid), '*', MUST_EXIST); + $course->visible = $copyinfo->visible; + $course->idnumber = $copyinfo->idnumber; + $course->enddate = $copyinfo->enddate; + + $DB->update_record('course', $course); + + // Send message to user if enabled. + $messageenabled = (bool)get_config('backup', 'backup_async_message_users'); + if ($messageenabled && $rc->get_status() == \backup::STATUS_FINISHED_OK) { + mtrace('Course copy: Sending user notification.'); + $asynchelper = new async_helper('copy', $restoreid); + $messageid = $asynchelper->send_message(); + mtrace('Course copy: Sent message: ' . $messageid); + } + + // Cleanup. + $bc->destroy(); + $rc->destroy(); + $file->delete(); + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + + $duration = time() - $started; + mtrace('Course copy: Copy completed in: ' . $duration . ' seconds'); + } +} diff --git a/lib/db/services.php b/lib/db/services.php index 2c4c81d6dc7d1..e8c1350e4b126 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -101,6 +101,24 @@ 'ajax' => true, 'loginrequired' => true, ), + 'core_backup_get_copy_progress' => array( + 'classname' => 'core_backup_external', + 'classpath' => 'backup/externallib.php', + 'methodname' => 'get_copy_progress', + 'description' => 'Gets the progress of course copy operations.', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => true, + ), + 'core_backup_submit_copy_form' => array( + 'classname' => 'core_backup_external', + 'classpath' => 'backup/externallib.php', + 'methodname' => 'submit_copy_form', + 'description' => 'Handles ajax submission of course copy form.', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => true, + ), 'core_badges_get_user_badges' => array( 'classname' => 'core_badges_external', 'methodname' => 'get_user_badges', diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 02a55e250065a..2e8d3fc94fcd5 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -4546,6 +4546,12 @@ protected function load_course_settings($forceopen = false) { $coursenode->add(get_string('import'), $url, self::TYPE_SETTING, null, 'import', new pix_icon('i/import', '')); } + // Copy this course. + if ($adminoptions->copy) { + $url = new moodle_url('/backup/copy.php', array('id' => $course->id)); + $coursenode->add(get_string('copycourse'), $url, self::TYPE_SETTING, null, 'copy', new pix_icon('t/copy', '')); + } + // Reset this course if ($adminoptions->reset) { $url = new moodle_url('/course/reset.php', array('id'=>$course->id)); diff --git a/lib/templates/async_backup_progress.mustache b/lib/templates/async_backup_progress.mustache index ba96e5311ccfd..119141ca0bb2f 100644 --- a/lib/templates/async_backup_progress.mustache +++ b/lib/templates/async_backup_progress.mustache @@ -35,11 +35,22 @@ Example context (json): { "backupid": "f04abf8cba0319e486a3dfa7e9cb4476", + "restoreid": "f04abf8cba0319e486a3dfa7e9cb4477", + "operation": "backup", "width": "500" } }}
-
+
{{# str }} asyncprocesspending, backup {{/ str }}
diff --git a/lib/templates/async_copy_complete_cell.mustache b/lib/templates/async_copy_complete_cell.mustache new file mode 100644 index 0000000000000..4dba08aa5d11e --- /dev/null +++ b/lib/templates/async_copy_complete_cell.mustache @@ -0,0 +1,39 @@ +{{! + 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 . +}} +{{! + @template core/async_restore_progress_row. + + Moodle Asynchronous restore status table row template. + + The purpose of this template is to render status + table row updates during an asynchronous restore process. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * + + Example context (json): + {} +}} + + + diff --git a/lib/tests/accesslib_test.php b/lib/tests/accesslib_test.php index fcaf107df0aa8..dcf3646c10a1c 100644 --- a/lib/tests/accesslib_test.php +++ b/lib/tests/accesslib_test.php @@ -4309,6 +4309,37 @@ public function test_get_users_by_capability_locked() { $users = get_users_by_capability($contexts->cat1course1forum, $caput); $this->assertArrayHasKey($uut->id, $users); } + + /** + * Test require_all_capabilities. + */ + public function test_require_all_capabilities() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'), '*', MUST_EXIST); + $teacher = $this->getDataGenerator()->create_user(); + role_assign($teacherrole->id, $teacher->id, $coursecontext); + + // Note: Here are used default capabilities, the full test is in permission evaluation bellow, + // use two capabilities that teacher has and one does not, none of them should be allowed for not-logged-in user. + $this->assertTrue($DB->record_exists('capabilities', array('name' => 'moodle/backup:backupsection'))); + $this->assertTrue($DB->record_exists('capabilities', array('name' => 'moodle/backup:backupcourse'))); + + $sca = array('moodle/backup:backupsection', 'moodle/backup:backupcourse'); + + $this->setUser($teacher); + require_all_capabilities($sca, $coursecontext); + require_all_capabilities($sca, $coursecontext, $teacher); + + // Guest users should not have any of these perms. + $this->setUser(0); + $this->expectException(\required_capability_exception::class); + require_all_capabilities($sca, $coursecontext); + } } /**