Skip to content

Commit

Permalink
MDL-77254 backup: Backup/restore xAPI state
Browse files Browse the repository at this point in the history
  • Loading branch information
sarjona committed Apr 4, 2023
1 parent c862857 commit 1996a7c
Show file tree
Hide file tree
Showing 14 changed files with 336 additions and 1 deletion.
15 changes: 15 additions & 0 deletions admin/settings/courses.php
Expand Up @@ -357,6 +357,12 @@
new lang_string('configgeneralcontentbankcontent', 'backup'),
['value' => 1, 'locked' => 0])
);
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_xapistate',
new lang_string('generalxapistate', 'backup'),
new lang_string('configgeneralxapistate', 'backup'),
['value' => 1, 'locked' => 0])
);


$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_legacyfiles',
new lang_string('generallegacyfiles', 'backup'),
Expand Down Expand Up @@ -521,6 +527,12 @@
new lang_string('configgeneralcontentbankcontent', 'backup'),
1)
);
$temp->add(new admin_setting_configcheckbox(
'backup/backup_auto_xapistate',
new lang_string('generalxapistate', 'backup'),
new lang_string('configgeneralxapistate', 'backup'),
1)
);

$temp->add(new admin_setting_configcheckbox('backup/backup_auto_legacyfiles',
new lang_string('generallegacyfiles', 'backup'),
Expand Down Expand Up @@ -591,6 +603,9 @@
$temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_contentbankcontent',
new lang_string('generalcontentbankcontent', 'backup'),
new lang_string('configrestorecontentbankcontent', 'backup'), array('value' => 1, 'locked' => 0)));
$temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_xapistate',
new lang_string('generalxapistate', 'backup'),
new lang_string('configrestorexapistate', 'backup'), array('value' => 1, 'locked' => 0)));
$temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_legacyfiles',
new lang_string('generallegacyfiles', 'backup'),
new lang_string('configlegacyfiles', 'backup'), array('value' => 1, 'locked' => 0)));
Expand Down
5 changes: 5 additions & 0 deletions backup/moodle2/backup_activity_task.class.php
Expand Up @@ -204,6 +204,11 @@ public function build() {
// Migrate the already exported inforef entries to final ones
$this->add_step(new move_inforef_annotations_to_final('migrate_inforef'));

// Generate the xAPI state file (conditionally).
if ($this->get_setting_value('xapistate')) {
$this->add_step(new backup_xapistate_structure_step('activity_xapistate', 'xapistate.xml'));
}

// At the end, mark it as built
$this->built = true;
}
Expand Down
6 changes: 6 additions & 0 deletions backup/moodle2/backup_root_task.class.php
Expand Up @@ -192,6 +192,12 @@ protected function define_settings() {
$contentbank->set_ui(new backup_setting_ui_checkbox($contentbank, get_string('rootsettingcontentbankcontent', 'backup')));
$this->add_setting($contentbank);

// Define xAPI state inclusion setting.
$xapistate = new backup_xapistate_setting('xapistate', base_setting::IS_BOOLEAN, true);
$xapistate->set_ui(new backup_setting_ui_checkbox($xapistate, get_string('rootsettingxapistate', 'backup')));
$this->add_setting($xapistate);
$users->add_dependency($xapistate);

// Define legacy file inclusion setting.
$legacyfiles = new backup_generic_setting('legacyfiles', base_setting::IS_BOOLEAN, true);
$legacyfiles->set_ui(new backup_setting_ui_checkbox($legacyfiles, get_string('rootsettinglegacyfiles', 'backup')));
Expand Down
6 changes: 6 additions & 0 deletions backup/moodle2/backup_settingslib.php
Expand Up @@ -211,3 +211,9 @@ class backup_activity_userinfo_setting extends activity_backup_setting {}
*/
class backup_contentbankcontent_setting extends backup_generic_setting {
}

/**
* Root setting to control if backup will include xAPI state or not.
*/
class backup_xapistate_setting extends backup_generic_setting {
}
32 changes: 32 additions & 0 deletions backup/moodle2/backup_stepslib.php
Expand Up @@ -2987,3 +2987,35 @@ protected function define_structure() {
return $contents;
}
}

/**
* Structure step in charge of constructing the xapistate.xml file for all the xAPI states found in a given context.
*/
class backup_xapistate_structure_step extends backup_structure_step {

/**
* Define structure for content bank step
*/
protected function define_structure() {

// Define each element separated.
$states = new backup_nested_element('states');
$state = new backup_nested_element(
'state',
['id'],
['component', 'userid', 'itemid', 'stateid', 'statedata', 'registration', 'timecreated', 'timemodified']
);

// Build the tree.
$states->add_child($state);

// Define sources.
$state->set_source_table('xapi_states', ['itemid' => backup::VAR_CONTEXTID]);

// Define annotations.
$state->annotate_ids('user', 'userid');

// Return the root element (contents).
return $states;
}
}
5 changes: 5 additions & 0 deletions backup/moodle2/restore_activity_task.class.php
Expand Up @@ -200,6 +200,11 @@ public function build() {
}
}

