Skip to content

Commit

Permalink
MDL-67789 xapi: Add support to save content state
Browse files Browse the repository at this point in the history
  • Loading branch information
ferranrecio authored and sarjona committed Mar 15, 2023
1 parent 12a8176 commit 03a4abd
Show file tree
Hide file tree
Showing 29 changed files with 3,349 additions and 50 deletions.
8 changes: 8 additions & 0 deletions admin/settings/server.php
Expand Up @@ -264,6 +264,14 @@
]
));

$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',
Expand Down
14 changes: 13 additions & 1 deletion lang/en/xapi.php
Expand Up @@ -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';
5 changes: 5 additions & 0 deletions lib/adminlib.php
Expand Up @@ -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);

Expand Down
23 changes: 22 additions & 1 deletion lib/db/install.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20221216" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20230118" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
Expand Down Expand Up @@ -4762,5 +4762,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>
27 changes: 27 additions & 0 deletions lib/db/services.php
Expand Up @@ -2758,6 +2758,33 @@
'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' => '',
Expand Down
11 changes: 10 additions & 1 deletion lib/db/tasks.php
Expand Up @@ -436,5 +436,14 @@
'day' => '*',
'dayofweek' => '*',
'month' => '*'
]
],
[
'classname' => 'core_xapi\task\state_cleanup_task',
'blocking' => 0,
'minute' => 'R',
'hour' => '0',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
],
);
37 changes: 37 additions & 0 deletions lib/db/upgrade.php
Expand Up @@ -3142,5 +3142,42 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2023031000.02);
}

if ($oldversion < 2023031400.01) {

// 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.01);
}

return true;
}
69 changes: 69 additions & 0 deletions lib/xapi/classes/api.php
@@ -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 changes: 114 additions & 0 deletions lib/xapi/classes/external/delete_state.php
@@ -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');
}
}

0 comments on commit 03a4abd

Please sign in to comment.