Merge branch 'MDL-67789-master' of https://github.com/sarjona/moodle
This commit is contained in:
@@ -264,6 +264,14 @@ if ($hassiteconfig) {
|
||||
]
|
||||
));
|
||||
|
||||
$temp->add(new admin_setting_configduration(
|
||||
'xapicleanupperiod',
|
||||
new lang_string('xapicleanupperiod', 'xapi'),
|
||||
new lang_string('xapicleanupperiod_help', 'xapi'),
|
||||
WEEKSECS * 8,
|
||||
WEEKSECS
|
||||
));
|
||||
|
||||
$ADMIN->add('server', $temp);
|
||||
|
||||
$temp->add(new admin_setting_configduration('filescleanupperiod',
|
||||
|
||||
Vendored
+10
@@ -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
|
||||
@@ -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 <[email protected]>\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"}
|
||||
@@ -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 <[email protected]>
|
||||
* @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];
|
||||
+39
-12
@@ -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);
|
||||
|
||||
+16
-9
@@ -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' => []
|
||||
|
||||
+65
-4
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
+90
-14
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 <[email protected]>
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+39
-18
@@ -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 <[email protected]>
|
||||
* @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,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <[email protected]>
|
||||
* @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 <[email protected]>
|
||||
* @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.
|
||||
|
||||
+13
-1
@@ -23,4 +23,16 @@
|
||||
*/
|
||||
|
||||
$string['eventxapipost'] = 'Post xAPI statement';
|
||||
$string['privacy:metadata'] = 'The xAPI library does not store any personal data.';
|
||||
$string['privacy:metadata:component'] = 'The component name in frankenstyle';
|
||||
$string['privacy:metadata:itemid'] = 'The item ID of the state';
|
||||
$string['privacy:metadata:registration'] = 'The xAPI registration UUID';
|
||||
$string['privacy:metadata:statedata'] = 'JSON object with the state data';
|
||||
$string['privacy:metadata:stateid'] = 'The xAPI state id.';
|
||||
$string['privacy:metadata:timecreated'] = 'The time when the state element was created';
|
||||
$string['privacy:metadata:timemodified'] = 'The last time state was updated';
|
||||
$string['privacy:metadata:userid'] = 'The ID of the user who belongs the state ';
|
||||
$string['privacy:metadata:xapi_states'] = 'The stored xAPI states';
|
||||
$string['privacy:xapistate'] = 'xAPI state';
|
||||
$string['xapicleanup'] = 'Stored xAPI states clean up';
|
||||
$string['xapicleanupperiod'] = 'Clean up xAPI states';
|
||||
$string['xapicleanupperiod_help'] = 'Remove any stored xAPI which is not updaded in the selected period';
|
||||
|
||||
@@ -237,6 +237,11 @@ function uninstall_plugin($type, $name) {
|
||||
unset_all_config_for_plugin($pluginname);
|
||||
}
|
||||
|
||||
// Wipe any xAPI state information.
|
||||
if (core_xapi\handler::supports_xapi($component)) {
|
||||
core_xapi\api::remove_states_from_component($component);
|
||||
}
|
||||
|
||||
// delete message provider
|
||||
message_provider_uninstall($component);
|
||||
|
||||
|
||||
@@ -4764,5 +4764,26 @@
|
||||
<INDEX NAME="userid-coursemoduleid" UNIQUE="true" FIELDS="userid, coursemoduleid"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
<TABLE NAME="xapi_states" COMMENT="The stored xAPI states">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="component" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The component name"/>
|
||||
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The Agent Id (usually the plugin instance)"/>
|
||||
<FIELD NAME="stateid" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Component identified for the state data"/>
|
||||
<FIELD NAME="statedata" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="JSON state data"/>
|
||||
<FIELD NAME="registration" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Optional registration identifier"/>
|
||||
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
</KEYS>
|
||||
<INDEXES>
|
||||
<INDEX NAME="component-itemid" UNIQUE="false" FIELDS="component, itemid"/>
|
||||
<INDEX NAME="userid" UNIQUE="false" FIELDS="userid"/>
|
||||
<INDEX NAME="timemodified" UNIQUE="false" FIELDS="timemodified"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
|
||||
@@ -2758,6 +2758,33 @@ $functions = array(
|
||||
'capabilities' => '',
|
||||
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
|
||||
],
|
||||
'core_xapi_post_state' => [
|
||||
'classname' => 'core_xapi\external\post_state',
|
||||
'classpath' => '',
|
||||
'description' => 'Post an xAPI state into an activityId.',
|
||||
'type' => 'write',
|
||||
'ajax' => true,
|
||||
'capabilities' => '',
|
||||
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
|
||||
],
|
||||
'core_xapi_get_state' => [
|
||||
'classname' => 'core_xapi\external\get_state',
|
||||
'classpath' => '',
|
||||
'description' => 'Get an xAPI state data from an activityId.',
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
'capabilities' => '',
|
||||
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
|
||||
],
|
||||
'core_xapi_delete_state' => [
|
||||
'classname' => 'core_xapi\external\delete_state',
|
||||
'classpath' => '',
|
||||
'description' => 'Delete an xAPI state data from an activityId.',
|
||||
'type' => 'write',
|
||||
'ajax' => true,
|
||||
'capabilities' => '',
|
||||
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
|
||||
],
|
||||
'core_contentbank_delete_content' => [
|
||||
'classname' => 'core_contentbank\external\delete_content',
|
||||
'classpath' => '',
|
||||
|
||||
+10
-1
@@ -436,5 +436,14 @@ $tasks = array(
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*'
|
||||
]
|
||||
],
|
||||
[
|
||||
'classname' => 'core_xapi\task\state_cleanup_task',
|
||||
'blocking' => 0,
|
||||
'minute' => 'R',
|
||||
'hour' => '0',
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*'
|
||||
],
|
||||
);
|
||||
|
||||
@@ -3162,5 +3162,42 @@ privatefiles,moodle|/user/files.php';
|
||||
upgrade_main_savepoint(true, 2023031400.01);
|
||||
}
|
||||
|
||||
if ($oldversion < 2023031400.02) {
|
||||
|
||||
// Define table xapi_states to be created.
|
||||
$table = new xmldb_table('xapi_states');
|
||||
|
||||
// Adding fields to table xapi_states.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('component', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
|
||||
$table->add_field('itemid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('stateid', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('statedata', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('registration', XMLDB_TYPE_CHAR, '255', null, null, null, null);
|
||||
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
|
||||
|
||||
// Adding keys to table xapi_states.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
|
||||
// Adding indexes to table xapi_states.
|
||||
$table->add_index('component-itemid', XMLDB_INDEX_NOTUNIQUE, ['component', 'itemid']);
|
||||
$table->add_index('userid', XMLDB_INDEX_NOTUNIQUE, ['userid']);
|
||||
$table->add_index('timemodified', XMLDB_INDEX_NOTUNIQUE, ['timemodified']);
|
||||
|
||||
// Conditionally launch create table for xapi_states.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
if (!isset($CFG->xapicleanupperiod)) {
|
||||
set_config('xapicleanupperiod', WEEKSECS * 8);
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2023031400.02);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi;
|
||||
|
||||
/**
|
||||
* The xAPI internal API.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @copyright 2023 Ferran Recio
|
||||
* @since Moodle 4.2
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class api {
|
||||
/**
|
||||
* Delete all states from a component.
|
||||
*
|
||||
* @param string $component The component name in frankenstyle.
|
||||
* @return void
|
||||
*/
|
||||
public static function remove_states_from_component(string $component): void {
|
||||
global $DB;
|
||||
|
||||
$statestore = null;
|
||||
$dbman = $DB->get_manager();
|
||||
try {
|
||||
$handler = handler::create($component);
|
||||
$statestore = $handler->get_state_store();
|
||||
} catch (xapi_exception $exception) {
|
||||
// If the component is not available but the xapi_states table exists, use the standard one to ensure we clean it.
|
||||
$table = new \xmldb_table('xapi_states');
|
||||
if ($dbman->table_exists($table)) {
|
||||
$statestore = new state_store($component);
|
||||
}
|
||||
}
|
||||
if ($statestore) {
|
||||
$statestore->wipe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the states clean up for all compatible components.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function execute_state_cleanup(): void {
|
||||
foreach (\core_component::get_plugin_types() as $ptype => $unused) {
|
||||
$components = \core_component::get_plugin_list_with_class($ptype, 'xapi\handler');
|
||||
foreach ($components as $component => $unused) {
|
||||
$handler = handler::create($component);
|
||||
$statestore = $handler->get_state_store();
|
||||
$statestore->cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi\external;
|
||||
|
||||
use core_xapi\local\state;
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use core_xapi\handler;
|
||||
use core_xapi\xapi_exception;
|
||||
use core_external\external_api;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_value;
|
||||
use core_xapi\iri;
|
||||
|
||||
/**
|
||||
* This is the external API for generic xAPI state deletion.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 4.2
|
||||
* @copyright 2023 Ferran Recio
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class delete_state extends external_api {
|
||||
|
||||
use \core_xapi\local\helper\state_trait;
|
||||
|
||||
/**
|
||||
* Parameters for execute.
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters([
|
||||
'component' => new external_value(PARAM_COMPONENT, 'Component name'),
|
||||
'activityId' => new external_value(PARAM_URL, 'xAPI activity ID IRI'),
|
||||
'agent' => new external_value(PARAM_RAW, 'The xAPI agent json'),
|
||||
'stateId' => new external_value(PARAM_ALPHAEXT, 'The xAPI state ID'),
|
||||
'registration' => new external_value(PARAM_ALPHANUMEXT, 'The xAPI registration UUID', VALUE_DEFAULT, null),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a state delete request.
|
||||
*
|
||||
* @param string $component The component name in frankenstyle.
|
||||
* @param string $activityiri The activity IRI.
|
||||
* @param string $agent The agent JSON.
|
||||
* @param string $stateid The xAPI state id.
|
||||
* @param string|null $registration The xAPI registration UUID.
|
||||
* @return bool Whether the state has been removed or not.
|
||||
*/
|
||||
public static function execute(
|
||||
string $component,
|
||||
string $activityiri,
|
||||
string $agent,
|
||||
string $stateid,
|
||||
?string $registration = null
|
||||
): bool {
|
||||
|
||||
$params = self::validate_parameters(self::execute_parameters(), [
|
||||
'component' => $component,
|
||||
'activityId' => $activityiri,
|
||||
'agent' => $agent,
|
||||
'stateId' => $stateid,
|
||||
'registration' => $registration,
|
||||
]);
|
||||
[
|
||||
'component' => $component,
|
||||
'activityId' => $activityiri,
|
||||
'agent' => $agent,
|
||||
'stateId' => $stateid,
|
||||
'registration' => $registration,
|
||||
] = $params;
|
||||
|
||||
static::validate_component($component);
|
||||
|
||||
$handler = handler::create($component);
|
||||
$activityid = iri::extract($activityiri, 'activity');
|
||||
|
||||
$state = new state(
|
||||
self::get_agent_from_json($agent),
|
||||
item_activity::create_from_id($activityid),
|
||||
$stateid,
|
||||
$registration,
|
||||
null
|
||||
);
|
||||
|
||||
if (!self::check_state_user($state)) {
|
||||
throw new xapi_exception('State agent is not the current user');
|
||||
}
|
||||
|
||||
return $handler->delete_state($state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return for execute.
|
||||
*/
|
||||
public static function execute_returns(): external_value {
|
||||
return new external_value(PARAM_BOOL, 'If the state data is deleted');
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi\external;
|
||||
|
||||
use core_xapi\local\state;
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use core_xapi\handler;
|
||||
use core_xapi\xapi_exception;
|
||||
use core_external\external_api;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_value;
|
||||
use core_xapi\iri;
|
||||
|
||||
/**
|
||||
* This is the external API for generic xAPI state get.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 4.2
|
||||
* @copyright 2023 Ferran Recio
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class get_state extends external_api {
|
||||
|
||||
use \core_xapi\local\helper\state_trait;
|
||||
|
||||
/**
|
||||
* Parameters for execute
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters([
|
||||
'component' => new external_value(PARAM_COMPONENT, 'Component name'),
|
||||
'activityId' => new external_value(PARAM_URL, 'xAPI activity ID IRI'),
|
||||
'agent' => new external_value(PARAM_RAW, 'The xAPI agent json'),
|
||||
'stateId' => new external_value(PARAM_ALPHAEXT, 'The xAPI state ID'),
|
||||
'registration' => new external_value(PARAM_ALPHANUMEXT, 'The xAPI registration UUID', VALUE_DEFAULT, null),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a get state request.
|
||||
*
|
||||
* @param string $component The component name in frankenstyle.
|
||||
* @param string $activityiri The activity IRI.
|
||||
* @param string $agent The agent JSON.
|
||||
* @param string $stateid The xAPI state id.
|
||||
* @param string|null $registration The xAPI registration UUID.
|
||||
* @return string|null
|
||||
*/
|
||||
public static function execute(
|
||||
string $component,
|
||||
string $activityiri,
|
||||
string $agent,
|
||||
string $stateid,
|
||||
?string $registration = null
|
||||
): ?string {
|
||||
|
||||
$params = self::validate_parameters(self::execute_parameters(), [
|
||||
'component' => $component,
|
||||
'activityId' => $activityiri,
|
||||
'agent' => $agent,
|
||||
'stateId' => $stateid,
|
||||
'registration' => $registration,
|
||||
]);
|
||||
[
|
||||
'component' => $component,
|
||||
'activityId' => $activityiri,
|
||||
'agent' => $agent,
|
||||
'stateId' => $stateid,
|
||||
'registration' => $registration,
|
||||
] = $params;
|
||||
|
||||
static::validate_component($component);
|
||||
|
||||
$handler = handler::create($component);
|
||||
$activityid = iri::extract($activityiri, 'activity');
|
||||
|
||||
$state = new state(
|
||||
self::get_agent_from_json($agent),
|
||||
item_activity::create_from_id($activityid),
|
||||
$stateid,
|
||||
null,
|
||||
$registration
|
||||
);
|
||||
|
||||
if (!self::check_state_user($state)) {
|
||||
throw new xapi_exception('State agent is not the current user');
|
||||
}
|
||||
|
||||
$result = $handler->load_state($state);
|
||||
if ($result !== null) {
|
||||
return json_encode($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return for execute.
|
||||
*/
|
||||
public static function execute_returns(): external_value {
|
||||
return new external_value(PARAM_RAW, 'The state data json');
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi\external;
|
||||
|
||||
use core_xapi\local\state;
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use core_xapi\handler;
|
||||
use core_xapi\xapi_exception;
|
||||
use core_external\external_api;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_value;
|
||||
use core_xapi\iri;
|
||||
|
||||
/**
|
||||
* This is the external API for generic xAPI state post.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 4.2
|
||||
* @copyright 2023 Ferran Recio
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class post_state extends external_api {
|
||||
|
||||
use \core_xapi\local\helper\state_trait;
|
||||
|
||||
/**
|
||||
* Parameters for execute
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters([
|
||||
'component' => new external_value(PARAM_COMPONENT, 'Component name'),
|
||||
'activityId' => new external_value(PARAM_URL, 'xAPI activity ID IRI'),
|
||||
'agent' => new external_value(PARAM_RAW, 'The xAPI agent json'),
|
||||
'stateId' => new external_value(PARAM_ALPHAEXT, 'The xAPI state ID'),
|
||||
'stateData' => new external_value(PARAM_RAW, 'JSON object with the state data'),
|
||||
'registration' => new external_value(PARAM_ALPHANUMEXT, 'The xAPI registration UUID', VALUE_DEFAULT, null),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a state post request.
|
||||
*
|
||||
* @param string $component The component name in frankenstyle.
|
||||
* @param string $activityiri The activity IRI.
|
||||
* @param string $agent The agent JSON.
|
||||
* @param string $stateid The xAPI state id.
|
||||
* @param string $statedata JSON object with the state data
|
||||
* @param string|null $registration The xAPI registration UUID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function execute(
|
||||
string $component,
|
||||
string $activityiri,
|
||||
string $agent,
|
||||
string $stateid,
|
||||
string $statedata,
|
||||
?string $registration = null
|
||||
): bool {
|
||||
|
||||
$params = self::validate_parameters(self::execute_parameters(), [
|
||||
'component' => $component,
|
||||
'activityId' => $activityiri,
|
||||
'agent' => $agent,
|
||||
'stateId' => $stateid,
|
||||
'stateData' => $statedata,
|
||||
'registration' => $registration,
|
||||
]);
|
||||
[
|
||||
'component' => $component,
|
||||
'activityId' => $activityiri,
|
||||
'agent' => $agent,
|
||||
'stateId' => $stateid,
|
||||
'stateData' => $statedata,
|
||||
'registration' => $registration,
|
||||
] = $params;
|
||||
|
||||
static::validate_component($component);
|
||||
|
||||
$handler = handler::create($component);
|
||||
$activityid = iri::extract($activityiri, 'activity');
|
||||
|
||||
$state = new state(
|
||||
self::get_agent_from_json($agent),
|
||||
item_activity::create_from_id($activityid),
|
||||
$stateid,
|
||||
self::get_statedata_from_json($statedata),
|
||||
$registration
|
||||
);
|
||||
|
||||
if (!self::check_state_user($state)) {
|
||||
throw new xapi_exception('State agent is not the current user');
|
||||
}
|
||||
|
||||
return $handler->save_state($state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return for execute.
|
||||
*/
|
||||
public static function execute_returns(): external_value {
|
||||
return new external_value(PARAM_BOOL, 'If the state is accepted');
|
||||
}
|
||||
}
|
||||
+124
-18
@@ -14,34 +14,28 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core_xapi;
|
||||
|
||||
use core_xapi\local\state;
|
||||
use core_xapi\local\statement;
|
||||
use core_xapi\xapi_exception;
|
||||
|
||||
/**
|
||||
* The core_xapi statement validation and tansformation.
|
||||
* Class handler handles basic xAPI statements and states.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 3.9
|
||||
* @copyright 2020 Ferran Recio
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_xapi;
|
||||
|
||||
use core_xapi\local\statement;
|
||||
use core_xapi\xapi_exception;
|
||||
use stdClass;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Class handler handles basic xapi statements.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @copyright 2020 Ferran Recio
|
||||
*/
|
||||
abstract class handler {
|
||||
|
||||
/** @var string component name in frankenstyle. */
|
||||
protected $component;
|
||||
|
||||
/** @var state_store the state_store instance. */
|
||||
protected $statestore;
|
||||
|
||||
/**
|
||||
* Constructor for a xAPI handler base class.
|
||||
*
|
||||
@@ -49,6 +43,7 @@ abstract class handler {
|
||||
*/
|
||||
final protected function __construct(string $component) {
|
||||
$this->component = $component;
|
||||
$this->statestore = $this->get_state_store();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,13 +54,24 @@ abstract class handler {
|
||||
* @throws xapi_exception
|
||||
*/
|
||||
final public static function create(string $component): self {
|
||||
$classname = "\\$component\\xapi\\handler";
|
||||
if (class_exists($classname)) {
|
||||
if (self::supports_xapi($component)) {
|
||||
$classname = "\\$component\\xapi\\handler";
|
||||
return new $classname($component);
|
||||
}
|
||||
throw new xapi_exception('Unknown handler');
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a component supports (and implements) xAPI.
|
||||
*
|
||||
* @param string $component the component name in frankenstyle.
|
||||
* @return bool true if the given component implements xAPI handler; false otherwise.
|
||||
*/
|
||||
final public static function supports_xapi(string $component): bool {
|
||||
$classname = "\\$component\\xapi\\handler";
|
||||
return class_exists($classname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a statement object into a Moodle xAPI Event.
|
||||
*
|
||||
@@ -115,4 +121,104 @@ abstract class handler {
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Note: this method must be overridden by the plugins which want to use xAPI states.
|
||||
*
|
||||
* @param state $state
|
||||
* @return bool if the state is valid or not
|
||||
*/
|
||||
abstract protected function validate_state(state $state): bool;
|
||||
|
||||
/**
|
||||
* Process a state save request.
|
||||
*
|
||||
* @param state $state the state object
|
||||
* @return bool if the state can be saved
|
||||
*/
|
||||
public function save_state(state $state): bool {
|
||||
if (!$this->validate_state($state)) {
|
||||
throw new xapi_exception('The state is not accepted, so it cannot be saved');
|
||||
}
|
||||
return $this->statestore->put($state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a state save request.
|
||||
*
|
||||
* @param state $state the state object
|
||||
* @return state|null the resulting loaded state
|
||||
*/
|
||||
public function load_state(state $state): ?state {
|
||||
if (!$this->validate_state($state)) {
|
||||
throw new xapi_exception('The state is not accepted, so it cannot be loaded');
|
||||
}
|
||||
$state = $this->statestore->get($state);
|
||||
return $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a state delete request.
|
||||
*
|
||||
* @param state $state the state object
|
||||
* @return bool if the deletion is successful
|
||||
*/
|
||||
public function delete_state(state $state): bool {
|
||||
if (!$this->validate_state($state)) {
|
||||
throw new xapi_exception('The state is not accepted, so it cannot be deleted');
|
||||
}
|
||||
return $this->statestore->delete($state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all states from this component.
|
||||
*
|
||||
* @param string|null $itemid
|
||||
* @param int|null $userid
|
||||
* @param string|null $stateid
|
||||
* @param string|null $registration
|
||||
*/
|
||||
public function wipe_states(
|
||||
?string $itemid = null,
|
||||
?int $userid = null,
|
||||
?string $stateid = null,
|
||||
?string $registration = null
|
||||
): void {
|
||||
$this->statestore->wipe($itemid, $userid, $stateid, $registration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all states from this component.
|
||||
*
|
||||
* @param string|null $itemid
|
||||
* @param int|null $userid
|
||||
* @param string|null $stateid
|
||||
* @param string|null $registration
|
||||
*/
|
||||
public function reset_states(
|
||||
?string $itemid = null,
|
||||
?int $userid = null,
|
||||
?string $stateid = null,
|
||||
?string $registration = null
|
||||
): void {
|
||||
$this->statestore->reset($itemid, $userid, $stateid, $registration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a valor state store for this component.
|
||||
*
|
||||
* Plugins may override this method is they want to use a different
|
||||
* state store class.
|
||||
* @return state_store the store to use to get/put/delete states.
|
||||
*/
|
||||
public function get_state_store(): state_store {
|
||||
return new state_store($this->component);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi\local\helper;
|
||||
|
||||
use core_component;
|
||||
use core_xapi\local\state;
|
||||
use core_xapi\local\statement\item_agent;
|
||||
use core_xapi\xapi_exception;
|
||||
use JsonException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* State trait helper, with common methods.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 4.2
|
||||
* @copyright 2023 Sara Arjona ([email protected])
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
trait state_trait {
|
||||
/**
|
||||
* Check component name.
|
||||
*
|
||||
* Note: this function is separated mainly for testing purposes to
|
||||
* be overridden to fake components.
|
||||
*
|
||||
* @throws xapi_exception if component is not available
|
||||
* @param string $component component name
|
||||
*/
|
||||
protected static function validate_component(string $component): void {
|
||||
// Check that $component is a real component name.
|
||||
$dir = core_component::get_component_directory($component);
|
||||
if (!$dir) {
|
||||
throw new xapi_exception("Component $component not available.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a JSON agent into a valid item_agent.
|
||||
*
|
||||
* @throws xapi_exception if JSON cannot be parsed
|
||||
* @param string $agentjson JSON encoded agent structure
|
||||
* @return item_agent the agent
|
||||
*/
|
||||
private static function get_agent_from_json(string $agentjson): item_agent {
|
||||
try {
|
||||
$agentdata = json_decode($agentjson, null, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
throw new xapi_exception('No agent detected');
|
||||
}
|
||||
return item_agent::create_from_data($agentdata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that $USER is actor in state.
|
||||
*
|
||||
* @param state $state The state
|
||||
* @return bool if $USER is actor of the state
|
||||
*/
|
||||
private static function check_state_user(state $state): bool {
|
||||
global $USER;
|
||||
$user = $state->get_user();
|
||||
if ($user->id != $USER->id) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the state data JSON into valid object.
|
||||
*
|
||||
* @throws xapi_exception if JSON cannot be parsed
|
||||
* @param string $statedatajson JSON encoded structure
|
||||
* @return stdClass the state data structure
|
||||
*/
|
||||
private static function get_statedata_from_json(string $statedatajson): stdClass {
|
||||
try {
|
||||
// Force it to be an object, because some statedata might be sent as array instead of JSON.
|
||||
$statedata = json_decode($statedatajson, false, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
throw new xapi_exception('Invalid state data format');
|
||||
}
|
||||
return $statedata;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi\local;
|
||||
|
||||
use core_xapi\local\statement\item_agent;
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use JsonSerializable;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* State resource object for xAPI structure checking and validation.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 4.2
|
||||
* @copyright 2023 Ferran Recio
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class state implements JsonSerializable {
|
||||
|
||||
/** @var item_agent The state agent (user). */
|
||||
protected $agent = null;
|
||||
|
||||
/** @var item_activity The state activity owner (the plugin instance). */
|
||||
protected $activity = null;
|
||||
|
||||
/** @var string The state identifier. */
|
||||
protected $stateid = null;
|
||||
|
||||
/** @var stdClass|null The state data. */
|
||||
protected $statedata = null;
|
||||
|
||||
/** @var string|null The state registration. */
|
||||
protected $registration = null;
|
||||
|
||||
/**
|
||||
* State constructor.
|
||||
*
|
||||
* @param item_agent $agent The state agent (user)
|
||||
* @param item_activity $activity The state activity owner
|
||||
* @param string $stateid The state identifier
|
||||
* @param stdClass|null $statedata The state data
|
||||
* @param string|null $registration The state registration
|
||||
*/
|
||||
public function __construct(
|
||||
item_agent $agent,
|
||||
item_activity $activity,
|
||||
string $stateid,
|
||||
?stdClass $statedata,
|
||||
?string $registration
|
||||
) {
|
||||
$this->agent = $agent;
|
||||
$this->activity = $activity;
|
||||
$this->stateid = $stateid;
|
||||
$this->statedata = $statedata;
|
||||
$this->registration = $registration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the data to serialize in case JSON state when needed.
|
||||
*
|
||||
* @return stdClass The state data structure
|
||||
*/
|
||||
public function jsonSerialize(): stdClass {
|
||||
if ($this->statedata) {
|
||||
return $this->statedata;
|
||||
}
|
||||
|
||||
return new stdClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the record data of this state.
|
||||
*
|
||||
* @return stdClass the record data structure
|
||||
*/
|
||||
public function get_record_data(): stdClass {
|
||||
$result = (object) [
|
||||
'userid' => $this->get_user()->id,
|
||||
'itemid' => $this->get_activity_id(),
|
||||
'stateid' => $this->stateid,
|
||||
'statedata' => json_encode($this),
|
||||
'registration' => $this->registration,
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a minified version of a given state.
|
||||
*
|
||||
* The returned structure is suitable to store in the "other" field
|
||||
* of logstore. xAPI standard specifies a list of attributes that can be calculated
|
||||
* instead of stored literally. This function get rid of these attributes.
|
||||
*
|
||||
* Note: it also converts stdClass to assoc array to make it compatible
|
||||
* with "other" field in the logstore
|
||||
*
|
||||
* @return array the minimal state needed to be stored a part from logstore data
|
||||
*/
|
||||
public function minify(): ?array {
|
||||
$result = [];
|
||||
$fields = ['activity', 'stateid', 'statedata', 'registration'];
|
||||
foreach ($fields as $field) {
|
||||
if (!empty($this->$field)) {
|
||||
$result[$field] = $this->$field;
|
||||
}
|
||||
}
|
||||
return json_decode(json_encode($result), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state data.
|
||||
*
|
||||
* @param stdClass|null $statedata the state data
|
||||
*/
|
||||
public function set_state_data(?stdClass $statedata): void {
|
||||
$this->statedata = $statedata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state data.
|
||||
* For getting the JSON representation of this state data, use jsonSerialize().
|
||||
*
|
||||
* @return stdClass|null The state data object.
|
||||
*/
|
||||
public function get_state_data(): ?stdClass {
|
||||
return $this->statedata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the moodle user represented by this state agent.
|
||||
*
|
||||
* @return stdClass user record
|
||||
*/
|
||||
public function get_user(): stdClass {
|
||||
return $this->agent->get_user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state activity ID.
|
||||
*
|
||||
* @return string activity ID
|
||||
*/
|
||||
public function get_activity_id(): string {
|
||||
return $this->activity->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the state agent.
|
||||
*
|
||||
* @return item_agent
|
||||
*/
|
||||
public function get_agent(): item_agent {
|
||||
return $this->agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the state object if it is defined.
|
||||
*
|
||||
* @return item_activity|null
|
||||
*/
|
||||
public function get_activity(): ?item_activity {
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state id.
|
||||
*
|
||||
* @return string state identifier
|
||||
*/
|
||||
public function get_state_id(): string {
|
||||
return $this->stateid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state registration if any.
|
||||
*
|
||||
* @return string|null state registration
|
||||
*/
|
||||
public function get_registration(): ?string {
|
||||
return $this->registration;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,33 +14,215 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Privacy Subsystem implementation for core xAPI Library.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @copyright 2020 Ferran Recio
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_xapi\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
|
||||
/**
|
||||
* Privacy Subsystem for core_xapi implementing null_provider.
|
||||
* Privacy implementation for core xAPI Library.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @category privacy
|
||||
* @copyright 2020 Ferran Recio
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\null_provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\subsystem\plugin_provider,
|
||||
\core_privacy\local\request\shared_userlist_provider {
|
||||
|
||||
/**
|
||||
* Get the language string identifier with the component's language
|
||||
* file to explain why this plugin stores no data.
|
||||
* Return the fields which contain personal data.
|
||||
*
|
||||
* @return string
|
||||
* @param collection $collection The initialised collection to add items to.
|
||||
* @return collection A listing of user data stored through this system.
|
||||
*/
|
||||
public static function get_reason(): string {
|
||||
return 'privacy:metadata';
|
||||
public static function get_metadata(collection $collection) : collection {
|
||||
$collection->add_database_table('xapi_states', [
|
||||
'component' => 'privacy:metadata:component',
|
||||
'userid' => 'privacy:metadata:userid',
|
||||
'itemid' => 'privacy:metadata:itemid',
|
||||
'stateid' => 'privacy:metadata:stateid',
|
||||
'statedata' => 'privacy:metadata:statedata',
|
||||
'registration' => 'privacy:metadata:registration',
|
||||
'timecreated' => 'privacy:metadata:timecreated',
|
||||
'timemodified' => 'privacy:metadata:timemodified',
|
||||
], 'privacy:metadata:xapi_states');
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a list of contexts which have xAPI for the user, in the respective area (component/itemtype combination).
|
||||
*
|
||||
* This method is to be called by consumers of the xAPI subsystem (plugins), in their get_contexts_for_userid() method,
|
||||
* to add the contexts for items which may have xAPI data, but would normally not be reported as having user data by the
|
||||
* plugin responsible for them.
|
||||
*
|
||||
* @param \core_privacy\local\request\contextlist $contextlist
|
||||
* @param int $userid The id of the user in scope.
|
||||
* @param string $component the frankenstyle component name.
|
||||
*/
|
||||
public static function add_contexts_for_userid(
|
||||
\core_privacy\local\request\contextlist $contextlist,
|
||||
int $userid,
|
||||
string $component) {
|
||||
$sql = "SELECT ctx.id
|
||||
FROM {xapi_states} xs
|
||||
JOIN {context} ctx
|
||||
ON ctx.id = xs.itemid
|
||||
WHERE xs.userid = :userid
|
||||
AND xs.component = :component";
|
||||
|
||||
$params = ['userid' => $userid, 'component' => $component];
|
||||
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add users to a userlist who have xAPI within the specified context.
|
||||
*
|
||||
* @param \core_privacy\local\request\userlist $userlist The userlist to add the users to.
|
||||
* @return void
|
||||
*/
|
||||
public static function add_userids_for_context(\core_privacy\local\request\userlist $userlist) {
|
||||
if (empty($userlist)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextid' => $userlist->get_context()->id,
|
||||
'component' => $userlist->get_component()
|
||||
];
|
||||
|
||||
$sql = "SELECT xs.userid
|
||||
FROM {xapi_states} xs
|
||||
JOIN {context} ctx
|
||||
ON ctx.id = xs.itemid
|
||||
WHERE ctx.id = :contextid
|
||||
AND xs.component = :component";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get xAPI states data for the specified user in the specified component and item ID.
|
||||
*
|
||||
* @param int $userid The id of the user in scope.
|
||||
* @param string $component The component name.
|
||||
* @param int $itemid The item ID.
|
||||
* @return array|null
|
||||
*/
|
||||
public static function get_xapi_states_for_user(int $userid, string $component, int $itemid) {
|
||||
global $DB;
|
||||
|
||||
$params = [
|
||||
'userid' => $userid,
|
||||
'component' => $component,
|
||||
'itemid' => $itemid,
|
||||
];
|
||||
|
||||
if (!$states = $DB->get_records('xapi_states', $params)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($states as $state) {
|
||||
$result[] = [
|
||||
'statedata' => $state->statedata,
|
||||
'timecreated' => transform::datetime($state->timecreated),
|
||||
'timemodified' => transform::datetime($state->timemodified)
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all xAPI states for all users in the specified contexts, and component area.
|
||||
*
|
||||
* @param \context $context The context to which deletion is scoped.
|
||||
* @param string $component The component name.
|
||||
* @throws \dml_exception if any errors are encountered during deletion.
|
||||
*/
|
||||
public static function delete_states_for_all_users(\context $context, string $component) {
|
||||
global $DB;
|
||||
|
||||
$params = [
|
||||
'component' => $component,
|
||||
];
|
||||
|
||||
$select = "component = :component";
|
||||
|
||||
if (!empty($context)) {
|
||||
$select .= " AND itemid = :itemid";
|
||||
$params['itemid'] = $context->id;
|
||||
}
|
||||
$DB->delete_records_select('xapi_states', $select, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all xAPI states for the specified users in the specified context, component area and item type.
|
||||
*
|
||||
* @param \core_privacy\local\request\approved_userlist $userlist The approved contexts and user information
|
||||
* to delete information for.
|
||||
* @param int $itemid Optional itemid associated with component.
|
||||
* @throws \dml_exception if any errors are encountered during deletion.
|
||||
*/
|
||||
public static function delete_states_for_userlist(\core_privacy\local\request\approved_userlist $userlist, int $itemid = 0) {
|
||||
global $DB;
|
||||
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
if (empty($userids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
|
||||
$params = [
|
||||
'component' => $userlist->get_component(),
|
||||
];
|
||||
|
||||
$params += $userparams;
|
||||
$select = "component = :component AND userid $usersql";
|
||||
|
||||
if (!empty($itemid)) {
|
||||
$select .= " AND itemid = :itemid";
|
||||
$params['itemid'] = $itemid;
|
||||
}
|
||||
|
||||
$DB->delete_records_select('xapi_states', $select, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all xAPI states for the specified user, in the specified contexts.
|
||||
*
|
||||
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
|
||||
* @param string $component The component name.
|
||||
* @param int $itemid Optional itemid associated with component.
|
||||
* @throws \coding_exception
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public static function delete_states_for_user(approved_contextlist $contextlist, string $component, int $itemid = 0) {
|
||||
global $DB;
|
||||
|
||||
$userid = $contextlist->get_user()->id;
|
||||
|
||||
$params = [
|
||||
'userid' => $userid,
|
||||
'component' => $component,
|
||||
];
|
||||
|
||||
$select = "userid = :userid AND component = :component";
|
||||
|
||||
if (!empty($itemid)) {
|
||||
$select .= " AND itemid = :itemid";
|
||||
$params['itemid'] = $itemid;
|
||||
}
|
||||
|
||||
$DB->delete_records_select('xapi_states', $select, $params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi;
|
||||
|
||||
use core_xapi\local\state;
|
||||
|
||||
/**
|
||||
* The state store manager.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 4.2
|
||||
* @copyright 2022 Ferran Recio <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class state_store {
|
||||
|
||||
/** @var string component name in frankenstyle. */
|
||||
protected $component;
|
||||
|
||||
/**
|
||||
* Constructor for a xAPI handler base class.
|
||||
*
|
||||
* @param string $component the component name
|
||||
*/
|
||||
public function __construct(string $component) {
|
||||
$this->component = $component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete any extra state data stored in the database.
|
||||
*
|
||||
* This method will be called only if the state is accepted by validate_state.
|
||||
*
|
||||
* Plugins may override this method add extra clean up tasks to the deletion.
|
||||
*
|
||||
* @param state $state
|
||||
* @return bool if the state is removed
|
||||
*/
|
||||
public function delete(state $state): bool {
|
||||
global $DB;
|
||||
$data = [
|
||||
'component' => $this->component,
|
||||
'userid' => $state->get_user()->id,
|
||||
'itemid' => $state->get_activity_id(),
|
||||
'stateid' => $state->get_state_id(),
|
||||
'registration' => $state->get_registration(),
|
||||
];
|
||||
return $DB->delete_records('xapi_states', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a state object from the database.
|
||||
*
|
||||
* This method will be called only if the state is accepted by validate_state.
|
||||
*
|
||||
* Plugins may override this method if they store some data in different tables.
|
||||
*
|
||||
* @param state $state
|
||||
* @return state|null the state
|
||||
*/
|
||||
public function get(state $state): ?state {
|
||||
global $DB;
|
||||
$data = [
|
||||
'component' => $this->component,
|
||||
'userid' => $state->get_user()->id,
|
||||
'itemid' => $state->get_activity_id(),
|
||||
'stateid' => $state->get_state_id(),
|
||||
'registration' => $state->get_registration(),
|
||||
];
|
||||
$record = $DB->get_record('xapi_states', $data);
|
||||
if ($record) {
|
||||
$statedata = null;
|
||||
if ($record->statedata !== null) {
|
||||
$statedata = json_decode($record->statedata, null, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
$state->set_state_data($statedata);
|
||||
return $state;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an state object into the database.
|
||||
*
|
||||
* This method will be called only if the state is accepted by validate_state.
|
||||
*
|
||||
* Plugins may override this method if they store some data in different tables.
|
||||
*
|
||||
* @param state $state
|
||||
* @return bool if the state is inserted/updated
|
||||
*/
|
||||
public function put(state $state): bool {
|
||||
global $DB;
|
||||
$data = [
|
||||
'component' => $this->component,
|
||||
'userid' => $state->get_user()->id,
|
||||
'itemid' => $state->get_activity_id(),
|
||||
'stateid' => $state->get_state_id(),
|
||||
'registration' => $state->get_registration(),
|
||||
];
|
||||
$record = $DB->get_record('xapi_states', $data) ?: (object) $data;
|
||||
if (isset($record->id)) {
|
||||
$record->statedata = json_encode($state->jsonSerialize());
|
||||
$record->timemodified = time();
|
||||
$result = $DB->update_record('xapi_states', $record);
|
||||
} else {
|
||||
$data['statedata'] = json_encode($state->jsonSerialize());
|
||||
$data['timecreated'] = time();
|
||||
$data['timemodified'] = $data['timecreated'];
|
||||
$result = $DB->insert_record('xapi_states', $data);
|
||||
}
|
||||
return $result ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all states from the component.
|
||||
* The given parameters are filters to decide the states to reset. If no parameters are defined, the only filter applied
|
||||
* will be the component.
|
||||
*
|
||||
* Plugins may override this method if they store some data in different tables.
|
||||
*
|
||||
* @param string|null $itemid
|
||||
* @param int|null $userid
|
||||
* @param string|null $stateid
|
||||
* @param string|null $registration
|
||||
*/
|
||||
public function reset(
|
||||
?string $itemid = null,
|
||||
?int $userid = null,
|
||||
?string $stateid = null,
|
||||
?string $registration = null
|
||||
): void {
|
||||
global $DB;
|
||||
|
||||
$data = [
|
||||
'component' => $this->component,
|
||||
];
|
||||
if ($itemid) {
|
||||
$data['itemid'] = $itemid;
|
||||
}
|
||||
if ($userid) {
|
||||
$data['userid'] = $userid;
|
||||
}
|
||||
if ($stateid) {
|
||||
$data['stateid'] = $stateid;
|
||||
}
|
||||
if ($registration) {
|
||||
$data['registration'] = $registration;
|
||||
}
|
||||
$DB->set_field('xapi_states', 'statedata', null, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all states from the component
|
||||
* The given parameters are filters to decide the states to wipe. If no parameters are defined, the only filter applied
|
||||
* will be the component.
|
||||
*
|
||||
* Plugins may override this method if they store some data in different tables.
|
||||
*
|
||||
* @param string|null $itemid
|
||||
* @param int|null $userid
|
||||
* @param string|null $stateid
|
||||
* @param string|null $registration
|
||||
*/
|
||||
public function wipe(
|
||||
?string $itemid = null,
|
||||
?int $userid = null,
|
||||
?string $stateid = null,
|
||||
?string $registration = null
|
||||
): void {
|
||||
global $DB;
|
||||
$data = [
|
||||
'component' => $this->component,
|
||||
];
|
||||
if ($itemid) {
|
||||
$data['itemid'] = $itemid;
|
||||
}
|
||||
if ($userid) {
|
||||
$data['userid'] = $userid;
|
||||
}
|
||||
if ($stateid) {
|
||||
$data['stateid'] = $stateid;
|
||||
}
|
||||
if ($registration) {
|
||||
$data['registration'] = $registration;
|
||||
}
|
||||
$DB->delete_records('xapi_states', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a state store clean up.
|
||||
*
|
||||
* Plugins can override this methos to provide an alternative clean up logic.
|
||||
*/
|
||||
public function cleanup(): void {
|
||||
global $DB;
|
||||
$xapicleanupperiod = get_config('core', 'xapicleanupperiod');
|
||||
if (empty($xapicleanupperiod)) {
|
||||
return;
|
||||
}
|
||||
$todelete = time() - $xapicleanupperiod;
|
||||
$DB->delete_records_select(
|
||||
'xapi_states',
|
||||
'component = :component AND timemodified < :todelete',
|
||||
['component' => $this->component, 'todelete' => $todelete]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi\task;
|
||||
|
||||
/**
|
||||
* A scheduled task to clear up old xAPI state data.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 4.2
|
||||
* @copyright 2022 Ferran Recio <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class state_cleanup_task extends \core\task\scheduled_task {
|
||||
|
||||
/**
|
||||
* Get a descriptive name for this task (shown to admins).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name() {
|
||||
return get_string('xapicleanup', 'xapi');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run task.
|
||||
*/
|
||||
public function execute() {
|
||||
\core_xapi\api::execute_state_cleanup();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi;
|
||||
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use advanced_testcase;
|
||||
|
||||
/**
|
||||
* Contains test cases for testing xAPI API base methods.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 4.2
|
||||
* @covers \core_xapi\api
|
||||
* @copyright 2023 Sara Arjona ([email protected])
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class api_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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing remove_states_from_component method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_remove_states_from_component(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
|
||||
// Add a few xAPI state records to database.
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('3')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('4')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'mod_h5pactivity'], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('6'), 'component' => 'mod_h5pactivity'], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('7'), 'component' => 'mod_h5pactivity'], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('8'), 'component' => 'unexisting'], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('9'), 'component' => 'unexisting'], true);
|
||||
|
||||
// Check no state has been removed (because there are no entries for the another_component).
|
||||
api::remove_states_from_component('another_component');
|
||||
$this->assertEquals(9, $DB->count_records('xapi_states'));
|
||||
|
||||
// Check states for the fake_component have been removed.
|
||||
api::remove_states_from_component('fake_component');
|
||||
$this->assertEquals(5, $DB->count_records('xapi_states'));
|
||||
$this->assertEquals(0, $DB->count_records('xapi_states', ['component' => 'fake_component']));
|
||||
$this->assertEquals(3, $DB->count_records('xapi_states', ['component' => 'mod_h5pactivity']));
|
||||
$this->assertEquals(2, $DB->count_records('xapi_states', ['component' => 'unexisting']));
|
||||
|
||||
// Check states for the mod_h5pactivity have been removed too.
|
||||
api::remove_states_from_component('mod_h5pactivity');
|
||||
$this->assertEquals(2, $DB->count_records('xapi_states'));
|
||||
$this->assertEquals(0, $DB->count_records('xapi_states', ['component' => 'mod_h5pactivity']));
|
||||
|
||||
// Check states for the unexisting component have been removed (using the default state_store).
|
||||
api::remove_states_from_component('unexisting');
|
||||
$this->assertEquals(0, $DB->count_records('xapi_states'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing execute_state_cleanup method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_execute_state_cleanup(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
|
||||
// Add a few xAPI state records to database.
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('3')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('4')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'mod_h5pactivity'], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('6'), 'component' => 'mod_h5pactivity'], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('7'), 'component' => 'mod_h5pactivity'], true);
|
||||
|
||||
// Perform test.
|
||||
api::execute_state_cleanup();
|
||||
|
||||
// Check no state has been removed yet (because the entries are not old enough).
|
||||
$this->assertEquals(7, $DB->count_records('xapi_states'));
|
||||
|
||||
// Make the existing state entries older.
|
||||
$timepast = time() - 2;
|
||||
$DB->set_field('xapi_states', 'timecreated', $timepast);
|
||||
$DB->set_field('xapi_states', 'timemodified', $timepast);
|
||||
|
||||
// Create 1 more state, that shouldn't be removed after the cleanup.
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('8'), 'component' => 'mod_h5pactivity'], true);
|
||||
|
||||
// Set the config to remove states older than 1 second.
|
||||
set_config('xapicleanupperiod', 1);
|
||||
|
||||
// Check old states have been removed.
|
||||
api::execute_state_cleanup();
|
||||
$this->assertEquals(5, $DB->count_records('xapi_states'));
|
||||
$this->assertEquals(4, $DB->count_records('xapi_states', ['component' => 'fake_component']));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['component' => 'mod_h5pactivity']));
|
||||
$this->assertEquals(0, $DB->count_records('xapi_states', ['component' => 'my_component']));
|
||||
}
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi\external;
|
||||
|
||||
use core_xapi\xapi_exception;
|
||||
use core_xapi\local\statement\item_agent;
|
||||
use externallib_advanced_testcase;
|
||||
use core_external\external_api;
|
||||
use core_xapi\iri;
|
||||
use core_xapi\local\state;
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use core_xapi\test_helper;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* Unit tests for xAPI delete state webservice.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @covers \core_xapi\external\post_state
|
||||
* @since Moodle 4.2
|
||||
* @copyright 2023 Sara Arjona ([email protected])
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class delete_state_test extends externallib_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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing different component names on valid states.
|
||||
*
|
||||
* @dataProvider components_provider
|
||||
* @param string $component component name
|
||||
* @param string|null $expected expected results
|
||||
*/
|
||||
public function test_component_names(string $component, ?string $expected): void {
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
|
||||
// Perform test.
|
||||
$data = test_helper::create_state([], true);
|
||||
$this->delete_state_data($component, $data, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the test_component_names tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function components_provider() : array {
|
||||
return [
|
||||
'Inexistent component' => [
|
||||
'component' => 'inexistent_component',
|
||||
'expected' => null,
|
||||
],
|
||||
'Compatible component' => [
|
||||
'component' => 'fake_component',
|
||||
'expected' => 'true',
|
||||
],
|
||||
'Incompatible component' => [
|
||||
'component' => 'core_xapi',
|
||||
'expected' => null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing invalid agent.
|
||||
*
|
||||
*/
|
||||
public function test_invalid_agent(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$other = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Invalid agent (use different user, instead of the current one).
|
||||
$info = [
|
||||
'agent' => item_agent::create_from_user($other),
|
||||
];
|
||||
$data = test_helper::create_state($info, true);
|
||||
$this->delete_state_data('fake_component', $data, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing valid/invalid state.
|
||||
*
|
||||
* @dataProvider states_provider
|
||||
* @param array $info array of overriden state data.
|
||||
* @param string|null $expected Expected results.
|
||||
* @return void
|
||||
*/
|
||||
public function test_delete_state(array $info, ?string $expected): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$component = $info['component'] ?? 'fake_component';
|
||||
$params = [];
|
||||
if ($component === 'mod_h5pactivity') {
|
||||
// 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');
|
||||
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
|
||||
|
||||
$activitycontext = \context_module::instance($activity->cmid);
|
||||
$info['activity'] = item_activity::create_from_id($activitycontext->id);
|
||||
$params['activity'] = $info['activity'];
|
||||
$this->setUser($user);
|
||||
}
|
||||
|
||||
// Add, at least, one xAPI state record to database (with the default values).
|
||||
test_helper::create_state($params, true);
|
||||
|
||||
// Perform test.
|
||||
$data = test_helper::create_state($info);
|
||||
|
||||
$this->delete_state_data($component, $data, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the test_get_state tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function states_provider() : array {
|
||||
return [
|
||||
'Existing and valid state' => [
|
||||
'info' => [],
|
||||
'expected' => 'true',
|
||||
],
|
||||
'No state (wrong activityid)' => [
|
||||
'info' => ['activity' => item_activity::create_from_id('1')],
|
||||
'expected' => 'false',
|
||||
],
|
||||
'No state (wrong stateid)' => [
|
||||
'info' => ['stateid' => 'food'],
|
||||
'expected' => 'false',
|
||||
],
|
||||
'No state (wrong component)' => [
|
||||
'info' => ['component' => 'mod_h5pactivity'],
|
||||
'expected' => 'false',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a xAPI external webservice class to operate.
|
||||
*
|
||||
* The test needs to fake a component in order to test without
|
||||
* using a real one. This way if in the future any component
|
||||
* implement it's xAPI handler this test will continue working.
|
||||
*
|
||||
* @return delete_state the external class
|
||||
*/
|
||||
private function get_external_class(): delete_state {
|
||||
$ws = new class extends delete_state {
|
||||
/**
|
||||
* Method to override validate_component.
|
||||
*
|
||||
* @param string $component The component name in frankenstyle.
|
||||
*/
|
||||
protected static function validate_component(string $component): void {
|
||||
if ($component != 'fake_component') {
|
||||
parent::validate_component($component);
|
||||
}
|
||||
}
|
||||
};
|
||||
return $ws;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function do all checks from a standard delete_state request.
|
||||
*
|
||||
* The reason for this function is because states crafting (special in error
|
||||
* scenarios) is complicated to do via data providers because every test need a specific
|
||||
* testing conditions. For this reason alls tests creates a scenario and then uses this
|
||||
* function to check the results.
|
||||
*
|
||||
* @param string $component component name
|
||||
* @param state $data data to encode and send to post_state
|
||||
* @param string $expected expected results (if null an exception is expected)
|
||||
*/
|
||||
private function delete_state_data(string $component, state $data, ?string $expected): void {
|
||||
global $DB;
|
||||
|
||||
// Get current states in database.
|
||||
$currentstates = $DB->count_records('xapi_states');
|
||||
|
||||
// When no result is expected, an exception will be incurred.
|
||||
if (is_null($expected)) {
|
||||
$this->expectException(xapi_exception::class);
|
||||
}
|
||||
|
||||
$external = $this->get_external_class();
|
||||
$result = $external::execute(
|
||||
$component,
|
||||
iri::generate($data->get_activity_id(), 'activity'),
|
||||
json_encode($data->get_agent()),
|
||||
$data->get_state_id(),
|
||||
$data->get_registration()
|
||||
);
|
||||
$result = external_api::clean_returnvalue($external::execute_returns(), $result);
|
||||
|
||||
// Check the state has been removed.
|
||||
$records = $DB->get_records('xapi_states');
|
||||
$this->assertTrue($result);
|
||||
if ($expected === 'true') {
|
||||
$this->assertCount($currentstates - 1, $records);
|
||||
} else if ($expected === 'false') {
|
||||
$this->assertCount($currentstates, $records);
|
||||
}
|
||||
}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi\external;
|
||||
|
||||
use core_xapi\xapi_exception;
|
||||
use core_xapi\local\statement\item_agent;
|
||||
use externallib_advanced_testcase;
|
||||
use core_external\external_api;
|
||||
use core_xapi\iri;
|
||||
use core_xapi\local\state;
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use core_xapi\test_helper;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* Unit tests for xAPI get state webservice.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @covers \core_xapi\external\get_state
|
||||
* @since Moodle 4.2
|
||||
* @copyright 2023 Sara Arjona ([email protected])
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class get_state_test extends externallib_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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing different component names on valid states.
|
||||
*
|
||||
* @dataProvider components_provider
|
||||
* @param string $component component name
|
||||
* @param string|null $expected expected results
|
||||
*/
|
||||
public function test_component_names(string $component, ?string $expected): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
|
||||
// Add, at least, one xAPI state record to database.
|
||||
$data = test_helper::create_state([], true);
|
||||
|
||||
// Perform test.
|
||||
$this->get_state_data($component, $data, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the test_component_names tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function components_provider() : array {
|
||||
return [
|
||||
'Inexistent component' => [
|
||||
'component' => 'inexistent_component',
|
||||
'expected' => null,
|
||||
],
|
||||
'Compatible component' => [
|
||||
'component' => 'fake_component',
|
||||
'expected' => 'true',
|
||||
],
|
||||
'Incompatible component' => [
|
||||
'component' => 'core_xapi',
|
||||
'expected' => null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing invalid agent for get_state.
|
||||
*
|
||||
*/
|
||||
public function test_invalid_agent(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$other = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Invalid agent (use different user, instead of the current one).
|
||||
$info = [
|
||||
'agent' => item_agent::create_from_user($other),
|
||||
];
|
||||
$data = test_helper::create_state($info, true);
|
||||
$this->get_state_data('fake_component', $data, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing valid/invalid state.
|
||||
*
|
||||
* @dataProvider states_provider
|
||||
* @param array $info The xAPI state information (to override default values).
|
||||
* @param string $expected Expected results.
|
||||
*/
|
||||
public function test_get_state(array $info, string $expected): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$component = $info['component'] ?? 'fake_component';
|
||||
$params = [];
|
||||
if ($component === 'mod_h5pactivity') {
|
||||
// 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');
|
||||
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
|
||||
|
||||
$activitycontext = \context_module::instance($activity->cmid);
|
||||
$info['activity'] = item_activity::create_from_id($activitycontext->id);
|
||||
$params['activity'] = $info['activity'];
|
||||
$this->setUser($user);
|
||||
}
|
||||
|
||||
// Add, at least, one xAPI state record to database (with the default values).
|
||||
test_helper::create_state($params, true);
|
||||
|
||||
// Perform test.
|
||||
$data = test_helper::create_state($info);
|
||||
$component = $info['component'] ?? 'fake_component';
|
||||
$this->get_state_data($component, $data, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the test_get_state tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function states_provider() : array {
|
||||
return [
|
||||
'Existing and valid state' => [
|
||||
'info' => [],
|
||||
'expected' => 'true',
|
||||
],
|
||||
'No state (wrong activityid)' => [
|
||||
'info' => ['activity' => item_activity::create_from_id('1')],
|
||||
'expected' => 'false',
|
||||
],
|
||||
'No state (wrong stateid)' => [
|
||||
'info' => ['stateid' => 'food'],
|
||||
'expected' => 'false',
|
||||
],
|
||||
'No state (wrong component)' => [
|
||||
'info' => ['component' => 'mod_h5pactivity'],
|
||||
'expected' => 'false',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a xAPI external webservice class to operate.
|
||||
*
|
||||
* The test needs to fake a component in order to test without
|
||||
* using a real one. This way if in the future any component
|
||||
* implement it's xAPI handler this test will continue working.
|
||||
*
|
||||
* @return get_state the external class
|
||||
*/
|
||||
private function get_external_class(): get_state {
|
||||
$ws = new class extends get_state {
|
||||
/**
|
||||
* Method to override validate_component.
|
||||
*
|
||||
* @param string $component The component name in frankenstyle.
|
||||
*/
|
||||
protected static function validate_component(string $component): void {
|
||||
if ($component != 'fake_component') {
|
||||
parent::validate_component($component);
|
||||
}
|
||||
}
|
||||
};
|
||||
return $ws;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function do all checks from a standard get_state request.
|
||||
*
|
||||
* The reason for this function is because states crafting (special in error
|
||||
* scenarios) is complicated to do via data providers because every test need a specific
|
||||
* testing conditions. For this reason alls tests creates a scenario and then uses this
|
||||
* function to check the results.
|
||||
*
|
||||
* @param string $component component name
|
||||
* @param state $data data to encode and send to get_state
|
||||
* @param string $expected expected results (if null an exception is expected)
|
||||
*/
|
||||
private function get_state_data(string $component, state $data, ?string $expected): void {
|
||||
// When no result is expected, an exception will be incurred.
|
||||
if (is_null($expected)) {
|
||||
$this->expectException(xapi_exception::class);
|
||||
}
|
||||
|
||||
$external = $this->get_external_class();
|
||||
$result = $external::execute(
|
||||
$component,
|
||||
iri::generate($data->get_activity_id(), 'activity'),
|
||||
json_encode($data->get_agent()),
|
||||
$data->get_state_id(),
|
||||
$data->get_registration()
|
||||
);
|
||||
$result = external_api::clean_returnvalue($external::execute_returns(), $result);
|
||||
|
||||
// Check the returned state has the expected values.
|
||||
if ($expected === 'true') {
|
||||
$this->assertEquals(json_encode($data->jsonSerialize()), $result);
|
||||
} else {
|
||||
$this->assertNull($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi\external;
|
||||
|
||||
use core_xapi\xapi_exception;
|
||||
use core_xapi\local\statement\item_agent;
|
||||
use externallib_advanced_testcase;
|
||||
use core_external\external_api;
|
||||
use core_xapi\iri;
|
||||
use core_xapi\local\state;
|
||||
use core_xapi\test_helper;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* Unit tests for xAPI post state webservice.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @covers \core_xapi\external\post_state
|
||||
* @since Moodle 4.2
|
||||
* @copyright 2023 Sara Arjona ([email protected])
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class post_state_test extends externallib_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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing different component names on valid states.
|
||||
*
|
||||
* @dataProvider components_provider
|
||||
* @param string $component component name
|
||||
* @param string|null $expected expected results
|
||||
*/
|
||||
public function test_component_names(string $component, ?string $expected): void {
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
|
||||
// Perform test.
|
||||
$data = test_helper::create_state();
|
||||
$this->post_state_data($component, $data, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the test_component_names tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function components_provider() : array {
|
||||
return [
|
||||
'Inexistent component' => [
|
||||
'component' => 'inexistent_component',
|
||||
'expected' => null,
|
||||
],
|
||||
'Compatible component' => [
|
||||
'component' => 'fake_component',
|
||||
'expected' => 'true',
|
||||
],
|
||||
'Incompatible component' => [
|
||||
'component' => 'core_xapi',
|
||||
'expected' => null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing invalid agent.
|
||||
*
|
||||
*/
|
||||
public function test_invalid_agent(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$other = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Invalid agent (use different user, instead of the current one).
|
||||
$info = [
|
||||
'agent' => item_agent::create_from_user($other),
|
||||
];
|
||||
$data = test_helper::create_state($info);
|
||||
$this->post_state_data('fake_component', $data, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing valid/invalid state.
|
||||
*
|
||||
* @dataProvider states_provider
|
||||
* @param string $stateid The xAPI state id.
|
||||
* @param string|null $expected Expected results.
|
||||
* @return void
|
||||
*/
|
||||
public function test_post_state(string $stateid, ?string $expected): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
|
||||
// Perform test.
|
||||
$info = [
|
||||
'stateid' => $stateid,
|
||||
];
|
||||
$data = test_helper::create_state($info);
|
||||
$this->post_state_data('fake_component', $data, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the test_post_state tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function states_provider() : array {
|
||||
return [
|
||||
'Empty stateid' => [
|
||||
'stateid' => '',
|
||||
'expected' => 'true',
|
||||
],
|
||||
'Valid stateid (any value but paella)' => [
|
||||
'stateid' => 'sangria',
|
||||
'expected' => 'true',
|
||||
],
|
||||
'Invalid stateid' => [
|
||||
'stateid' => 'paella',
|
||||
'expected' => null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a xAPI external webservice class to operate.
|
||||
*
|
||||
* The test needs to fake a component in order to test without
|
||||
* using a real one. This way if in the future any component
|
||||
* implement it's xAPI handler this test will continue working.
|
||||
*
|
||||
* @return post_state the external class
|
||||
*/
|
||||
private function get_external_class(): post_state {
|
||||
$ws = new class extends post_state {
|
||||
/**
|
||||
* Method to override validate_component.
|
||||
*
|
||||
* @param string $component The component name in frankenstyle.
|
||||
*/
|
||||
protected static function validate_component(string $component): void {
|
||||
if ($component != 'fake_component') {
|
||||
parent::validate_component($component);
|
||||
}
|
||||
}
|
||||
};
|
||||
return $ws;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function do all checks from a standard post_state request.
|
||||
*
|
||||
* The reason for this function is because states crafting (special in error
|
||||
* scenarios) is complicated to do via data providers because every test need a specific
|
||||
* testing conditions. For this reason alls tests creates a scenario and then uses this
|
||||
* function to check the results.
|
||||
*
|
||||
* @param string $component component name
|
||||
* @param state $data data to encode and send to post_state
|
||||
* @param string $expected expected results (if null an exception is expected)
|
||||
*/
|
||||
private function post_state_data(string $component, state $data, ?string $expected): void {
|
||||
global $DB;
|
||||
|
||||
// Get current states in database.
|
||||
$currentstates = $DB->count_records('xapi_states');
|
||||
|
||||
// When no result is expected, an exception will be incurred.
|
||||
if (is_null($expected)) {
|
||||
$this->expectException(xapi_exception::class);
|
||||
}
|
||||
|
||||
$external = $this->get_external_class();
|
||||
$result = $external::execute(
|
||||
$component,
|
||||
iri::generate($data->get_activity_id(), 'activity'),
|
||||
json_encode($data->get_agent()),
|
||||
$data->get_state_id(),
|
||||
json_encode($data->jsonSerialize()),
|
||||
$data->get_registration()
|
||||
);
|
||||
$result = external_api::clean_returnvalue($external::execute_returns(), $result);
|
||||
|
||||
// Check the state has been saved with the expected values.
|
||||
$this->assertTrue($result);
|
||||
$records = $DB->get_records('xapi_states');
|
||||
$this->assertCount($currentstates + 1, $records);
|
||||
$record = reset($records);
|
||||
$this->assertEquals($component, $record->component);
|
||||
$this->assertEquals($data->get_activity_id(), $record->itemid);
|
||||
$this->assertEquals($data->get_user()->id, $record->userid);
|
||||
$this->assertEquals(json_encode($data->jsonSerialize()), $record->statedata);
|
||||
$this->assertEquals($data->get_registration(), $record->registration);
|
||||
}
|
||||
}
|
||||
Vendored
+21
@@ -30,6 +30,7 @@ use core_xapi\handler as handler_base;
|
||||
use core_xapi\event\xapi_test_statement_post;
|
||||
use context_system;
|
||||
use core\event\base;
|
||||
use core_xapi\local\state;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -112,4 +113,24 @@ class handler extends handler_base {
|
||||
}
|
||||
return parent::supports_group_actors();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// For testing purposes, the state will be considered NOT valid when stateid is set to 'paella'.
|
||||
if ($state->get_state_id() === 'paella') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+234
-13
@@ -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/>.
|
||||
|
||||
/**
|
||||
* This file contains unit test related to xAPI library.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @copyright 2020 Ferran Recio
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_xapi;
|
||||
|
||||
use core_xapi\xapi_exception;
|
||||
@@ -30,15 +22,15 @@ use core_xapi\local\statement\item_agent;
|
||||
use core_xapi\local\statement\item_verb;
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use advanced_testcase;
|
||||
use core\event\base;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
use core_xapi\local\state;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Contains test cases for testing xAPI statement handler base methods.
|
||||
* Contains test cases for testing xAPI handler base methods.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 3.9
|
||||
* @covers \core_xapi\handler
|
||||
* @copyright 2020 Ferran Recio
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
@@ -47,7 +39,7 @@ class handler_test extends advanced_testcase {
|
||||
/**
|
||||
* Setup to ensure that fixtures are loaded.
|
||||
*/
|
||||
public static function setupBeforeClass(): void {
|
||||
public static function setUpBeforeClass(): void {
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
|
||||
}
|
||||
@@ -65,6 +57,19 @@ class handler_test extends advanced_testcase {
|
||||
$value = handler::create('potato_omelette');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test xAPI support.
|
||||
*/
|
||||
public function test_supports_xapi() {
|
||||
// Get an existent handler.
|
||||
$result = handler::supports_xapi('fake_component');
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Get a non existent handler.
|
||||
$result = handler::supports_xapi('potato_omelette');
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test support group.
|
||||
*/
|
||||
@@ -115,6 +120,7 @@ class handler_test extends advanced_testcase {
|
||||
$this->assertEquals(false, $result[1]);
|
||||
|
||||
// Check log entries.
|
||||
/** @var \core_xapi\event\xapi_test_statement_post $log */
|
||||
$log = $testhelper->get_last_log_entry();
|
||||
$this->assertNotEmpty($log);
|
||||
|
||||
@@ -126,4 +132,219 @@ class handler_test extends advanced_testcase {
|
||||
$this->assertEquals($value, 'User \''.$user->id.'\' send a statement to component \'core_xapi\'');
|
||||
$this->assertTrue($log->compare_statement($statement));
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing save_state method.
|
||||
*/
|
||||
public function test_save_state(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$component = 'fake_component';
|
||||
$handler = handler::create($component);
|
||||
|
||||
// Check the state has been added.
|
||||
$state = test_helper::create_state();
|
||||
$this->assertEquals(0, $DB->count_records('xapi_states'));
|
||||
$result = $handler->save_state($state);
|
||||
$this->assertTrue($result);
|
||||
$records = $DB->get_records('xapi_states');
|
||||
$this->assertCount(1, $records);
|
||||
$record = reset($records);
|
||||
$this->check_state($component, $state, $record);
|
||||
|
||||
// Check the state has been updated.
|
||||
$statedata = '{"progress":0,"answers":[[["BB"],[""]],[{"answers":[]}]],"answered":[true,false]}';
|
||||
$state->set_state_data(json_decode($statedata));
|
||||
$result = $handler->save_state($state);
|
||||
$this->assertTrue($result);
|
||||
$records = $DB->get_records('xapi_states');
|
||||
$this->assertCount(1, $records);
|
||||
$record = reset($records);
|
||||
$this->check_state($component, $state, $record);
|
||||
|
||||
// Check an exception is thrown when the state is not valid.
|
||||
$this->expectException(xapi_exception::class);
|
||||
$state = test_helper::create_state(['stateid' => 'paella']);
|
||||
$result = $handler->save_state($state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing load_state method.
|
||||
*/
|
||||
public function test_load_state(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$component = 'fake_component';
|
||||
$handler = handler::create($component);
|
||||
|
||||
// Check the state is not found (when there are no states).
|
||||
$state = test_helper::create_state();
|
||||
$state->set_state_data(null);
|
||||
$this->assertEquals(0, $DB->count_records('xapi_states'));
|
||||
$result = $handler->load_state($state);
|
||||
$this->assertEquals(0, $DB->count_records('xapi_states'));
|
||||
$this->assertNull($result);
|
||||
|
||||
// Add, at least, one xAPI state record to database (with the default values).
|
||||
test_helper::create_state([], true);
|
||||
|
||||
// Check the state is found when it exists.
|
||||
$result = $handler->load_state($state);
|
||||
$records = $DB->get_records('xapi_states');
|
||||
$this->assertCount(1, $records);
|
||||
$record = reset($records);
|
||||
$this->check_state($component, $state, $record);
|
||||
$this->assertEquals($state->jsonSerialize(), $result->jsonSerialize());
|
||||
|
||||
// Check the state is not found when it doesn't exist.
|
||||
$state = test_helper::create_state(['activity' => item_activity::create_from_id('1')]);
|
||||
$state->set_state_data(null);
|
||||
$result = $handler->load_state($state);
|
||||
$records = $DB->get_records('xapi_states');
|
||||
$this->assertCount(1, $DB->get_records('xapi_states'));
|
||||
$this->assertNull($result);
|
||||
|
||||
// Check an exception is thrown when the state is not valid.
|
||||
$this->expectException(xapi_exception::class);
|
||||
$state = test_helper::create_state(['stateid' => 'paella']);
|
||||
$result = $handler->load_state($state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing delete_state method.
|
||||
*/
|
||||
public function test_delete_state(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$component = 'fake_component';
|
||||
$handler = handler::create($component);
|
||||
|
||||
// Check the state is not deleted (when there are no states).
|
||||
$state = test_helper::create_state();
|
||||
$this->assertEquals(0, $DB->count_records('xapi_states'));
|
||||
$result = $handler->delete_state($state);
|
||||
$this->assertTrue($result);
|
||||
$this->assertEquals(0, $DB->count_records('xapi_states'));
|
||||
|
||||
// Add, at least, one xAPI state record to database (with the default values).
|
||||
test_helper::create_state([], true);
|
||||
|
||||
// Check the state is not deleted if the given state doesn't meet its values.
|
||||
$state2 = test_helper::create_state(['activity' => item_activity::create_from_id('1')]);
|
||||
$result = $handler->delete_state($state2);
|
||||
$this->assertTrue($result);
|
||||
$this->assertCount(1, $DB->get_records('xapi_states'));
|
||||
|
||||
// Check the state is deleted if it exists.
|
||||
$result = $handler->delete_state($state);
|
||||
$this->assertTrue($result);
|
||||
$this->assertCount(0, $DB->get_records('xapi_states'));
|
||||
|
||||
// Check an exception is thrown when the state is not valid.
|
||||
$this->expectException(xapi_exception::class);
|
||||
$state = test_helper::create_state(['stateid' => 'paella']);
|
||||
$result = $handler->delete_state($state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing reset_states method.
|
||||
*/
|
||||
public function test_reset_states(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$component = 'fake_component';
|
||||
$handler = handler::create($component);
|
||||
|
||||
// Check the state is not reset (when there are no states).
|
||||
$this->assertCount(0, $DB->get_records_select('xapi_states', 'statedata IS NULL'));
|
||||
$handler->reset_states();
|
||||
$this->assertCount(0, $DB->get_records_select('xapi_states', 'statedata IS NULL'));
|
||||
|
||||
// Add, at least, one xAPI state record to database (with the default values).
|
||||
test_helper::create_state([], true);
|
||||
|
||||
// Check the state is not reset if the given state doesn't meet its values.
|
||||
$handler->reset_states('1');
|
||||
$this->assertCount(1, $DB->get_records('xapi_states'));
|
||||
$this->assertCount(0, $DB->get_records_select('xapi_states', 'statedata IS NULL'));
|
||||
|
||||
// Check the state is reset if it exists.
|
||||
$handler->reset_states();
|
||||
$this->assertCount(1, $DB->get_records('xapi_states'));
|
||||
$this->assertCount(1, $DB->get_records_select('xapi_states', 'statedata IS NULL'));
|
||||
|
||||
// Check the state is reset too when using some of the given parameters.
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
|
||||
$handler->reset_states('1');
|
||||
$this->assertCount(2, $DB->get_records('xapi_states'));
|
||||
$this->assertCount(2, $DB->get_records_select('xapi_states', 'statedata IS NULL'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing wipe_states method.
|
||||
*/
|
||||
public function test_wipe_states(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$component = 'fake_component';
|
||||
$handler = handler::create($component);
|
||||
|
||||
// Check the state is not wiped (when there are no states).
|
||||
$this->assertCount(0, $DB->get_records('xapi_states'));
|
||||
$handler->wipe_states();
|
||||
$this->assertCount(0, $DB->get_records('xapi_states'));
|
||||
|
||||
// Add, at least, one xAPI state record to database (with the default values).
|
||||
test_helper::create_state([], true);
|
||||
|
||||
// Check the state is not wiped if the given state doesn't meet its values.
|
||||
$handler->wipe_states('1');
|
||||
$this->assertCount(1, $DB->get_records('xapi_states'));
|
||||
|
||||
// Check the state is wiped if it exists.
|
||||
$handler->wipe_states();
|
||||
$this->assertCount(0, $DB->get_records('xapi_states'));
|
||||
|
||||
// Check the state is wiped too when using some of the given parameters.
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
|
||||
$this->assertCount(1, $DB->get_records('xapi_states'));
|
||||
$handler->wipe_states('1');
|
||||
$this->assertCount(0, $DB->get_records('xapi_states'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given state and record are equals.
|
||||
*
|
||||
* @param string $component The component name in frankenstyle.
|
||||
* @param state $state The state to check.
|
||||
* @param stdClass $record The record to be compared with the state.
|
||||
*/
|
||||
private function check_state(string $component, state $state, stdClass $record): void {
|
||||
$this->assertEquals($component, $record->component);
|
||||
$this->assertEquals($state->get_activity_id(), $record->itemid);
|
||||
$this->assertEquals($state->get_user()->id, $record->userid);
|
||||
$this->assertEquals(json_encode($state->jsonSerialize()), $record->statedata);
|
||||
$this->assertEquals($state->get_registration(), $record->registration);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
*/
|
||||
namespace core_xapi;
|
||||
|
||||
use stdClass;
|
||||
use core_xapi\local\state;
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use core_xapi\local\statement\item_agent;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -76,4 +78,41 @@ class test_helper {
|
||||
}
|
||||
return array_pop($records);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a valid state object with the params passed.
|
||||
*
|
||||
* All tests are based on craft different types of states. This function
|
||||
* is made to prevent redundant code on the test.
|
||||
*
|
||||
* @param array $info array of overriden state data (default []).
|
||||
* @param bool $createindatabase Whether the state object should be created in database too or not.
|
||||
* @return state the resulting state
|
||||
*/
|
||||
public static function create_state(array $info = [], bool $createindatabase = false): state {
|
||||
global $USER;
|
||||
|
||||
$component = $info['component'] ?? 'fake_component';
|
||||
$agent = $info['agent'] ?? item_agent::create_from_user($USER);
|
||||
$activity = $info['activity'] ?? item_activity::create_from_id('12345');
|
||||
$stateid = $info['stateid'] ?? 'state';
|
||||
$data = $info['data'] ?? json_decode('{"progress":0,"answers":[[["AA"],[""]],[{"answers":[]}]],"answered":[true,false]}');
|
||||
$registration = $info['registration'] ?? null;
|
||||
|
||||
$state = new state($agent, $activity, $stateid, (object)$data, $registration);
|
||||
|
||||
if ($createindatabase) {
|
||||
try {
|
||||
$handler = handler::create($component);
|
||||
$statestore = $handler->get_state_store();
|
||||
} catch (\Exception $exception) {
|
||||
// If the component is not available, use the standard one to force it's creation.
|
||||
$statestore = new state_store($component);
|
||||
}
|
||||
$statestore->put($state);
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi\privacy;
|
||||
|
||||
use core_privacy\tests\provider_testcase;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_xapi\privacy\provider;
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use core_xapi\test_helper;
|
||||
|
||||
/**
|
||||
* Privacy tests for core_xapi.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @category test
|
||||
* @copyright 2023 Sara Arjona ([email protected])
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_xapi\privacy\provider
|
||||
*/
|
||||
class provider_test extends provider_testcase {
|
||||
|
||||
/**
|
||||
* Setup to ensure that fixtures are loaded.
|
||||
*/
|
||||
public static function setUpBeforeClass(): void {
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to set up some sample data.
|
||||
*
|
||||
* @return array Array with the users that have been created.
|
||||
*/
|
||||
protected function set_up_data(): array {
|
||||
$user1 = self::getDataGenerator()->create_user();
|
||||
$user2 = self::getDataGenerator()->create_user();
|
||||
$user3 = self::getDataGenerator()->create_user();
|
||||
|
||||
// Add a few xAPI state records to database.
|
||||
$context = \context_system::instance();
|
||||
$cid = $context->id;
|
||||
$this->setUser($user1);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id($context->id)], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('3'), 'component' => 'mod_h5pactivity'], true);
|
||||
$this->setUser($user2);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id($context->id)], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('4')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('5')], true);
|
||||
$this->setUser($user3);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id($cid), 'component' => 'mod_h5pactivity'], true);
|
||||
|
||||
return [$user1, $user2, $user3];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test confirming that contexts of xapi items can be added to the contextlist.
|
||||
*/
|
||||
public function test_add_contexts_for_userid(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
list($user1, $user2) = $this->set_up_data();
|
||||
|
||||
// Ask the xapi privacy api to export contexts for xapi of the type we just created, for user1.
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
provider::add_contexts_for_userid($contextlist, $user1->id, 'fake_component');
|
||||
$this->assertCount(2, $contextlist->get_contextids());
|
||||
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
provider::add_contexts_for_userid($contextlist, $user1->id, 'mod_h5pactivity');
|
||||
$this->assertCount(1, $contextlist->get_contextids());
|
||||
|
||||
// Ask the xapi privacy api to export contexts for xapi of the type we just created, for user2.
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
provider::add_contexts_for_userid($contextlist, $user2->id, 'fake_component');
|
||||
$this->assertCount(4, $contextlist->get_contextids());
|
||||
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
provider::add_contexts_for_userid($contextlist, $user2->id, 'mod_h5pactivity');
|
||||
$this->assertCount(0, $contextlist->get_contextids());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test confirming that user ID's of xapi states can be added to the userlist.
|
||||
*/
|
||||
public function test_add_userids_for_context() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
list($user1, $user2, $user3) = $this->set_up_data();
|
||||
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user1->id]));
|
||||
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
|
||||
$systemcontext = \context_system::instance();
|
||||
|
||||
// Ask the xapi privacy api to export userids for xapi states of the type we just created, in the system context.
|
||||
$userlist = new \core_privacy\local\request\userlist($systemcontext, 'fake_component');
|
||||
provider::add_userids_for_context($userlist, 'fake_component');
|
||||
// Only user1 and user2 should be returned, because user3 has a different component for the system context.
|
||||
$this->assertCount(2, $userlist->get_userids());
|
||||
$expected = [
|
||||
$user1->id,
|
||||
$user2->id,
|
||||
];
|
||||
$this->assertEqualsCanonicalizing($expected, $userlist->get_userids());
|
||||
|
||||
// Ask the xapi privacy api to export userids for xapi states of the type we just created for a different component.
|
||||
$userlist = new \core_privacy\local\request\userlist($systemcontext, 'mod_h5pactivity');
|
||||
provider::add_userids_for_context($userlist, 'mod_h5pactivity');
|
||||
// Only user3 should be returned, because the others have a different component for the system context.
|
||||
$this->assertCount(1, $userlist->get_userids());
|
||||
$expected = [$user3->id];
|
||||
$this->assertEqualsCanonicalizing($expected, $userlist->get_userids());
|
||||
|
||||
// Ask the xapi privacy api to export userids xapi states for an empty component.
|
||||
$userlist = new \core_privacy\local\request\userlist($systemcontext, 'empty_component');
|
||||
provider::add_userids_for_context($userlist, 'empty_component');
|
||||
$this->assertCount(0, $userlist->get_userids());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test fetching the xapi state data for a specified user in a specified component and itemid.
|
||||
*/
|
||||
public function test_get_xapi_states_for_user() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
list($user1, $user2, $user3) = $this->set_up_data();
|
||||
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user1->id]));
|
||||
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
|
||||
$systemcontext = \context_system::instance();
|
||||
|
||||
// Get the states info for user1 in the system context.
|
||||
$result = provider::get_xapi_states_for_user($user1->id, 'fake_component', $systemcontext->id);
|
||||
$info = (object) reset($result);
|
||||
// Ensure the correct data has been returned.
|
||||
$this->assertNotEmpty($info->statedata);
|
||||
$this->assertNotEmpty(transform::datetime($info->timecreated));
|
||||
$this->assertNotEmpty(transform::datetime($info->timemodified));
|
||||
|
||||
// Get the states info for user2 in the system context.
|
||||
$result = provider::get_xapi_states_for_user($user2->id, 'fake_component', $systemcontext->id);
|
||||
$info = (object) reset($result);
|
||||
// Ensure the correct data has been returned.
|
||||
$this->assertNotEmpty($info->statedata);
|
||||
$this->assertNotEmpty(transform::datetime($info->timecreated));
|
||||
$this->assertNotEmpty(transform::datetime($info->timemodified));
|
||||
|
||||
// Get the states info for user3 in the system context (it should be empty).
|
||||
$info = provider::get_xapi_states_for_user($user3->id, 'fake_component', $systemcontext->id);
|
||||
// Ensure the correct data has been returned.
|
||||
$this->assertEmpty($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deletion of user xapi states based on an approved_contextlist and component area.
|
||||
*/
|
||||
public function test_delete_states_for_user(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
list($user1, $user2, $user3) = $this->set_up_data();
|
||||
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user1->id]));
|
||||
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
|
||||
|
||||
// Now, delete the xapistates for user1 only.
|
||||
$user1context = \context_user::instance($user1->id);
|
||||
$approvedcontextlist = new \core_privacy\local\request\approved_contextlist($user1, 'fake_component', [$user1context->id]);
|
||||
provider::delete_states_for_user($approvedcontextlist, 'fake_component');
|
||||
|
||||
// Verify that we have no xapi states for user1 for the fake_component but that the rest of records are intact.
|
||||
$this->assertEquals(0, $DB->count_records('xapi_states', ['userid' => $user1->id, 'component' => 'fake_component']));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user1->id, 'component' => 'mod_h5pactivity']));
|
||||
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deletion of all user xapi states.
|
||||
*/
|
||||
public function test_delete_states_for_all_users(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
list($user1, $user2, $user3) = $this->set_up_data();
|
||||
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user1->id]));
|
||||
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
|
||||
|
||||
// Now, delete all course module xapi states in the 'fake_component' context only.
|
||||
provider::delete_states_for_all_users(\context_system::instance(), 'fake_component');
|
||||
|
||||
// Verify that only content with the context_system for the fake_component have been removed.
|
||||
$this->assertEquals(2, $DB->count_records('xapi_states', ['userid' => $user1->id]));
|
||||
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user2->id]));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deletion of user xapi states based on an approved_userlist and component area.
|
||||
*/
|
||||
public function test_delete_states_for_userlist() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
list($user1, $user2, $user3) = $this->set_up_data();
|
||||
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user1->id]));
|
||||
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
|
||||
$systemcontext = \context_system::instance();
|
||||
|
||||
// Ask the xapi privacy api to export userids for states of the type we just created, in the system context.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($systemcontext, 'fake_component');
|
||||
provider::add_userids_for_context($userlist1);
|
||||
// Verify we have two userids in the list for system context.
|
||||
$this->assertCount(2, $userlist1->get_userids());
|
||||
|
||||
// Now, delete the states for user1 only in the system context.
|
||||
$approveduserlist = new \core_privacy\local\request\approved_userlist($systemcontext, 'fake_component', [$user1->id]);
|
||||
provider::delete_states_for_userlist($approveduserlist);
|
||||
// Ensure user1's data was deleted and user2 is still returned for system context.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($systemcontext, 'fake_component');
|
||||
provider::add_userids_for_context($userlist1);
|
||||
$this->assertCount(1, $userlist1->get_userids());
|
||||
// Verify that user2 is still in the list for system context.
|
||||
$expected = [$user2->id];
|
||||
$this->assertEquals($expected, $userlist1->get_userids());
|
||||
// Verify that the data of user1 in other contexts was not deleted.
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user1->id]));
|
||||
$this->assertEquals(2, $DB->count_records('xapi_states', ['itemid' => $systemcontext->id]));
|
||||
|
||||
// Verify that no data is removed if the component is empty.
|
||||
$userlist3 = new \core_privacy\local\request\userlist($systemcontext, 'empty_component');
|
||||
provider::add_userids_for_context($userlist3);
|
||||
$this->assertCount(0, $userlist3->get_userids());
|
||||
$this->assertEquals(2, $DB->count_records('xapi_states', ['itemid' => $systemcontext->id]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi;
|
||||
|
||||
use core_xapi\local\statement\item_agent;
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use advanced_testcase;
|
||||
|
||||
/**
|
||||
* Contains test cases for testing xAPI state store methods.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 4.2
|
||||
* @covers \core_xapi\state_store
|
||||
* @copyright 2023 Sara Arjona ([email protected])
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class state_store_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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing delete method.
|
||||
*
|
||||
* @dataProvider states_provider
|
||||
* @param array $info Array of overriden state data.
|
||||
* @param bool $expected Expected results.
|
||||
* @return void
|
||||
*/
|
||||
public function test_state_store_delete(array $info, bool $expected): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
// Add, at least, one xAPI state record to database (with the default values).
|
||||
test_helper::create_state([], true);
|
||||
|
||||
// Get current states in database.
|
||||
$currentstates = $DB->count_records('xapi_states');
|
||||
|
||||
// Perform test.
|
||||
$component = $info['component'] ?? 'fake_component';
|
||||
$state = test_helper::create_state($info);
|
||||
$store = new state_store($component);
|
||||
$result = $store->delete($state);
|
||||
|
||||
// Check the state has been removed.
|
||||
$records = $DB->get_records('xapi_states');
|
||||
$this->assertTrue($result);
|
||||
if ($expected) {
|
||||
$this->assertCount($currentstates - 1, $records);
|
||||
} else if ($expected === 'false') {
|
||||
$this->assertCount($currentstates, $records);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing get method.
|
||||
*
|
||||
* @dataProvider states_provider
|
||||
* @param array $info Array of overriden state data.
|
||||
* @param bool $expected Expected results.
|
||||
* @return void
|
||||
*/
|
||||
public function test_state_store_get(array $info, bool $expected): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
// Add, at least, one xAPI state record to database (with the default values).
|
||||
test_helper::create_state([], true);
|
||||
|
||||
// Perform test.
|
||||
$component = $info['component'] ?? 'fake_component';
|
||||
$state = test_helper::create_state($info);
|
||||
// Remove statedata from the state object, to guarantee the get method is working as expected.
|
||||
$state->set_state_data(null);
|
||||
$store = new state_store($component);
|
||||
$result = $store->get($state);
|
||||
|
||||
// Check the returned state has the expected values.
|
||||
if ($expected) {
|
||||
$this->assertEquals(json_encode($state->jsonSerialize()), json_encode($result->jsonSerialize()));
|
||||
} else {
|
||||
$this->assertNull($result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the test_state_store_delete and test_state_store_get tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function states_provider() : array {
|
||||
return [
|
||||
'Existing and valid state' => [
|
||||
'info' => [],
|
||||
'expected' => true,
|
||||
],
|
||||
'No state (wrong activityid)' => [
|
||||
'info' => ['activity' => item_activity::create_from_id('1')],
|
||||
'expected' => false,
|
||||
],
|
||||
'No state (wrong stateid)' => [
|
||||
'info' => ['stateid' => 'food'],
|
||||
'expected' => false,
|
||||
],
|
||||
'No state (wrong component)' => [
|
||||
'info' => ['component' => 'mod_h5pactivity'],
|
||||
'expected' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing put method.
|
||||
*
|
||||
* @dataProvider put_states_provider
|
||||
* @param array $info Array of overriden state data.
|
||||
* @param string $expected Expected results.
|
||||
* @return void
|
||||
*/
|
||||
public function test_state_store_put(array $info, string $expected): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
// Add, at least, one xAPI state record to database (with the default values).
|
||||
test_helper::create_state([], true);
|
||||
|
||||
// Get current states in database.
|
||||
$currentstates = $DB->count_records('xapi_states');
|
||||
|
||||
// Perform test.
|
||||
$component = $info['component'] ?? 'fake_component';
|
||||
$state = test_helper::create_state($info);
|
||||
$store = new state_store($component);
|
||||
$result = $store->put($state);
|
||||
|
||||
// Check the state has been added/updated.
|
||||
$this->assertTrue($result);
|
||||
$recordsnum = $DB->count_records('xapi_states');
|
||||
$params = [
|
||||
'component' => $component,
|
||||
'userid' => $state->get_user()->id,
|
||||
'itemid' => $state->get_activity_id(),
|
||||
'stateid' => $state->get_state_id(),
|
||||
'registration' => $state->get_registration(),
|
||||
];
|
||||
$records = $DB->get_records('xapi_states', $params);
|
||||
$record = reset($records);
|
||||
if ($expected === 'added') {
|
||||
$this->assertEquals($currentstates + 1, $recordsnum);
|
||||
$this->assertEquals($record->timecreated, $record->timemodified);
|
||||
} else if ($expected === 'updated') {
|
||||
$this->assertEquals($currentstates, $recordsnum);
|
||||
$this->assertGreaterThanOrEqual($record->timecreated, $record->timemodified);
|
||||
}
|
||||
|
||||
$this->assertEquals($component, $record->component);
|
||||
$this->assertEquals($state->get_activity_id(), $record->itemid);
|
||||
$this->assertEquals($state->get_user()->id, $record->userid);
|
||||
$this->assertEquals(json_encode($state->jsonSerialize()), $record->statedata);
|
||||
$this->assertEquals($state->get_registration(), $record->registration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the test_state_store_put tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function put_states_provider() : array {
|
||||
return [
|
||||
'Update existing state' => [
|
||||
'info' => [],
|
||||
'expected' => 'updated',
|
||||
],
|
||||
'Update existing state (change statedata)' => [
|
||||
'info' => ['statedata' => '{"progress":0,"answers":[[["BB"],[""]],[{"answers":[]}]],"answered":[true,false]}'],
|
||||
'expected' => 'updated',
|
||||
],
|
||||
'Add state (with different itemid)' => [
|
||||
'info' => ['activity' => item_activity::create_from_id('1')],
|
||||
'expected' => 'added',
|
||||
],
|
||||
'Add state (with different stateid)' => [
|
||||
'info' => ['stateid' => 'food'],
|
||||
'expected' => 'added',
|
||||
],
|
||||
'Add state (with different component)' => [
|
||||
'info' => ['component' => 'mod_h5pactivity'],
|
||||
'expected' => 'added',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing reset method.
|
||||
*
|
||||
* @dataProvider reset_wipe_states_provider
|
||||
* @param array $info Array of overriden state data.
|
||||
* @param int $expected The states that will be reset.
|
||||
* @return void
|
||||
*/
|
||||
public function test_state_store_reset(array $info, int $expected): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$other = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Add a few xAPI state records to database.
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('2'), 'stateid' => 'paella'], true);
|
||||
test_helper::create_state([
|
||||
'activity' => item_activity::create_from_id('3'),
|
||||
'agent' => item_agent::create_from_user($other),
|
||||
'stateid' => 'paella',
|
||||
'registration' => 'ABC',
|
||||
], true);
|
||||
test_helper::create_state([
|
||||
'activity' => item_activity::create_from_id('4'),
|
||||
'agent' => item_agent::create_from_user($other),
|
||||
], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'my_component'], true);
|
||||
test_helper::create_state([
|
||||
'activity' => item_activity::create_from_id('6'),
|
||||
'component' => 'my_component',
|
||||
'stateid' => 'paella',
|
||||
'agent' => item_agent::create_from_user($other),
|
||||
], true);
|
||||
|
||||
// Get current states in database.
|
||||
$currentstates = $DB->count_records('xapi_states');
|
||||
|
||||
// Perform test.
|
||||
$component = $info['component'] ?? 'fake_component';
|
||||
$itemid = $info['activity'] ?? null;
|
||||
$userid = (array_key_exists('agent', $info) && $info['agent'] === 'other') ? $other->id : null;
|
||||
$stateid = $info['stateid'] ?? null;
|
||||
$registration = $info['registration'] ?? null;
|
||||
$store = new state_store($component);
|
||||
$store->reset($itemid, $userid, $stateid, $registration);
|
||||
|
||||
// Check the states haven't been removed.
|
||||
$this->assertCount($currentstates, $DB->get_records('xapi_states'));
|
||||
$records = $DB->get_records_select('xapi_states', 'statedata IS NULL');
|
||||
$this->assertCount($expected, $records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing wipe method.
|
||||
*
|
||||
* @dataProvider reset_wipe_states_provider
|
||||
* @param array $info Array of overriden state data.
|
||||
* @param int $expected The removed states.
|
||||
* @return void
|
||||
*/
|
||||
public function test_state_store_wipe(array $info, int $expected): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$other = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Add a few xAPI state records to database.
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('2'), 'stateid' => 'paella'], true);
|
||||
test_helper::create_state([
|
||||
'activity' => item_activity::create_from_id('3'),
|
||||
'agent' => item_agent::create_from_user($other),
|
||||
'stateid' => 'paella',
|
||||
'registration' => 'ABC',
|
||||
], true);
|
||||
test_helper::create_state([
|
||||
'activity' => item_activity::create_from_id('4'),
|
||||
'agent' => item_agent::create_from_user($other),
|
||||
], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'my_component'], true);
|
||||
test_helper::create_state([
|
||||
'activity' => item_activity::create_from_id('6'),
|
||||
'component' => 'my_component',
|
||||
'stateid' => 'paella',
|
||||
'agent' => item_agent::create_from_user($other),
|
||||
], true);
|
||||
|
||||
// Get current states in database.
|
||||
$currentstates = $DB->count_records('xapi_states');
|
||||
|
||||
// Perform test.
|
||||
$component = $info['component'] ?? 'fake_component';
|
||||
$itemid = $info['activity'] ?? null;
|
||||
$userid = (array_key_exists('agent', $info) && $info['agent'] === 'other') ? $other->id : null;
|
||||
$stateid = $info['stateid'] ?? null;
|
||||
$registration = $info['registration'] ?? null;
|
||||
$store = new state_store($component);
|
||||
$store->wipe($itemid, $userid, $stateid, $registration);
|
||||
|
||||
// Check the states have been removed.
|
||||
$records = $DB->get_records('xapi_states');
|
||||
$this->assertCount($currentstates - $expected, $records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the test_state_store_reset and test_state_store_wipe tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function reset_wipe_states_provider() : array {
|
||||
return [
|
||||
'With fake_component' => [
|
||||
'info' => [],
|
||||
'expected' => 4,
|
||||
],
|
||||
'With my_component' => [
|
||||
'info' => ['component' => 'my_component'],
|
||||
'expected' => 2,
|
||||
],
|
||||
'With unexisting_component' => [
|
||||
'info' => ['component' => 'unexisting_component'],
|
||||
'expected' => 0,
|
||||
],
|
||||
'Existing activity' => [
|
||||
'info' => ['activity' => '1'],
|
||||
'expected' => 1,
|
||||
],
|
||||
'Unexisting activity' => [
|
||||
'info' => ['activity' => '1111'],
|
||||
'expected' => 0,
|
||||
],
|
||||
'Existing userid' => [
|
||||
'info' => ['agent' => 'other'],
|
||||
'expected' => 2,
|
||||
],
|
||||
'Existing stateid' => [
|
||||
'info' => ['stateid' => 'paella'],
|
||||
'expected' => 2,
|
||||
],
|
||||
'Unexisting stateid' => [
|
||||
'info' => ['stateid' => 'chorizo'],
|
||||
'expected' => 0,
|
||||
],
|
||||
'Existing registration' => [
|
||||
'info' => ['registration' => 'ABC'],
|
||||
'expected' => 1,
|
||||
],
|
||||
'Uxexisting registration' => [
|
||||
'info' => ['registration' => 'XYZ'],
|
||||
'expected' => 0,
|
||||
],
|
||||
'Existing stateid combined with activity' => [
|
||||
'info' => ['activity' => '3', 'stateid' => 'paella'],
|
||||
'expected' => 1,
|
||||
],
|
||||
'Uxexisting stateid combined with activity' => [
|
||||
'info' => ['activity' => '1', 'stateid' => 'paella'],
|
||||
'expected' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing cleanup method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_state_store_cleanup(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
$other = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Add a few xAPI state records to database.
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('3')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('4')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'my_component'], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('6'), 'component' => 'my_component'], true);
|
||||
|
||||
// Get current states in database.
|
||||
$currentstates = $DB->count_records('xapi_states');
|
||||
|
||||
// Perform test.
|
||||
$component = 'fake_component';
|
||||
$store = new state_store($component);
|
||||
$store->cleanup();
|
||||
|
||||
// Check no state has been removed (because the entries are not old enough).
|
||||
$this->assertEquals($currentstates, $DB->count_records('xapi_states'));
|
||||
|
||||
// Make the existing state entries older.
|
||||
$timepast = time() - 2;
|
||||
$DB->set_field('xapi_states', 'timecreated', $timepast);
|
||||
$DB->set_field('xapi_states', 'timemodified', $timepast);
|
||||
|
||||
// Create 1 more state, that shouldn't be removed after the cleanup.
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('7')], true);
|
||||
|
||||
// Set the config to remove states older than 1 second.
|
||||
set_config('xapicleanupperiod', 1);
|
||||
|
||||
// Check old states for fake_component have been removed.
|
||||
$currentstates = $DB->count_records('xapi_states');
|
||||
$store->cleanup();
|
||||
$this->assertEquals($currentstates - 4, $DB->count_records('xapi_states'));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['component' => $component]));
|
||||
$this->assertEquals(2, $DB->count_records('xapi_states', ['component' => 'my_component']));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_xapi\task;
|
||||
|
||||
use core_xapi\local\statement\item_activity;
|
||||
use advanced_testcase;
|
||||
use core_xapi\test_helper;
|
||||
|
||||
/**
|
||||
* Contains test cases for testing the scheduled task state_cleanup_task.
|
||||
*
|
||||
* @package core_xapi
|
||||
* @since Moodle 4.2
|
||||
* @covers \core_xapi\task\state_cleanup_task
|
||||
* @copyright 2023 Sara Arjona ([email protected])
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class state_cleanup_task_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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing execute method in state_cleanup_task.
|
||||
*/
|
||||
public function test_state_cleanup_task(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Scenario.
|
||||
$this->setAdminUser();
|
||||
|
||||
// Add a few xAPI state records to database.
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('3')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('4')], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'mod_h5pactivity'], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('6'), 'component' => 'mod_h5pactivity'], true);
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('7'), 'component' => 'mod_h5pactivity'], true);
|
||||
|
||||
// Perform test.
|
||||
$task = new state_cleanup_task();
|
||||
$task->execute();
|
||||
|
||||
// Check no state has been removed yet (because the entries are not old enough).
|
||||
$this->assertEquals(7, $DB->count_records('xapi_states'));
|
||||
|
||||
// Make the existing state entries older.
|
||||
$timepast = time() - 2;
|
||||
$DB->set_field('xapi_states', 'timecreated', $timepast);
|
||||
$DB->set_field('xapi_states', 'timemodified', $timepast);
|
||||
|
||||
// Create 1 more state, that shouldn't be removed after the cleanup.
|
||||
test_helper::create_state(['activity' => item_activity::create_from_id('8'), 'component' => 'mod_h5pactivity'], true);
|
||||
|
||||
// Set the config to remove states older than 1 second.
|
||||
set_config('xapicleanupperiod', 1);
|
||||
|
||||
// Check old states have been removed.
|
||||
$task->execute();
|
||||
$this->assertEquals(5, $DB->count_records('xapi_states'));
|
||||
$this->assertEquals(4, $DB->count_records('xapi_states', ['component' => 'fake_component']));
|
||||
$this->assertEquals(1, $DB->count_records('xapi_states', ['component' => 'mod_h5pactivity']));
|
||||
$this->assertEquals(0, $DB->count_records('xapi_states', ['component' => 'my_component']));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <[email protected]>
|
||||
* @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 <[email protected]>
|
||||
* @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);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 <[email protected]>
|
||||
* @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 <[email protected]>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ([email protected])
|
||||
* @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));
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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 <[email protected]>
|
||||
* @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 <[email protected]>
|
||||
* @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.
|
||||
*/
|
||||
@@ -253,4 +315,102 @@ class lib_test extends advanced_testcase {
|
||||
// activity for separate groups mode.
|
||||
$this->assertCount(0, $recentactivity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <[email protected]>
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,25 +14,16 @@
|
||||
// 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 <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace mod_h5pactivity\xapi;
|
||||
|
||||
use \core_xapi\local\statement;
|
||||
use \core_xapi\local\statement\item;
|
||||
use \core_xapi\local\statement\item_agent;
|
||||
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 context_module;
|
||||
use core_xapi\test_helper;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
@@ -42,9 +33,18 @@ use stdClass;
|
||||
* @category test
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
* @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.
|
||||
*
|
||||
@@ -387,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,5 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'mod_h5pactivity';
|
||||
$plugin->version = 2022112800;
|
||||
$plugin->version = 2023020900;
|
||||
$plugin->requires = 2022111800;
|
||||
|
||||
Reference in New Issue
Block a user