// The xAPI state (conditionally).
if ($this->get_setting_value('xapistate')) {
$this->add_step(new restore_xapistate_structure_step('activity_xapistate', 'xapistate.xml'));
}

// At the end, mark it as built
$this->built = true;
}
Expand Down
12 changes: 12 additions & 0 deletions backup/moodle2/restore_root_task.class.php
Expand Up @@ -316,6 +316,18 @@ protected function define_settings() {
$contents->get_ui()->set_changeable($changeable);
$this->add_setting($contents);

// Define xAPI states.
$defaultvalue = false;
$changeable = false;
if (isset($rootsettings['xapistate']) && $rootsettings['xapistate']) { // Only enabled when available.
$defaultvalue = true;
$changeable = true;
}
$xapistate = new restore_xapistate_setting('xapistate', base_setting::IS_BOOLEAN, $defaultvalue);
$xapistate->set_ui(new backup_setting_ui_checkbox($xapistate, get_string('rootsettingxapistate', 'backup')));
$xapistate->get_ui()->set_changeable($changeable);
$this->add_setting($xapistate);

// Include legacy files.
$defaultvalue = true;
$changeable = true;
Expand Down
6 changes: 6 additions & 0 deletions backup/moodle2/restore_settingslib.php
Expand Up @@ -248,3 +248,9 @@ class restore_activity_userinfo_setting extends restore_activity_generic_setting
*/
class restore_contentbankcontent_setting extends restore_generic_setting {
}

/**
* Root setting to control if restore will create xAPI states or not.
*/
class restore_xapistate_setting extends restore_generic_setting {
}
55 changes: 55 additions & 0 deletions backup/moodle2/restore_stepslib.php
Expand Up @@ -4226,6 +4226,61 @@ protected function after_execute() {
}
}

/**
* This structure steps restores the xAPI states.
*/
class restore_xapistate_structure_step extends restore_structure_step {

/**
* Define structure for xAPI state step
*/
protected function define_structure() {
return [new restore_path_element('xapistate', '/states/state')];
}

/**
* Define data processed for xAPI state.
*
* @param array|stdClass $data
*/
public function process_xapistate($data) {
global $DB;

$data = (object)$data;
$oldid = $data->id;
$exists = false;

$params = [
'component' => $data->component,
'itemid' => $this->task->get_contextid(),
// Set stateid to 'restored', to let plugins identify the origin of this state is a backup.
'stateid' => 'restored',
'statedata' => $data->statedata,
'registration' => $data->registration,
'timecreated' => $data->timecreated,
'timemodified' => time(),
];

// Trying to map users. Users cannot always be mapped, for instance, when copying.
$params['userid'] = $this->get_mappingid('user', $data->userid);
if (!$params['userid']) {
// Leave the userid unchanged when we are restoring the same site.
if ($this->task->is_samesite()) {
$params['userid'] = $data->userid;
}
$filter = $params;
unset($filter['statedata']);
$exists = $DB->record_exists('xapi_states', $filter);
}

if (!$exists && $params['userid']) {
// Only insert the record if the user exists or can be mapped.
$newitemid = $DB->insert_record('xapi_states', $params);
$this->set_mapping('xapi_states', $oldid, $newitemid, true);
}
}
}

/**
* This structure steps restores one instance + positions of one block
* Note: Positions corresponding to one existing context are restored
Expand Down
85 changes: 84 additions & 1 deletion backup/moodle2/tests/moodle2_test.php
Expand Up @@ -18,6 +18,7 @@

use backup;
use backup_controller;
use backup_setting;
use restore_controller;
use restore_dbops;

Expand Down Expand Up @@ -463,9 +464,10 @@ public function test_restore_frontpage() {
* @param \stdClass $course Course object to backup
* @param int $newdate If non-zero, specifies custom date for new course
* @param callable|null $inbetween If specified, function that is called before restore
* @param bool $userdata Whether the backup/restory must be with user data or not.
* @return int ID of newly restored course
*/
protected function backup_and_restore($course, $newdate = 0, $inbetween = null) {
protected function backup_and_restore($course, $newdate = 0, $inbetween = null, bool $userdata = false) {
global $USER, $CFG;

// Turn off file logging, otherwise it can't delete the file (Windows).
Expand All @@ -476,6 +478,9 @@ protected function backup_and_restore($course, $newdate = 0, $inbetween = null)
$bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
$USER->id);
$bc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED);
$bc->get_plan()->get_setting('users')->set_value($userdata);

