MDL-67789 h5p: Save current state using xAPI State

Co-author: Andrew Lyons <andrew@moodle.com>
This commit is contained in:
Sara Arjona 2023-01-18 16:19:18 +01:00
parent 03a4abde0f
commit bd0a6e6dcc
25 changed files with 1292 additions and 182 deletions

10
h5p/amd/build/repository.min.js vendored Normal file
View File

@ -0,0 +1,10 @@
define("core_h5p/repository",["exports","core/ajax","core/config"],(function(_exports,_ajax,config){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.postStatement=_exports.postState=_exports.deleteState=void 0,config=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}
/**
* Module to handle AJAX interactions.
*
* @module core_h5p/repository
* @copyright 2023 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/(config);_exports.postStatement=(component,statements)=>(0,_ajax.call)([{methodname:"core_xapi_statement_post",args:{component:component,requestjson:JSON.stringify(statements)}}])[0];_exports.postState=(component,activityId,agent,stateId,stateData)=>{const requestUrl=new URL("".concat(config.wwwroot,"/lib/ajax/service.php"));requestUrl.searchParams.set("sesskey",config.sesskey),navigator.sendBeacon(requestUrl,JSON.stringify([{index:0,methodname:"core_xapi_post_state",args:{component:component,activityId:activityId,agent:JSON.stringify(agent),stateId:stateId,stateData:stateData}}]))};_exports.deleteState=(component,activityId,agent,stateId)=>(0,_ajax.call)([{methodname:"core_xapi_delete_state",args:{component:component,activityId:activityId,agent:JSON.stringify(agent),stateId:stateId}}])[0]}));
//# sourceMappingURL=repository.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"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 <http://www.gnu.org/licenses/>.\n\n/**\n * Module to handle AJAX interactions.\n *\n * @module core_h5p/repository\n * @copyright 2023 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {call as fetchMany} from 'core/ajax';\nimport * as config from 'core/config';\n\n/**\n * Send a xAPI statement to LMS.\n *\n * @param {string} component\n * @param {Object} statements\n * @returns {Promise}\n */\nexport const postStatement = (component, statements) => fetchMany([{\n methodname: 'core_xapi_statement_post',\n args: {\n component,\n requestjson: JSON.stringify(statements),\n }\n}])[0];\n\n/**\n * Send a xAPI state to LMS.\n *\n * @param {string} component\n * @param {string} activityId\n * @param {Object} agent\n * @param {string} stateId\n * @param {string} stateData\n */\nexport const postState = (\n component,\n activityId,\n agent,\n stateId,\n stateData,\n) => {\n // Please note that we must use a Beacon send here.\n // The XHR is not guaranteed because it will be aborted on page transition.\n // https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API\n // Note: Moodle does not currently have a sendBeacon API endpoint.\n const requestUrl = new URL(`${config.wwwroot}/lib/ajax/service.php`);\n requestUrl.searchParams.set('sesskey', config.sesskey);\n\n navigator.sendBeacon(requestUrl, JSON.stringify([{\n index: 0,\n methodname: 'core_xapi_post_state',\n args: {\n component,\n activityId,\n agent: JSON.stringify(agent),\n stateId,\n stateData,\n }\n }]));\n};\n\n/**\n * Delete a xAPI state from LMS.\n *\n * @param {string} component\n * @param {string} activityId\n * @param {Object} agent\n * @param {string} stateId\n * @returns {Promise}\n */\nexport const deleteState = (\n component,\n activityId,\n agent,\n stateId,\n) => fetchMany([{\n methodname: 'core_xapi_delete_state',\n args: {\n component,\n activityId,\n agent: JSON.stringify(agent),\n stateId,\n },\n}])[0];\n"],"names":["component","statements","methodname","args","requestjson","JSON","stringify","activityId","agent","stateId","stateData","requestUrl","URL","config","wwwroot","searchParams","set","sesskey","navigator","sendBeacon","index"],"mappings":";;;;;;;qCAgC6B,CAACA,UAAWC,cAAe,cAAU,CAAC,CAC/DC,WAAY,2BACZC,KAAM,CACFH,UAAAA,UACAI,YAAaC,KAAKC,UAAUL,gBAEhC,sBAWqB,CACrBD,UACAO,WACAC,MACAC,QACAC,mBAMMC,WAAa,IAAIC,cAAOC,OAAOC,kCACrCH,WAAWI,aAAaC,IAAI,UAAWH,OAAOI,SAE9CC,UAAUC,WAAWR,WAAYN,KAAKC,UAAU,CAAC,CAC7Cc,MAAO,EACPlB,WAAY,uBACZC,KAAM,CACFH,UAAAA,UACAO,WAAAA,WACAC,MAAOH,KAAKC,UAAUE,OACtBC,QAAAA,QACAC,UAAAA,qCAce,CACvBV,UACAO,WACAC,MACAC,WACC,cAAU,CAAC,CACZP,WAAY,yBACZC,KAAM,CACFH,UAAAA,UACAO,WAAAA,WACAC,MAAOH,KAAKC,UAAUE,OACtBC,QAAAA,YAEJ"}

99
h5p/amd/src/repository.js Normal file
View File

@ -0,0 +1,99 @@
// 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/>.
/**
* Module to handle AJAX interactions.
*
* @module core_h5p/repository
* @copyright 2023 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {call as fetchMany} from 'core/ajax';
import * as config from 'core/config';
/**
* Send a xAPI statement to LMS.
*
* @param {string} component
* @param {Object} statements
* @returns {Promise}
*/
export const postStatement = (component, statements) => fetchMany([{
methodname: 'core_xapi_statement_post',
args: {
component,
requestjson: JSON.stringify(statements),
}
}])[0];
/**
* Send a xAPI state to LMS.
*
* @param {string} component
* @param {string} activityId
* @param {Object} agent
* @param {string} stateId
* @param {string} stateData
*/
export const postState = (
component,
activityId,
agent,
stateId,
stateData,
) => {
// Please note that we must use a Beacon send here.
// The XHR is not guaranteed because it will be aborted on page transition.
// https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API
// Note: Moodle does not currently have a sendBeacon API endpoint.
const requestUrl = new URL(`${config.wwwroot}/lib/ajax/service.php`);
requestUrl.searchParams.set('sesskey', config.sesskey);
navigator.sendBeacon(requestUrl, JSON.stringify([{
index: 0,
methodname: 'core_xapi_post_state',
args: {
component,
activityId,
agent: JSON.stringify(agent),
stateId,
stateData,
}
}]));
};
/**
* Delete a xAPI state from LMS.
*
* @param {string} component
* @param {string} activityId
* @param {Object} agent
* @param {string} stateId
* @returns {Promise}
*/
export const deleteState = (
component,
activityId,
agent,
stateId,
) => fetchMany([{
methodname: 'core_xapi_delete_state',
args: {
component,
activityId,
agent: JSON.stringify(agent),
stateId,
},
}])[0];

View File

@ -16,6 +16,8 @@
namespace core_h5p;
use core_xapi\handler;
use core_xapi\xapi_exception;
use Moodle\H5PFrameworkInterface;
use Moodle\H5PCore;
@ -886,14 +888,6 @@ class framework implements H5PFrameworkInterface {
public function updateContent($content, $contentmainid = null) {
global $DB;
if (!isset($content['pathnamehash'])) {
$content['pathnamehash'] = '';
}
if (!isset($content['contenthash'])) {
$content['contenthash'] = '';
}
// If the libraryid declared in the package is empty, get the latest version.
if (empty($content['library']['libraryId'])) {
$mainlibrary = $this->get_latest_library_version($content['library']['machineName']);
@ -919,11 +913,19 @@ class framework implements H5PFrameworkInterface {
'mainlibraryid' => $content['library']['libraryId'],
'timemodified' => time(),
'filtered' => null,
'pathnamehash' => $content['pathnamehash'],
'contenthash' => $content['contenthash']
];
if (isset($content['pathnamehash'])) {
$data['pathnamehash'] = $content['pathnamehash'];
}
if (isset($content['contenthash'])) {
$data['contenthash'] = $content['contenthash'];
}
if (!isset($content['id'])) {
$data['pathnamehash'] = $data['pathnamehash'] ?? '';
$data['contenthash'] = $data['contenthash'] ?? '';
$data['timecreated'] = $data['timemodified'];
$id = $DB->insert_record('h5p', $data);
} else {
@ -941,7 +943,28 @@ class framework implements H5PFrameworkInterface {
* @param int $contentid The h5p content id
*/
public function resetContentUserData($contentid) {
// Currently, we do not store user data for a content.
global $DB;
// Get the component associated to the H5P content to reset.
$h5p = $DB->get_record('h5p', ['id' => $contentid]);
if (!$h5p) {
return;
}
$fs = get_file_storage();
$file = $fs->get_file_by_hash($h5p->pathnamehash);
if (!$file) {
return;
}
// Reset user data.
try {
$xapihandler = handler::create($file->get_component());
$xapihandler->reset_states($file->get_contextid());
} catch (xapi_exception $exception) {
// This component doesn't support xAPI State, so no content needs to be reset.
return;
}
}
/**
@ -998,8 +1021,12 @@ class framework implements H5PFrameworkInterface {
public function deleteContentData($contentid) {
global $DB;
// The user content should be reset (instead of removed), because this method is called when H5P content needs
// to be updated too (and the previous states must be kept, but reset).
$this->resetContentUserData($contentid);
// Remove content.
$DB->delete_records('h5p', array('id' => $contentid));
$DB->delete_records('h5p', ['id' => $contentid]);
// Remove content library dependencies.
$this->deleteLibraryUsage($contentid);

View File

@ -314,18 +314,20 @@ class helper {
/**
* Get the settings needed by the H5P library.
*
* @param string|null $component
* @return array The settings.
*/
public static function get_core_settings(): array {
public static function get_core_settings(?string $component = null): array {
global $CFG, $USER;
$basepath = $CFG->wwwroot . '/';
$systemcontext = context_system::instance();
// Generate AJAX paths.
$ajaxpaths = [];
$ajaxpaths['xAPIResult'] = '';
$ajaxpaths['contentUserData'] = '';
// H5P doesn't currently support xAPI State. It implements a mechanism in contentUserDataAjax() in h5p.js to update user
// data. However, in our case, we're overriding this method to call the xAPI State web services.
$ajaxpaths = [
'contentUserData' => '',
];
$factory = new factory();
$core = $factory->get_core();
@ -336,13 +338,17 @@ class helper {
$usersettings['name'] = $USER->username;
$usersettings['id'] = $USER->id;
}
$savefreq = false;
if ($component !== null && get_config($component, 'enablesavestate')) {
$savefreq = get_config($component, 'savestatefreq');
}
$settings = array(
'baseUrl' => $basepath,
'url' => "{$basepath}pluginfile.php/{$systemcontext->instanceid}/core_h5p",
'urlLibraries' => "{$basepath}pluginfile.php/{$systemcontext->id}/core_h5p/libraries",
'postUserStatistics' => false,
'ajax' => $ajaxpaths,
'saveFreq' => false,
'saveFreq' => $savefreq,
'siteUrl' => $CFG->wwwroot,
'l10n' => array('H5P' => $core->getLocalization()),
'user' => $usersettings,
@ -360,13 +366,14 @@ class helper {
/**
* Get the core H5P assets, including all core H5P JavaScript and CSS.
*
* @param string|null $component
* @return Array core H5P assets.
*/
public static function get_core_assets(): array {
global $CFG, $PAGE;
public static function get_core_assets(?string $component = null): array {
global $PAGE;
// Get core settings.
$settings = self::get_core_settings();
$settings = self::get_core_settings($component);
$settings['core'] = [
'styles' => [],
'scripts' => []

View File

@ -27,7 +27,11 @@ namespace core_h5p;
defined('MOODLE_INTERNAL') || die();
use core_h5p\local\library\autoloader;
use core_xapi\handler;
use core_xapi\local\state;
use core_xapi\local\statement\item_activity;
use core_xapi\local\statement\item_agent;
use core_xapi\xapi_exception;
/**
* H5P player class, for displaying any local H5P content.
@ -102,7 +106,7 @@ class player {
* Inits the H5P player for rendering the content.
*
* @param string $url Local URL of the H5P file to display.
* @param stdClass $config Configuration for H5P buttons.
* @param \stdClass $config Configuration for H5P buttons.
* @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
* @param string $component optional moodle component to sent xAPI tracking
* @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they
@ -207,7 +211,7 @@ class player {
* main H5P config variable.
*/
public function add_assets_to_page() {
global $PAGE;
global $PAGE, $USER;
$cid = $this->get_cid();
$systemcontext = \context_system::instance();
@ -219,6 +223,7 @@ class player {
\core_h5p\file_storage::CONTENT_FILEAREA, $this->h5pid, null, null);
$exporturl = $this->get_export_settings($displayoptions[ core::DISPLAY_OPTION_DOWNLOAD ]);
$xapiobject = item_activity::create_from_id($this->context->id);
$contentsettings = [
'library' => core::libraryToString($this->content['library']),
'fullScreen' => $this->content['library']['fullscreen'],
@ -231,7 +236,7 @@ class player {
'url' => $xapiobject->get_data()->id,
'contentUrl' => $contenturl->out(),
'metadata' => $this->content['metadata'],
'contentUserData' => [0 => ['state' => '{}']]
'contentUserData' => [0 => ['state' => $this->get_state_data($xapiobject)]],
];
// Get the core H5P assets, needed by the H5P classes to render the H5P content.
$settings = $this->get_assets();
@ -241,6 +246,62 @@ class player {
$PAGE->requires->data_for_js('H5PIntegration', $settings, true);
}
/**
* Get the stored xAPI state to use as user data.
*
* @param item_activity $xapiobject
* @return string The state data to pass to the player frontend
*/
private function get_state_data(item_activity $xapiobject): string {
global $USER;
// Initialize the H5P content with the saved state (if it's enabled and the user has some stored state).
$emptystatedata = '{}';
$savestate = (bool) get_config($this->component, 'enablesavestate');
if (!$savestate) {
return $emptystatedata;
}
$xapihandler = handler::create($this->component);
if (!$xapihandler) {
return $emptystatedata;
}
// The component implements the xAPI handler, so the state can be loaded.
$state = new state(
item_agent::create_from_user($USER),
$xapiobject,
'state',
null,
null
);
try {
$state = $xapihandler->load_state($state);
if (!$state) {
return $emptystatedata;
}
if (is_null($state->get_state_data())) {
// The state content should be reset because, for instance, the content has changed.
return 'RESET';
}
$statedata = $state->jsonSerialize();
if (is_null($statedata)) {
return $emptystatedata;
}
if (property_exists($statedata, 'h5p')) {
// As the H5P state doesn't always use JSON, we have added this h5p object to jsonize it.
return $statedata->h5p;
}
} catch (xapi_exception $exception) {
return $emptystatedata;
}
return $emptystatedata;
}
/**
* Outputs H5P wrapper HTML.
*
@ -371,7 +432,7 @@ class player {
*/
private function get_assets(): array {
// Get core assets.
$settings = helper::get_core_assets();
$settings = helper::get_core_assets($this->component);
// Added here because in the helper we don't have the h5p content id.
$settings['moodleLibraryPaths'] = $this->core->get_dependency_roots($this->h5pid);
// Add also the Moodle component where the results will be tracked.

View File

@ -2344,6 +2344,11 @@ H5P.createTitle = function (rawTitle, maxLength) {
done('Not signed in.');
return;
}
// Moodle patch to let override this method.
if (H5P.contentUserDataAjax !== undefined) {
return H5P.contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async);
}
// End of Moodle patch.
var options = {
url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0),

View File

@ -35,3 +35,18 @@ The library needs to be saved in the database first before creating the files, b
5. Check if new methods have been added to any of the interfaces. If that's the case, implement them in the proper class. For
instance, if a new method is added to h5p-file-storage.interface.php, it should be implemented in h5p/classes/file_storage.php.
6. Open js/h5p.js and in function contentUserDataAjax() add the following patch:
function contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async) {
if (H5PIntegration.user === undefined) {
// Not logged in, no use in saving.
done('Not signed in.');
return;
}
// Moodle patch to let override this method.
if (H5P.contentUserDataAjax !== undefined) {
return H5P.contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async);
}
// End of Moodle patch.
var options = {

View File

@ -72,26 +72,71 @@ H5PEmbedCommunicator = (function() {
window.parent.postMessage(data, '*');
};
/* eslint-disable promise/avoid-new */
const repositoryPromise = new Promise((resolve) => {
require(['core_h5p/repository'], (Repository) => {
// Replace the default versions.
self.post = Repository.postStatement;
self.postState = Repository.postState;
self.deleteState = Repository.deleteState;
// Resolve the Promise with Repository to allow any queued calls to be executed.
resolve(Repository);
});
});
/**
* Send a xAPI statement to LMS.
*
* @param {string} component
* @param {Object} statements
* @returns {Promise}
*/
self.post = function(component, statements) {
require(['core/ajax'], function(ajax) {
var data = {
component: component,
requestjson: JSON.stringify(statements)
};
ajax.call([
{
methodname: 'core_xapi_statement_post',
args: data
}
]);
});
};
self.post = (component, statements) => repositoryPromise.then((Repository) => Repository.postStatement(
component,
statements,
));
/**
* Send a xAPI state to LMS.
*
* @param {string} component
* @param {string} activityId
* @param {Object} agent
* @param {string} stateId
* @param {string} stateData
* @returns {void}
*/
self.postState = (
component,
activityId,
agent,
stateId,
stateData,
) => repositoryPromise.then((Repository) => Repository.postState(
component,
activityId,
agent,
stateId,
stateData,
));
/**
* Delete a xAPI state from LMS.
*
* @param {string} component
* @param {string} activityId
* @param {Object} agent
* @param {string} stateId
* @returns {Promise}
*/
self.deleteState = (component, activityId, agent, stateId) => repositoryPromise.then((Repository) => Repository.deleteState(
component,
activityId,
agent,
stateId,
));
}
return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
@ -120,6 +165,9 @@ document.onreadystatechange = async() => {
return;
}
/** @var {boolean} statementPosted Whether the statement has been sent or not, to avoid sending xAPI State after it. */
var statementPosted = false;
// Check for H5P iFrame.
var iFrame = document.querySelector('.h5p-iframe');
if (!iFrame || !iFrame.contentWindow) {
@ -188,6 +236,7 @@ document.onreadystatechange = async() => {
// Get emitted xAPI data.
H5P.externalDispatcher.on('xAPI', function(event) {
statementPosted = false;
var moodlecomponent = H5P.getMoodleComponent();
if (moodlecomponent == undefined) {
return;
@ -215,6 +264,33 @@ document.onreadystatechange = async() => {
if (isCompleted && !isChild) {
var statements = H5P.getXAPIStatements(this.contentId, statement);
H5PEmbedCommunicator.post(moodlecomponent, statements);
// Mark the statement has been sent, to avoid sending xAPI State after it.
statementPosted = true;
}
});
H5P.externalDispatcher.on('xAPIState', function(event) {
var moodlecomponent = H5P.getMoodleComponent();
var contentId = event.data.activityId;
var stateId = event.data.stateId;
var state = event.data.state;
if (state === undefined) {
// When state is undefined, a call to the WS for getting the state could be done. However, for now, this is not
// required because the content state is initialised with PHP.
return;
}
if (state === null) {
// When this method is called from the H5P API with null state, the state must be deleted using the rest of attributes.
H5PEmbedCommunicator.deleteState(moodlecomponent, contentId, H5P.getxAPIActor(), stateId);
} else if (!statementPosted) {
// Only update the state if a statement hasn't been posted recently.
// When state is defined, it needs to be updated. As not all the H5P content types are returning a JSON, we need
// to simulate it because xAPI State defines statedata as a JSON.
var statedata = {
h5p: state
};
H5PEmbedCommunicator.postState(moodlecomponent, contentId, H5P.getxAPIActor(), stateId, JSON.stringify(statedata));
}
});

View File

@ -1,3 +1,18 @@
// 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/>.
H5P._getLibraryPath = H5P.getLibraryPath;
H5P.getLibraryPath = function (library) {
if (H5PIntegration.moodleLibraryPaths) {
@ -88,3 +103,72 @@ H5P.XAPIEvent.prototype.setActor = function () {
};
}
};
/**
* Get the actor.
*
* @returns {Object} The Actor object.
*/
H5P.getxAPIActor = function() {
var actor = null;
if (H5PIntegration.user !== undefined) {
actor = {
'name': H5PIntegration.user.name,
'objectType': 'Agent'
};
if (H5PIntegration.user.id !== undefined) {
actor.account = {
'name': H5PIntegration.user.id,
'homePage': H5PIntegration.siteUrl
};
} else if (H5PIntegration.user.mail !== undefined) {
actor.mbox = 'mailto:' + H5PIntegration.user.mail;
}
} else {
var uuid;
try {
if (localStorage.H5PUserUUID) {
uuid = localStorage.H5PUserUUID;
} else {
uuid = H5P.createUUID();
localStorage.H5PUserUUID = uuid;
}
} catch (err) {
// LocalStorage and Cookies are probably disabled. Do not track the user.
uuid = 'not-trackable-' + H5P.createUUID();
}
actor = {
'account': {
'name': uuid,
'homePage': H5PIntegration.siteUrl
},
'objectType': 'Agent'
};
}
return actor;
};
/**
* Creates requests for inserting, updating and deleting content user data.
* It overrides the contentUserDataAjax private method in h5p.js.
*
* @param {number} contentId What content to store the data for.
* @param {string} dataType Identifies the set of data for this content.
* @param {string} subContentId Identifies sub content
* @param {function} [done] Callback when ajax is done.
* @param {object} [data] To be stored for future use.
* @param {boolean} [preload=false] Data is loaded when content is loaded.
* @param {boolean} [invalidate=false] Data is invalidated when content changes.
* @param {boolean} [async=true]
*/
H5P.contentUserDataAjax = function(contentId, dataType, subContentId, done, data, preload, invalidate, async) {
var instance = H5P.findInstanceFromId(contentId);
if (instance !== undefined) {
var xAPIState = {
activityId: H5P.XAPIEvent.prototype.getContentXAPIId(instance),
stateId: dataType,
state: data
};
H5P.externalDispatcher.trigger('xAPIState', xAPIState);
}
};

View File

@ -20,6 +20,8 @@ use core_collator;
use Moodle\H5PCore;
use Moodle\H5PDisplayOptionBehaviour;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
*
* Test class covering the H5PFrameworkInterface interface implementation.
@ -28,6 +30,7 @@ use Moodle\H5PDisplayOptionBehaviour;
* @category test
* @copyright 2019 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_h5p\framework
* @runTestsInSeparateProcesses
*/
class framework_test extends \advanced_testcase {
@ -1061,6 +1064,7 @@ class framework_test extends \advanced_testcase {
$this->resetAfterTest();
/** @var \core_h5p_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
// Create a library record.
@ -1085,6 +1089,8 @@ class framework_test extends \advanced_testcase {
// Make sure the h5p content was properly updated.
$this->assertNotEmpty($h5pcontent);
$this->assertNotEmpty($h5pcontent->pathnamehash);
$this->assertNotEmpty($h5pcontent->contenthash);
$this->assertEquals($content['params'], $h5pcontent->jsoncontent);
$this->assertEquals($content['library']['libraryId'], $h5pcontent->mainlibraryid);
$this->assertEquals($content['disable'], $h5pcontent->displayoptions);
@ -1139,33 +1145,102 @@ class framework_test extends \advanced_testcase {
$this->resetAfterTest();
/** @var \core_h5p_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
// For the mod_h5pactivity component, the activity needs to be created too.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$this->setUser($user);
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$activitycontext = \context_module::instance($activity->cmid);
$filerecord = [
'contextid' => $activitycontext->id,
'component' => 'mod_h5pactivity',
'filearea' => 'package',
'itemid' => 0,
'filepath' => '/',
'filename' => 'dummy.h5p',
'addxapistate' => true,
];
// Generate some h5p related data.
$data = $generator->generate_h5p_data();
$data = $generator->generate_h5p_data(false, $filerecord);
$h5pid = $data->h5pcontent->h5pid;
$h5pcontent = $DB->get_record('h5p', ['id' => $h5pid]);
// Make sure the particular h5p content exists in the DB.
$this->assertNotEmpty($h5pcontent);
// Get the h5p content libraries from the DB.
$h5pcontentlibraries = $DB->get_records('h5p_contents_libraries', ['h5pid' => $h5pid]);
$this->assertNotEmpty($DB->get_record('h5p', ['id' => $h5pid]));
// Make sure the content libraries exists in the DB.
$this->assertNotEmpty($h5pcontentlibraries);
$this->assertCount(5, $h5pcontentlibraries);
$this->assertCount(5, $DB->get_records('h5p_contents_libraries', ['h5pid' => $h5pid]));
// Make sure the particular xAPI state exists in the DB.
$records = $DB->get_records('xapi_states');
$record = reset($records);
$this->assertCount(1, $records);
$this->assertNotNull($record->statedata);
// Delete the h5p content and it's related data.
$this->framework->deleteContentData($h5pid);
$h5pcontent = $DB->get_record('h5p', ['id' => $h5pid]);
$h5pcontentlibraries = $DB->get_record('h5p_contents_libraries', ['h5pid' => $h5pid]);
// The particular h5p content should no longer exist in the db.
$this->assertEmpty($h5pcontent);
$this->assertEmpty($DB->get_record('h5p', ['id' => $h5pid]));
// The particular content libraries should no longer exist in the db.
$this->assertEmpty($h5pcontentlibraries);
$this->assertEmpty($DB->get_record('h5p_contents_libraries', ['h5pid' => $h5pid]));
// The xAPI state should be reseted.
$records = $DB->get_records('xapi_states');
$record = reset($records);
$this->assertCount(1, $records);
$this->assertNull($record->statedata);
}
/**
* Test the behaviour of resetContentUserData().
*/
public function test_resetContentUserData() {
global $DB;
$this->resetAfterTest();
/** @var \core_h5p_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
// For the mod_h5pactivity component, the activity needs to be created too.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$this->setUser($user);
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$activitycontext = \context_module::instance($activity->cmid);
$filerecord = [
'contextid' => $activitycontext->id,
'component' => 'mod_h5pactivity',
'filearea' => 'package',
'itemid' => 0,
'filepath' => '/',
'filename' => 'dummy.h5p',
'addxapistate' => true,
];
// Generate some h5p related data.
$data = $generator->generate_h5p_data(false, $filerecord);
$h5pid = $data->h5pcontent->h5pid;
// Make sure the H5P content, libraries and xAPI state exist in the DB.
$this->assertNotEmpty($DB->get_record('h5p', ['id' => $h5pid]));
$this->assertCount(5, $DB->get_records('h5p_contents_libraries', ['h5pid' => $h5pid]));
$records = $DB->get_records('xapi_states');
$record = reset($records);
$this->assertCount(1, $records);
$this->assertNotNull($record->statedata);
// Reset the user data associated to this H5P content.
$this->framework->resetContentUserData($h5pid);
// The H5P content should still exist in the db.
$this->assertNotEmpty($DB->get_record('h5p', ['id' => $h5pid]));
// The particular content libraries should still exist in the db.
$this->assertCount(5, $DB->get_records('h5p_contents_libraries', ['h5pid' => $h5pid]));
// The xAPI state should still exist in the db, but should be reset.
$records = $DB->get_records('xapi_states');
$record = reset($records);
$this->assertCount(1, $records);
$this->assertNull($record->statedata);
}
/**

View File

@ -14,21 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Generator for the core_h5p subsystem.
*
* @package core_h5p
* @category test
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_h5p\local\library\autoloader;
use core_h5p\core;
use core_h5p\player;
use core_h5p\factory;
defined('MOODLE_INTERNAL') || die();
use core_xapi\local\statement\item_activity;
/**
* Generator for the core_h5p subsystem.
@ -169,9 +159,10 @@ class core_h5p_generator extends \component_generator_base {
* Populate H5P database tables with relevant data to simulate the process of adding H5P content.
*
* @param bool $createlibraryfiles Whether to create and store library files on the filesystem
* @param array|null $filerecord The file associated to the H5P entry.
* @return stdClass An object representing the added H5P records
*/
public function generate_h5p_data(bool $createlibraryfiles = false): stdClass {
public function generate_h5p_data(bool $createlibraryfiles = false, ?array $filerecord = null): stdClass {
// Create libraries.
$mainlib = $libraries[] = $this->create_library_record('MainLibrary', 'Main Lib', 1, 0, 1, '', null,
'http://tutorial.org', 'http://example.org');
@ -189,7 +180,7 @@ class core_h5p_generator extends \component_generator_base {
}
// Create h5p content.
$h5p = $this->create_h5p_record($mainlib->id);
$h5p = $this->create_h5p_record($mainlib->id, null, null, $filerecord);
// Create h5p content library dependencies.
$this->create_contents_libraries_record($h5p, $mainlib->id);
$this->create_contents_libraries_record($h5p, $lib1->id);
@ -289,9 +280,11 @@ class core_h5p_generator extends \component_generator_base {
* @param int $mainlibid The ID of the content's main library
* @param string $jsoncontent The content in json format
* @param string $filtered The filtered content parameters
* @param array|null $filerecord The file associated to the H5P entry.
* @return int The ID of the added record
*/
public function create_h5p_record(int $mainlibid, string $jsoncontent = null, string $filtered = null): int {
public function create_h5p_record(int $mainlibid, string $jsoncontent = null, string $filtered = null,
?array $filerecord = null): int {
global $DB;
if (!$jsoncontent) {
@ -312,18 +305,46 @@ class core_h5p_generator extends \component_generator_base {
);
}
// Load the H5P file into DB.
$pathnamehash = sha1('pathname');
$contenthash = sha1('content');
if ($filerecord) {
$fs = get_file_storage();
if (!$fs->get_file(
$filerecord['contextid'],
$filerecord['component'],
$filerecord['filearea'],
$filerecord['itemid'],
$filerecord['filepath'],
$filerecord['filename'])) {
$file = $fs->create_file_from_string($filerecord, $jsoncontent);
$pathnamehash = $file->get_pathnamehash();
$contenthash = $file->get_contenthash();
if (array_key_exists('addxapistate', $filerecord) && $filerecord['addxapistate']) {
// Save some xAPI state associated to this H5P content.
$params = [
'component' => $filerecord['component'],
'activity' => item_activity::create_from_id($filerecord['contextid']),
];
global $CFG;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
\core_xapi\test_helper::create_state($params, true);
}
}
}
return $DB->insert_record(
'h5p',
array(
[
'jsoncontent' => $jsoncontent,
'displayoptions' => 8,
'mainlibraryid' => $mainlibid,
'timecreated' => time(),
'timemodified' => time(),
'filtered' => $filtered,
'pathnamehash' => sha1('pathname'),
'contenthash' => sha1('content')
)
'pathnamehash' => $pathnamehash,
'contenthash' => $contenthash,
]
);
}

View File

@ -19,13 +19,14 @@ namespace core_h5p;
use core_h5p\local\library\autoloader;
/**
* Test class covering the h5p data generator class.
*
* @package core_h5p
* @category test
* @copyright 2019 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @runTestsInSeparateProcesses
* Test class covering the h5p data generator class.
*
* @package core_h5p
* @category test
* @copyright 2019 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @runTestsInSeparateProcesses
* @covers \core_h5p_generator
*/
class generator_test extends \advanced_testcase {
@ -207,6 +208,120 @@ class generator_test extends \advanced_testcase {
];
}
/**
* Test the returned data of generate_h5p_data() when the method requests
* creation of H5P file and xAPI states.
*
* @dataProvider generate_h5p_data_xapistates_provider
* @param array|null $filerecord
*/
public function test_generate_h5p_data_xapistates(?array $filerecord) {
global $DB;
$this->resetAfterTest();
/** @var \core_h5p_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$this->setUser($user);
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$activitycontext = \context_module::instance($activity->cmid);
if ($filerecord) {
$filerecord['contextid'] = $activitycontext->id;
$filerecord['component'] = 'mod_h5pactivity';
$filerecord['filearea'] = 'package';
$filerecord['itemid'] = 0;
$filerecord['filepath'] = '/';
$filerecord['filepath'] = '/';
$filerecord['filename'] = 'dummy.h5p';
}
$data = $generator->generate_h5p_data(false, $filerecord);
$mainlib = $DB->get_record('h5p_libraries', ['machinename' => 'MainLibrary']);
$lib1 = $DB->get_record('h5p_libraries', ['machinename' => 'Library1']);
$lib2 = $DB->get_record('h5p_libraries', ['machinename' => 'Library2']);
$lib3 = $DB->get_record('h5p_libraries', ['machinename' => 'Library3']);
$lib4 = $DB->get_record('h5p_libraries', ['machinename' => 'Library4']);
$lib5 = $DB->get_record('h5p_libraries', ['machinename' => 'Library5']);
$h5p = $DB->get_record('h5p', ['mainlibraryid' => $mainlib->id]);
$expected = (object) [
'h5pcontent' => (object) [
'h5pid' => $h5p->id,
'contentdependencies' => [$mainlib, $lib1, $lib2, $lib3, $lib4],
],
'mainlib' => (object) [
'data' => $mainlib,
'dependencies' => [$lib1, $lib2, $lib3],
],
'lib1' => (object) [
'data' => $lib1,
'dependencies' => [$lib2, $lib3, $lib4],
],
'lib2' => (object) [
'data' => $lib2,
'dependencies' => [],
],
'lib3' => (object) [
'data' => $lib3,
'dependencies' => [$lib5],
],
'lib4' => (object) [
'data' => $lib4,
'dependencies' => [],
],
'lib5' => (object) [
'data' => $lib5,
'dependencies' => [],
],
];
$this->assertEquals($expected, $data);
if ($filerecord) {
// Confirm the H5P file has been created (when $filerecord is not empty).
$fs = get_file_storage();
$this->assertNotFalse($fs->get_file_by_hash($h5p->pathnamehash));
// Confirm xAPI state has been created when $filerecord['addxapistate'] is given.
if (array_key_exists('addxapistate', $filerecord) && $filerecord['addxapistate']) {
$this->assertEquals(1, $DB->count_records('xapi_states'));
} else {
$this->assertEquals(0, $DB->count_records('xapi_states'));
}
} else {
// Confirm the H5P file doesn't exist when $filerecord is null.
$fs = get_file_storage();
$this->assertFalse($fs->get_file_by_hash($h5p->pathnamehash));
// Confirm xAPI state hasn't been created when $filerecord is null.
$this->assertEquals(0, $DB->count_records('xapi_states'));
}
}
/**
* Data provider for test_generate_h5p_data_xapistates().
*
* @return array
*/
public function generate_h5p_data_xapistates_provider(): array {
return [
'Do not create the file nor xAPI states' => [
'filerecord' => null,
],
'Create the H5P file but not create any xAPI state' => [
'filerecord' => [
'addxapistate' => false,
],
],
'Create the H5P file and the xAPI state' => [
'filerecord' => [
'addxapistate' => true,
],
],
];
}
/**
* Test the behaviour of create_library_record(). Test whether the library data is properly
* saved in the database.

View File

@ -25,6 +25,7 @@
namespace mod_h5pactivity\local;
use core_xapi\handler;
use stdClass;
use core_xapi\local\statement;
@ -83,6 +84,11 @@ class attempt {
if (!$record->id) {
return null;
}
// Remove any xAPI State associated to this attempt.
$context = \context_module::instance($cm->id);
$xapihandler = handler::create('mod_h5pactivity');
$xapihandler->wipe_states($context->id);
return new attempt($record);
}

View File

@ -14,15 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defines {@link \mod_h5pactivity\privacy\provider} class.
*
* @package mod_h5pactivity
* @category privacy
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\privacy;
use core_privacy\local\metadata\collection;
@ -38,6 +29,8 @@ use stdClass;
/**
* Privacy API implementation for the H5P activity plugin.
*
* @package mod_h5pactivity
* @category privacy
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@ -46,16 +39,6 @@ class provider implements
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason() : string {
return 'privacy:metadata';
}
/**
* Return the fields which contain personal data.
*
@ -77,6 +60,8 @@ class provider implements
'rawscore' => 'privacy:metadata:rawscore',
], 'privacy:metadata:xapi_track_results');
$collection->add_subsystem_link('core_xapi', [], 'privacy:metadata:xapisummary');
return $collection;
}
@ -103,6 +88,8 @@ class provider implements
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, $params);
\core_xapi\privacy\provider::add_contexts_for_userid($contextlist, $userid, 'mod_h5pactivity');
return $contextlist;
}
@ -133,6 +120,8 @@ class provider implements
$params = ['modlevel' => CONTEXT_MODULE, 'contextid' => $context->id];
$userlist->add_from_sql('userid', $sql, $params);
\core_xapi\privacy\provider::add_userids_for_context($userlist);
}
/**
@ -163,6 +152,16 @@ class provider implements
$data = helper::get_context_data($context, $user);
writer::with_context($context)->export_data([], $data);
helper::export_context_files($context, $user);
// Get user's xAPI state data for the particular context.
$state = \core_xapi\privacy\provider::get_xapi_states_for_user($contextlist->get_user()->id,
'mod_h5pactivity', $context->instanceid);
if ($state) {
// If the activity has xAPI state data by the user, include it in the export.
writer::with_context($context)->export_data(
[get_string('privacy:xapistate', 'core_xapi')], (object) $state);
}
}
// Get attempts track data.
@ -226,7 +225,7 @@ class provider implements
/**
* Delete all user data which matches the specified context.
*
* @param context $context A user context.
* @param \context $context A user context.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
// This should not happen, but just in case.
@ -241,6 +240,10 @@ class provider implements
}
self::delete_all_attempts($cm);
// Delete xAPI state data.
\core_xapi\privacy\provider::delete_states_for_all_users($context, 'mod_h5pactivity');
}
/**
@ -264,6 +267,9 @@ class provider implements
$user = $contextlist->get_user();
self::delete_all_attempts($cm, $user);
// Delete xAPI state data.
\core_xapi\privacy\provider::delete_states_for_user($contextlist, 'mod_h5pactivity');
}
}
@ -291,6 +297,10 @@ class provider implements
foreach ($userids as $userid) {
self::delete_all_attempts ($cm, (object)['id' => $userid]);
}
// Delete xAPI states data.
\core_xapi\privacy\provider::delete_states_for_userlist($userlist);
}
/**

View File

@ -14,15 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The xapi_handler for xAPI statements.
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\xapi;
use mod_h5pactivity\local\attempt;
@ -31,7 +22,8 @@ use mod_h5pactivity\event\statement_received;
use core_xapi\local\statement;
use core_xapi\handler as handler_base;
use core\event\base as event_base;
use context_module;
use core_xapi\local\state;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
@ -39,11 +31,12 @@ global $CFG;
require_once($CFG->dirroot.'/mod/h5pactivity/lib.php');
/**
* Class xapi_handler for H5P statements.
* Class xapi_handler for H5P statements and states.
*
* @package mod_h5pactivity
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class handler extends handler_base {
@ -145,4 +138,49 @@ class handler extends handler_base {
];
return statement_received::create($params);
}
/**
* Validate a xAPI state.
*
* Check if the state is valid for this handler.
*
* This method is used also for the state get requests so the validation
* cannot rely on having state data.
*
* @param state $state
* @return bool if the state is valid or not
*/
protected function validate_state(state $state): bool {
$xapiobject = $state->get_activity_id();
// H5P add some extra params to ID to define subcontents.
$parts = explode('?', $xapiobject, 2);
$contextid = array_shift($parts);
if (empty($contextid) || !is_numeric($contextid)) {
return false;
}
try {
$context = \context::instance_by_id($contextid);
if (!$context instanceof \context_module) {
return false;
}
} catch (moodle_exception $exception) {
return false;
}
$cm = get_coursemodule_from_id('h5pactivity', $context->instanceid, 0, false);
if (!$cm) {
return false;
}
// If tracking is not enabled, the state won't be considered valid.
$manager = manager::create_from_coursemodule($cm);
$user = $state->get_user();
if (!$manager->is_tracking_enabled($user)) {
return false;
}
return true;
}
}

View File

@ -62,6 +62,8 @@ $string['displayembed'] = 'Embed button';
$string['displaycopyright'] = 'Copyright button';
$string['dnduploadh5pactivity'] = 'Add an H5P activity';
$string['duration'] = 'Duration';
$string['enablesavestate'] = 'Save state';
$string['enablesavestate_help'] = 'Automatically save the user\'s current state. The user can return later and resume where they left off.';
$string['enabletracking'] = 'Enable attempt tracking';
$string['false'] = 'False';
$string['grade_grademethod'] = 'Grading method';
@ -112,6 +114,7 @@ $string['privacy:metadata:rawscore'] = 'The score obtained';
$string['privacy:metadata:timecreated'] = 'The time when the tracked element was created';
$string['privacy:metadata:timemodified'] = 'The last time element was tracked';
$string['privacy:metadata:userid'] = 'The ID of the user who accessed the H5P activity';
$string['privacy:metadata:xapisummary'] = 'The H5P activity contains information relating to the xAPI content state stored by the user.';
$string['privacy:metadata:xapi_track'] = 'Attempt tracking information';
$string['privacy:metadata:xapi_track_results'] = 'Attempt results tracking information';
$string['report_viewed'] = 'Report viewed';
@ -129,6 +132,8 @@ $string['review_my_attempts'] = 'View my attempts';
$string['review_user_attempts'] = 'View user attempts ({$a})';
$string['review_none'] = 'Participants cannot review their own attempts';
$string['review_on_completion'] = 'Participants can review their own attempts';
$string['savestatefreq'] = 'Save state frequency';
$string['savestatefreq_help'] = 'How often (in seconds) that the user\'s current state is saved.';
$string['score'] = 'Score';
$string['score_out_of'] = '{$a->rawscore} out of {$a->maxscore}';
$string['search:activity'] = 'H5P - activity information';

View File

@ -26,6 +26,7 @@ defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\local\manager;
use mod_h5pactivity\local\grader;
use mod_h5pactivity\xapi\handler;
/**
* Checks if H5P activity supports a specific feature.
@ -145,6 +146,12 @@ function h5pactivity_delete_instance(int $id): bool {
return false;
}
if ($cm = get_coursemodule_from_instance('h5pactivity', $activity->id)) {
$context = context_module::instance($cm->id);
$xapihandler = handler::create('mod_h5pactivity');
$xapihandler->wipe_states($context->id);
}
$DB->delete_records('h5pactivity', ['id' => $id]);
h5pactivity_grade_item_delete($activity);
@ -270,6 +277,7 @@ function h5pactivity_reset_userdata(stdClass $data): array {
$params = ['courseid' => $data->courseid];
$sql = "SELECT a.id FROM {h5pactivity} a WHERE a.course=:courseid";
if ($activities = $DB->get_records_sql($sql, $params)) {
$xapihandler = handler::create('mod_h5pactivity');
foreach ($activities as $activity) {
$cm = get_coursemodule_from_instance('h5pactivity',
$activity->id,
@ -277,6 +285,8 @@ function h5pactivity_reset_userdata(stdClass $data): array {
false,
MUST_EXIST);
mod_h5pactivity\local\attempt::delete_all_attempts ($cm);
$context = context_module::instance($cm->id);
$xapihandler->wipe_states($context->id);
}
}
// Remove all grades from gradebook.

View File

@ -0,0 +1,33 @@
<?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/>.
/**
* Module admin settings.
*
* @package mod_h5pactivity
* @copyright 2023 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
if ($ADMIN->fulltree) {
$settings->add(new admin_setting_configcheckbox('mod_h5pactivity/enablesavestate',
get_string('enablesavestate', 'mod_h5pactivity'), get_string('enablesavestate_help', 'mod_h5pactivity'), 1));
$settings->add(new admin_setting_configtext('mod_h5pactivity/savestatefreq',
get_string('savestatefreq', 'mod_h5pactivity'), get_string('savestatefreq_help', 'mod_h5pactivity'), 60, PARAM_INT));
}

View File

@ -0,0 +1,143 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript
Feature: Users can save the current state of an H5P activity
In order to continue an H5P activity where I left
As a user
I need to be able to save the current state
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| packagefilepath | h5p/tests/fixtures/filltheblanks.h5p |
Scenario: Content state is not saved when enablesavestate is disabled
Given the following config values are set as admin:
| enablesavestate | 0 | mod_h5pactivity|
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
When 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
Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia"
Scenario: Content state is saved when enablesavestate is enabled
Given the following config values are set as admin:
| enablesavestate | 1 | mod_h5pactivity|
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
When 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
Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Narnia"
Scenario: Content state is not saved for teachers when enablesavestate is enabled
Given the following config values are set as admin:
| enablesavestate | 1 | mod_h5pactivity|
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1
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
When 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
Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia"
Scenario: Content state is reseted when content changes
Given the following config values are set as admin:
| enablesavestate | 1 | mod_h5pactivity|
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
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as admin
# Change the content.
And I follow "Edit H5P content"
And I switch to "h5p-editor-iframe" class iframe
And I set the field "Title" to "Capitals"
And I switch to the main frame
And I click on "Save changes" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Check"
# Check the content state has been reseted.
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
Then I should see "Data Reset"
And I should see "This content has changed since you last used it."
And I click on "OK" "button"
And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia"
Scenario: Content state is not reseted when content edition is cancelled
Given the following config values are set as admin:
| enablesavestate | 1 | mod_h5pactivity|
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
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as admin
# Start content edition.
And I follow "Edit H5P content"
And I switch to "h5p-editor-iframe" class iframe
And I set the field "Title" to "Capitals"
And I switch to the main frame
And I click on "Cancel" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Check"
# Check the content state hasn't been reseted.
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I should see "Awesome H5P package"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then I should not see "Data Reset"
And I should not see "This content has changed since you last used it."
And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Narnia"
Scenario: Content state is removed when an attempt is created
Given the following config values are set as admin:
| enablesavestate | 1 | mod_h5pactivity|
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
# Check there are no attempts.
And I should not see "Attempts report"
# Create an attempt.
When 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 click on "Check" "button"
# Check the state content has been removed.
And I reload the page
Then I should see "Attempts report"
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\")]" does not match value "Narnia"

View File

@ -14,14 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for (some of) mod/h5pactivity/lib.php.
*
* @package mod_h5pactivity
* @copyright 2021 Ilya Tregubov <ilya@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_h5pactivity\local\manager;
defined('MOODLE_INTERNAL') || die();
@ -32,11 +24,81 @@ require_once($CFG->dirroot . '/mod/h5pactivity/lib.php');
/**
* Unit tests for (some of) mod/h5pactivity/lib.php.
*
* @package mod_h5pactivity
* @copyright 2021 Ilya Tregubov <ilya@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lib_test extends advanced_testcase {
/**
* Test that h5pactivity_delete_instance removes data.
*
* @covers ::h5pactivity_delete_instance
*/
public function test_h5pactivity_delete_instance() {
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->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check nothing happens when given activity id doesn't exist.
h5pactivity_delete_instance($activity->id + 1);
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check the H5P instance and its associated data is removed.
h5pactivity_delete_instance($activity->id);
$this->assertEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(1, $DB->count_records('grade_items'));
$this->assertEquals(1, $DB->count_records('grade_grades'));
$this->assertEquals(0, $DB->count_records('xapi_states'));
}
/**
* Test that assign_print_recent_activity shows ungraded submitted assignments.
*/
@ -239,4 +301,102 @@ class lib_test extends advanced_testcase {
$this->assertEquals($students[1]->id, $recentactivity[$students[1]->id]->userid);
$this->assertEquals($students[2]->id, $recentactivity[$students[2]->id]->userid);
}
/**
* Test that h5pactivity_reset_userdata reset user data.
*
* @covers ::h5pactivity_reset_userdata
*/
public function test_h5pactivity_reset_userdata() {
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 with the expected data.
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check nothing happens when reset_h5pactivity is not set.
$data = new stdClass();
h5pactivity_reset_userdata($data);
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check nothing happens when reset_h5pactivity is not set.
$data = (object) [
'courseid' => $course->id,
];
h5pactivity_reset_userdata($data);
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check nothing happens when the given course doesn't exist.
$data = (object) [
'reset_h5pactivity' => true,
'courseid' => $course->id + 1,
];
h5pactivity_reset_userdata($data);
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check the H5P instance and its associated data is reset.
$data = (object) [
'reset_h5pactivity' => true,
'courseid' => $course->id,
];
h5pactivity_reset_userdata($data);
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(1, $DB->count_records('grade_grades'));
$this->assertEquals(0, $DB->count_records('xapi_states'));
}
}

View File

@ -32,6 +32,7 @@ use \core_xapi\local\statement\item_activity;
use \core_xapi\local\statement\item_definition;
use \core_xapi\local\statement\item_verb;
use \core_xapi\local\statement\item_result;
use core_xapi\test_helper;
use stdClass;
/**
@ -64,14 +65,25 @@ class attempt_test extends \advanced_testcase {
* Test for create_attempt method.
*/
public function test_create_attempt() {
global $CFG, $DB;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
list($cm, $student) = $this->generate_testing_scenario();
// Save the current state for this activity (before creating the first attempt).
$manager = manager::create_from_coursemodule($cm);
test_helper::create_state([
'activity' => item_activity::create_from_id($manager->get_context()->id),
'component' => 'mod_h5pactivity',
], true);
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Create first attempt.
$attempt = attempt::new_attempt($student, $cm);
$this->assertEquals($student->id, $attempt->get_userid());
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
$this->assertEquals(1, $attempt->get_attempt());
$this->assertEquals(0, $DB->count_records('xapi_states'));
// Create a second attempt.
$attempt = attempt::new_attempt($student, $cm);

View File

@ -29,6 +29,9 @@ use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\approved_userlist;
use \core_privacy\local\request\writer;
use \core_privacy\tests\provider_testcase;
use core_xapi\local\statement\item_activity;
use core_xapi\test_helper;
use stdClass;
/**
* Privacy tests class for mod_h5pactivity.
@ -37,6 +40,7 @@ use \core_privacy\tests\provider_testcase;
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_h5pactivity\privacy\provider
*/
class provider_test extends provider_testcase {
@ -49,7 +53,10 @@ class provider_test extends provider_testcase {
/** @var stdClass User with some attempt. */
protected $student2;
/** @var context context_module of the H5P activity. */
/** @var stdClass User with some attempt. */
protected $student3;
/** @var \context context_module of the H5P activity. */
protected $context;
/**
@ -149,24 +156,22 @@ class provider_test extends provider_testcase {
$this->resetAfterTest(true);
$this->setAdminUser();
$this->h5pactivity_setup_test_scenario_data();
$this->h5pactivity_setup_test_scenario_data(true);
// Before deletion, we should have 4 entries in the attempts table.
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(4, $count);
// Before deletion, we should have 12 entries in the results table.
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(12, $count);
// Check data before deletion.
$this->assertEquals(6, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals(18, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(2, $DB->count_records('xapi_states'));
// Delete data based on the context.
provider::delete_data_for_all_users_in_context($this->context);
// After deletion, the attempts entries should have been deleted.
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(0, $count);
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts'));
// After deletion, the results entries should have been deleted.
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(0, $count);
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts_results'));
// After deletion, the xapi states should have been deleted.
$this->assertEquals(0, $DB->count_records('xapi_states'));
}
/**
@ -177,16 +182,14 @@ class provider_test extends provider_testcase {
$this->resetAfterTest(true);
$this->setAdminUser();
$this->h5pactivity_setup_test_scenario_data();
$this->h5pactivity_setup_test_scenario_data(true);
$params = ['userid' => $this->student1->id];
// Before deletion, we should have 4 entries in the attempts table.
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(4, $count);
// Before deletion, we should have 12 entries in the results table.
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(12, $count);
// Check data before deletion.
$this->assertEquals(6, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals(18, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(2, $DB->count_records('xapi_states'));
// Save student1 attempts ids.
$attemptsids = $DB->get_records_menu('h5pactivity_attempts', $params, '', 'attempt, id');
@ -197,16 +200,15 @@ class provider_test extends provider_testcase {
provider::delete_data_for_user($approvedcontextlist);
// After deletion, the h5pactivity_attempts entries for the first student should have been deleted.
$count = $DB->count_records('h5pactivity_attempts', $params);
$this->assertEquals(0, $count);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(2, $count);
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts', $params));
$this->assertEquals(4, $DB->count_records('h5pactivity_attempts'));
// After deletion, the results entries for the first student should have been deleted.
$count = $DB->count_records_select('h5pactivity_attempts_results', $resultselect, $attemptids);
$this->assertEquals(0, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(6, $count);
$this->assertEquals(12, $DB->count_records('h5pactivity_attempts_results'));
// After deletion, the results entries for the first student should have been deleted.
$this->assertEquals(0, $DB->count_records('xapi_states', $params));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Confirm that the h5pactivity hasn't been removed.
$h5pactivitycount = $DB->get_records('h5pactivity');
@ -216,10 +218,9 @@ class provider_test extends provider_testcase {
$approvedcontextlist = new approved_contextlist($this->student0, 'h5pactivity', [$this->context->id]);
provider::delete_data_for_user($approvedcontextlist);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(2, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(6, $count);
$this->assertEquals(4, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals(12, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
}
/**
@ -235,12 +236,10 @@ class provider_test extends provider_testcase {
// Create student2 with 2 attempts.
$this->h5pactivity_setup_test_scenario_data(true);
// Before deletion, we should have 6 entries in the attempts table.
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(6, $count);
// Before deletion, we should have 18 entries in the results table.
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(18, $count);
// Check data before deletion.
$this->assertEquals(6, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals(18, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(2, $DB->count_records('xapi_states'));
// Save student1 and student2 attempts ids.
$params1 = ['userid' => $this->student1->id];
@ -256,18 +255,17 @@ class provider_test extends provider_testcase {
provider::delete_data_for_users($approvedlist);
// After deletion, the h5pactivity_attempts entries for student1 and student2 should have been deleted.
$count = $DB->count_records('h5pactivity_attempts', $params1);
$this->assertEquals(0, $count);
$count = $DB->count_records('h5pactivity_attempts', $params2);
$this->assertEquals(0, $count);
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts', $params1));
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts', $params2));
$this->assertEquals(0, $DB->count_records('xapi_states', $params1));
$this->assertEquals(0, $DB->count_records('xapi_states', $params2));
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(2, $count);
$this->assertEquals(2, $DB->count_records('h5pactivity_attempts'));
// After deletion, the results entries for the first and second student should have been deleted.
$count = $DB->count_records_select('h5pactivity_attempts_results', $resultselect, $attemptids);
$this->assertEquals(0, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(6, $count);
$this->assertEquals(6, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Confirm that the h5pactivity hasn't been removed.
$h5pactivitycount = $DB->get_records('h5pactivity');
@ -278,10 +276,9 @@ class provider_test extends provider_testcase {
$approvedlist = new approved_userlist($this->context, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(2, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(6, $count);
$this->assertEquals(2, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals(6, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
}
/**
@ -291,7 +288,8 @@ class provider_test extends provider_testcase {
* @param bool $extrauser generate a 3rd user (default false).
*/
protected function h5pactivity_setup_test_scenario_data(bool $extrauser = false): void {
global $DB;
global $CFG, $USER;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
$generator = $this->getDataGenerator();
@ -301,19 +299,24 @@ class provider_test extends provider_testcase {
$cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST);
$this->context = \context_module::instance($activity->cmid);
// Users enrolments.
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
/** @var \mod_h5pactivity_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
// Create student0 withot any attempt.
// Create student0 without any attempt.
$this->student0 = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Create student1 with 2 attempts.
// Create student1 with 2 attempts and 1 xapi state.
$this->student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
$params = ['cmid' => $cm->id, 'userid' => $this->student1->id];
$generator->create_content($activity, $params);
$generator->create_content($activity, $params);
$currentuser = $USER;
$this->setUser($this->student1);
test_helper::create_state([
'activity' => item_activity::create_from_id($this->context->id),
'component' => 'mod_h5pactivity',
], true);
$this->setUser($currentuser);
// Create student2 with 2 attempts.
$this->student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
@ -326,6 +329,14 @@ class provider_test extends provider_testcase {
$params = ['cmid' => $cm->id, 'userid' => $this->student3->id];
$generator->create_content($activity, $params);
$generator->create_content($activity, $params);
// Add 1 xapi state.
$currentuser = $USER;
$this->setUser($this->student3);
test_helper::create_state([
'activity' => item_activity::create_from_id($this->context->id),
'component' => 'mod_h5pactivity',
], true);
$this->setUser($currentuser);
}
}
}

View File

@ -14,15 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* mod_h5pactivity generator tests
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\xapi;
use \core_xapi\local\statement;
@ -32,6 +23,7 @@ use \core_xapi\local\statement\item_definition;
use \core_xapi\local\statement\item_verb;
use \core_xapi\local\statement\item_result;
use context_module;
use core_xapi\test_helper;
use stdClass;
/**
@ -41,9 +33,18 @@ use stdClass;
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_h5pactivity\xapi\handler
*/
class handler_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
}
/**
* Generate a valid scenario for each tests.
*
@ -386,4 +387,89 @@ class handler_test extends \advanced_testcase {
return $statements;
}
/**
* Test validate_state method.
*/
public function test_validate_state(): void {
global $DB;
$this->resetAfterTest();
/** @var \core_h5p_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
// Create a valid H5P activity with a valid xAPI state.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$this->setUser($user);
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$coursecontext = \context_course::instance($course->id);
$activitycontext = \context_module::instance($activity->cmid);
$component = 'mod_h5pactivity';
$filerecord = [
'contextid' => $activitycontext->id,
'component' => $component,
'filearea' => 'package',
'itemid' => 0,
'filepath' => '/',
'filename' => 'dummy.h5p',
'addxapistate' => true,
];
$generator->generate_h5p_data(false, $filerecord);
$handler = handler::create($component);
// Change the method visibility for validate_state in order to test it.
$method = new \ReflectionMethod(handler::class, 'validate_state');
$method->setAccessible(true);
// The activity id should be numeric.
$state = test_helper::create_state(['activity' => item_activity::create_from_id('AA')]);
$result = $method->invoke($handler, $state);
$this->assertFalse($result);
// The activity id should exist.
$state = test_helper::create_state();
$result = $method->invoke($handler, $state);
$this->assertFalse($result);
// The given activity should be H5P activity.
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course]);
$state = test_helper::create_state([
'activity' => item_activity::create_from_id($forum->cmid),
]);
$result = $method->invoke($handler, $state);
$this->assertFalse($result);
// Tracking should be enabled for the H5P activity.
$state = test_helper::create_state([
'activity' => item_activity::create_from_id($activitycontext->id),
'component' => $component,
]);
$result = $method->invoke($handler, $state);
$this->assertTrue($result);
// So, when tracking is disabled, the state won't be considered valid.
$activity2 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => 0]);
$activitycontext2 = \context_module::instance($activity2->cmid);
$state = test_helper::create_state([
'activity' => item_activity::create_from_id($activitycontext2->id),
'component' => $component,
]);
$result = $method->invoke($handler, $state);
$this->assertFalse($result);
// The user should have permission to submit.
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
assign_capability('mod/h5pactivity:submit', CAP_PROHIBIT, $studentrole->id, $coursecontext->id);
// Empty all the caches that may be affected by this change.
accesslib_clear_all_caches_for_unit_testing();
\course_modinfo::clear_instance_cache();
$state = test_helper::create_state([
'activity' => item_activity::create_from_id($activitycontext->id),
'component' => $component,
]);
$result = $method->invoke($handler, $state);
$this->assertFalse($result);
}
}

View File

@ -25,5 +25,5 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_h5pactivity';
$plugin->version = 2022112800;
$plugin->version = 2023020900;
$plugin->requires = 2022111800;