Skip to content

Commit

Permalink
MDL-64843 Backup: Course copy user interface
Browse files Browse the repository at this point in the history
This patch adds better core support for copying courses.
There is now a simplified and dedicated UI for copying
courses. This can be accessed from the course context
menu or course management screens.

All backups are done asynchronously and there can be multiple
copies of a course in flight at once.
  • Loading branch information
Matt Porritt committed May 15, 2020
1 parent 206e179 commit 01436f7
Show file tree
Hide file tree
Showing 40 changed files with 2,868 additions and 73 deletions.
9 changes: 9 additions & 0 deletions backup/backup.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
32 changes: 30 additions & 2 deletions backup/controller/backup_controller.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
}

Expand Down
33 changes: 33 additions & 0 deletions backup/controller/base_controller.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
}
26 changes: 25 additions & 1 deletion backup/controller/restore_controller.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
50 changes: 50 additions & 0 deletions backup/controller/tests/controller_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down
95 changes: 95 additions & 0 deletions backup/copy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* This script is used to configure and execute the course copy proccess.
*
* @package core_backup
* @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/>
* @author Matt Porritt <[email protected]>
* @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();
}
59 changes: 59 additions & 0 deletions backup/copyprogress.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* This script is used to configure and execute the course copy proccess.
*
* @package core_backup
* @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/>
* @author Matt Porritt <[email protected]>
* @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();
Loading

0 comments on commit 01436f7

Please sign in to comment.