$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
Expand All @@ -493,6 +498,13 @@ protected function backup_and_restore($course, $newdate = 0, $inbetween = null)
if ($newdate) {
$rc->get_plan()->get_setting('course_startdate')->set_value($newdate);
}

$rc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED);
$rc->get_plan()->get_setting('users')->set_value($userdata);
if ($userdata) {
$rc->get_plan()->get_setting('xapistate')->set_value(true);
}

$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
Expand Down Expand Up @@ -1090,4 +1102,75 @@ public function test_contentbank_content_backup() {
$this->assertEquals(4, $DB->count_records('contentbank_content'));
$this->assertEquals(2, $DB->count_records('contentbank_content', ['contextid' => $newcontext->id]));
}

/**
* Test the xAPI state through a backup and restore.
*
* @covers \backup_xapistate_structure_step
* @covers \restore_xapistate_structure_step
*/
public function test_xapistate_backup() {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();

$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$this->setUser($user);

/** @var \mod_h5pactivity_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');

/** @var \core_h5p_generator $h5pgenerator */
$h5pgenerator = $this->getDataGenerator()->get_plugin_generator('core_h5p');

// Add an attempt to the H5P activity.
$attemptinfo = [
'userid' => $user->id,
'h5pactivityid' => $activity->id,
'attempt' => 1,
'interactiontype' => 'compound',
'rawscore' => 2,
'maxscore' => 2,
'duration' => 1,
'completion' => 1,
'success' => 0,
];
$generator->create_attempt($attemptinfo);

// Add also a xAPI state to the H5P activity.
$filerecord = [
'contextid' => \context_module::instance($activity->cmid)->id,
'component' => 'mod_h5pactivity',
'filearea' => 'package',
'itemid' => 0,
'filepath' => '/',
'filepath' => '/',
'filename' => 'dummy.h5p',
'addxapistate' => true,
];
$h5pgenerator->generate_h5p_data(false, $filerecord);

// Check the H5P activity exists and the attempt has been created.
$this->assertEquals(1, $DB->count_records('h5pactivity'));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));

// Do backup and restore.
$this->setAdminUser();
$newcourseid = $this->backup_and_restore($course, 0, null, true);

// Confirm that values were transferred correctly into H5P activity on new course.
$this->assertEquals(2, $DB->count_records('h5pactivity'));
$this->assertEquals(4, $DB->count_records('grade_items'));
$this->assertEquals(4, $DB->count_records('grade_grades'));
$this->assertEquals(2, $DB->count_records('xapi_states'));

$newactivity = $DB->get_record('h5pactivity', ['course' => $newcourseid]);
$cm = get_coursemodule_from_instance('h5pactivity', $newactivity->id);
$context = \context_module::instance($cm->id);
$this->assertEquals(1, $DB->count_records('xapi_states', ['itemid' => $context->id]));
}
}
2 changes: 2 additions & 0 deletions backup/util/dbops/backup_controller_dbops.class.php
Expand Up @@ -567,6 +567,7 @@ public static function apply_config_defaults(backup_controller $controller) {
'backup_general_groups' => 'groups',
'backup_general_competencies' => 'competencies',
'backup_general_contentbankcontent' => 'contentbankcontent',
'backup_general_xapistate' => 'xapistate',
'backup_general_legacyfiles' => 'legacyfiles'
);
self::apply_admin_config_defaults($controller, $settings, true);
Expand Down Expand Up @@ -616,6 +617,7 @@ public static function apply_config_defaults(backup_controller $controller) {
'backup_auto_groups' => 'groups',
'backup_auto_competencies' => 'competencies',
'backup_auto_contentbankcontent' => 'contentbankcontent',
'backup_auto_xapistate' => 'xapistate',
'backup_auto_legacyfiles' => 'legacyfiles'
);
self::apply_admin_config_defaults($controller, $settings, false);
Expand Down
1 change: 1 addition & 0 deletions backup/util/dbops/restore_controller_dbops.class.php
Expand Up @@ -160,6 +160,7 @@ public static function apply_config_defaults(restore_controller $controller) {
'restore_general_groups' => 'groups',
'restore_general_competencies' => 'competencies',
'restore_general_contentbankcontent' => 'contentbankcontent',
'restore_general_xapistate' => 'xapistate',
'restore_general_legacyfiles' => 'legacyfiles'
);
self::apply_admin_config_defaults($controller, $settings, true);
Expand Down

0 comments on commit 1996a7c

Please sign in to comment.