Merge branch 'MDL-77254-master' of https://github.com/sarjona/moodle

This commit is contained in:
Paul Holden 2023-04-04 17:36:51 +01:00
commit 1f6722e696
No known key found for this signature in database
GPG Key ID: A81A96D6045F6164
17 changed files with 364 additions and 3 deletions

View File

@ -357,6 +357,12 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
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'),
@ -521,6 +527,12 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
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'),
@ -591,6 +603,9 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
$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)));

View File

@ -204,6 +204,11 @@ abstract class backup_activity_task extends backup_task {
// 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;
}

View File

@ -192,6 +192,12 @@ class backup_root_task extends backup_task {
$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')));

View File

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

View File

@ -2987,3 +2987,35 @@ class backup_contentbankcontent_structure_step extends backup_structure_step {
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;
}
}

View File

@ -200,6 +200,11 @@ abstract class restore_activity_task extends restore_task {
}
}
// 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;
}

View File

@ -316,6 +316,18 @@ class restore_root_task extends restore_task {
$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;

View File

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

View File

@ -4226,6 +4226,61 @@ class restore_contentbankcontent_structure_step extends restore_structure_step {
}
}
/**
* 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

View File

@ -18,6 +18,7 @@ namespace core_backup;
use backup;
use backup_controller;
use backup_setting;
use restore_controller;
use restore_dbops;
@ -463,9 +464,10 @@ class moodle2_test extends \advanced_testcase {
* @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).
@ -476,6 +478,9 @@ class moodle2_test extends \advanced_testcase {
$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();
@ -493,6 +498,13 @@ class moodle2_test extends \advanced_testcase {
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();
@ -1090,4 +1102,75 @@ class moodle2_test extends \advanced_testcase {
$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]));
}
}

View File

@ -567,6 +567,7 @@ abstract class backup_controller_dbops extends backup_dbops {
'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);
@ -616,6 +617,7 @@ abstract class backup_controller_dbops extends backup_dbops {
'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);

View File

@ -160,6 +160,7 @@ abstract class restore_controller_dbops extends restore_dbops {
'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);

View File

@ -0,0 +1,103 @@
@core @core_backup @core_h5p @mod_h5pactivity @_switch_iframe @javascript
Feature: Backup xAPI states
In order to save and restore xAPI states
As an admin
I need to create backups with xAPI states and restore them
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
And the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| packagefilepath | h5p/tests/fixtures/filltheblanks.h5p |
# Save state for the student user.
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia"
And I switch to the main frame
And I am on the "Course 1" course page
And I am on the "Awesome H5P package" "h5pactivity activity" page
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Narnia"
And I log out
Scenario: Content state is backup/restored when user data is included
# Backup and restore the course.
Given I log in as "admin"
And I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into a new course using this options:
| Settings | Include enrolled users | 1 |
| Schema | User data | 1 |
| Schema | Course name | Course 2 |
| Schema | Course short name | C2 |
# Login as student and confirm xAPI state has been restored.
When I am on the "Course 2" course page logged in as student1
And I click on "Awesome H5P package" "link" in the "region-main" "region"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Narnia"
Scenario: Content state is not restored when user data is not included in the backup
# Backup course without user data and then restore it.
When I log in as "admin"
And I backup "Course 1" course using this options:
| Initial | Include enrolled users | 0 |
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into a new course using this options:
| Schema | Course name | Course 2 |
| Schema | Course short name | C2 |
# Enrol student to the new course.
And the following "course enrolments" exist:
| user | course | role |
| student1 | C2 | student |
# Login as student and confirm xAPI state hasn't been restored.
And I am on the "Course 2" course page logged in as student1
And I click on "Awesome H5P package" "link" in the "region-main" "region"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia"
Scenario: Content state is not restored when user data is included in the backup but xAPI state is not restored
# Backup with user data and restore it without user data the course.
Given I log in as "admin"
And I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into a new course using this options:
| Settings | Include user's state in content such as H5P activities | 0 |
| Schema | Course name | Course 2 |
| Schema | Course short name | C2 |
# Login as student and confirm xAPI state hasn't been restored.
When I am on the "Course 2" course page logged in as student1
And I click on "Awesome H5P package" "link" in the "region-main" "region"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia"
Scenario: Content state is not restored when it is not included explicitly in the backup
# Backup course with user data but without xAPI state and then restore it.
When I log in as "admin"
And I backup "Course 1" course using this options:
| Initial | Include user's state in content such as H5P activities | 0 |
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into a new course using this options:
| Schema | Course name | Course 2 |
| Schema | Course short name | C2 |
And I should see "Awesome H5P package"
# Login as student and confirm xAPI state hasn't been restored.
And I am on the "Course 2" course page logged in as student1
And I click on "Awesome H5P package" "link" in the "region-main" "region"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia"

View File

@ -960,7 +960,9 @@ class framework implements H5PFrameworkInterface {
// Reset user data.
try {
$xapihandler = handler::create($file->get_component());
$xapihandler->reset_states($file->get_contextid());
// Reset only entries with 'state' as stateid (the ones restored shouldn't be restored, because the H5P
// content hasn't been created yet).
$xapihandler->reset_states($file->get_contextid(), null, 'state');
} catch (xapi_exception $exception) {
// This component doesn't support xAPI State, so no content needs to be reset.
return;

View File

@ -277,6 +277,30 @@ class player {
);
try {
$state = $xapihandler->load_state($state);
if (!$state) {
// Check if the state has been restored from a backup for the current user.
$state = new state(
item_agent::create_from_user($USER),
$xapiobject,
'restored',
null,
null
);
$state = $xapihandler->load_state($state);
if ($state && !is_null($state->get_state_data())) {
// A restored state has been found. It will be replaced with one with the proper stateid and statedata.
$xapihandler->delete_state($state);
$state = new state(
item_agent::create_from_user($USER),
$xapiobject,
'state',
$state->jsonSerialize(),
null
);
$xapihandler->save_state($state);
}
}
if (!$state) {
return $emptystatedata;
}

View File

@ -139,6 +139,7 @@ $string['configgeneralroleassignments'] = 'If enabled by default roles assignmen
$string['configgeneralpermissions'] = 'If enabled the role permissions will be imported. This may override existing permissions for enrolled users.';
$string['configgeneraluserscompletion'] = 'If enabled user completion information will be included in backups by default.';
$string['configgeneralusers'] = 'Sets the default for whether to include users in backups.';
$string['configgeneralxapistate'] = 'Sets the default for including the user\'s state in content such as H5P activities in a backup.';
$string['configlegacyfiles'] = 'Sets the default for including legacy course files in a backup. Legacy course files are from versions of Moodle prior to 2.0.';
$string['configloglifetime'] = 'This specifies the length of time you want to keep backup logs information. Logs that are older than this age are automatically deleted. It is recommended to keep this value small, because backup logged information can be huge.';
$string['configrestoreactivities'] = 'Sets the default for restoring activities.';
@ -157,6 +158,7 @@ $string['configrestoreroleassignments'] = 'If enabled by default roles assignmen
$string['configrestorepermissions'] = 'If enabled the role permissions will be restored. This may override existing permissions for enrolled users.';
$string['configrestoreuserscompletion'] = 'If enabled user completion information will be restored by default if it was included in the backup.';
$string['configrestoreusers'] = 'Sets the default for whether to restore users if they were included in the backup.';
$string['configrestorexapistate'] = 'Sets the default for restoring the user\'s state in content such as H5P activities.';
$string['confirmcancel'] = 'Cancel backup';
$string['confirmcancelrestore'] = 'Cancel restore';
$string['confirmcancelimport'] = 'Cancel import';
@ -244,6 +246,7 @@ $string['generalpermissions'] = 'Include permission overrides';
$string['generalsettings'] = 'General backup settings';
$string['generaluserscompletion'] = 'Include user completion information';
$string['generalusers'] = 'Include users';
$string['generalxapistate'] = 'Include user\'s state in content such as H5P activities';
$string['hidetypes'] = 'Hide type options';
$string['importgeneralsettings'] = 'General import defaults';
$string['importgeneralmaxresults'] = 'Maximum number of courses listed for import';
@ -372,6 +375,7 @@ $string['rootsettinggradehistories'] = 'Include grade history';
$string['rootsettinggroups'] = 'Include groups and groupings';
$string['rootsettingimscc1'] = 'Convert to IMS Common Cartridge 1.0';
$string['rootsettingimscc11'] = 'Convert to IMS Common Cartridge 1.1';
$string['rootsettingxapistate'] = 'Include user\'s state in content such as H5P activities';
$string['samesitenotification'] = 'This backup was created with only references to files, not the files themselves. Restoring will only work on this site.';
$string['sitecourseformatwarning'] = 'This is a site home backup. It can only be restored on the site home.';
$string['storagecourseonly'] = 'Course backup filearea';

View File

@ -72,7 +72,7 @@ class state implements JsonSerializable {
/**
* Return the data to serialize in case JSON state when needed.
*
* @return stdClass The state data structure
* @return stdClass The state data structure. If statedata is null, this method will return an empty class.
*/
public function jsonSerialize(): stdClass {
if ($this->statedata) {