MDL-67788 core_h5p: add tracking to player
authorFerran Recio <ferran@moodle.com>
Tue, 3 Mar 2020 15:41:10 +0000 (16:41 +0100)
committerFerran Recio <ferran@moodle.com>
Mon, 13 Apr 2020 10:39:58 +0000 (12:39 +0200)
h5p/classes/player.php
h5p/embed.php
h5p/js/embed.js
h5p/js/h5p_overrides.js

index 9a86b44..3f664fa 100644 (file)
@@ -27,6 +27,7 @@ namespace core_h5p;
 defined('MOODLE_INTERNAL') || die();
 
 use core_h5p\local\library\autoloader;
+use core_xapi\local\statement\item_activity;
 
 /**
  * H5P player class, for displaying any local H5P content.
@@ -67,6 +68,11 @@ class player {
      */
     private $content;
 
+    /**
+     * @var string optional component name to send xAPI statements.
+     */
+    private $component;
+
     /**
      * @var string Type of embed object, div or iframe.
      */
@@ -98,8 +104,9 @@ class player {
      * @param string $url Local URL of the H5P file to display.
      * @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
      */
-    public function __construct(string $url, \stdClass $config, bool $preventredirect = true) {
+    public function __construct(string $url, \stdClass $config, bool $preventredirect = true, string $component = '') {
         if (empty($url)) {
             throw new \moodle_exception('h5pinvalidurl', 'core_h5p');
         }
@@ -110,6 +117,8 @@ class player {
 
         $this->messages = new \stdClass();
 
+        $this->component = $component;
+
         // Create \core_h5p\core instance.
         $this->core = $this->factory->get_core();
 
@@ -129,14 +138,17 @@ class player {
      * @param string $url Local URL of the H5P file to display.
      * @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
      *
      * @return string The embedable code to display a H5P file.
      */
-    public static function display(string $url, \stdClass $config, bool $preventredirect = true): string {
+    public static function display(string $url, \stdClass $config, bool $preventredirect = true,
+            string $component = ''): string {
         global $OUTPUT;
         $params = [
                 'url' => $url,
                 'preventredirect' => $preventredirect,
+                'component' => $component,
             ];
 
         $optparams = ['frame', 'export', 'embed', 'copyright'];
@@ -193,6 +205,7 @@ class player {
         $contenturl = \moodle_url::make_pluginfile_url($systemcontext->id, \core_h5p\file_storage::COMPONENT,
             \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'],
@@ -202,7 +215,7 @@ class player {
             'resizeCode'      => self::get_resize_code(),
             'title'           => $this->content['slug'],
             'displayOptions'  => $displayoptions,
-            'url'             => self::get_embed_url($this->url->out())->out(),
+            'url'             => $xapiobject->get_data()->id,
             'contentUrl'      => $contenturl->out(),
             'metadata'        => $this->content['metadata'],
             'contentUserData' => [0 => ['state' => '{}']]
@@ -698,7 +711,7 @@ class player {
      * @return array The settings.
      */
     private function get_core_settings(): array {
-        global $CFG;
+        global $CFG, $USER;
 
         $basepath = $CFG->wwwroot . '/';
         $systemcontext = \context_system::instance();
@@ -717,7 +730,7 @@ class player {
             'saveFreq' => false,
             'siteUrl' => $CFG->wwwroot,
             'l10n' => array('H5P' => $this->core->getLocalization()),
-            'user' => [],
+            'user' => ['name' => $USER->username, 'mail' => $USER->email],
             'hubIsEnabled' => false,
             'reportingIsEnabled' => false,
             'crossorigin' => null,
@@ -725,6 +738,7 @@ class player {
             'pluginCacheBuster' => $this->get_cache_buster(),
             'libraryUrl' => autoloader::get_h5p_core_library_url('js'),
             'moodleLibraryPaths' => $this->core->get_dependency_roots($this->h5pid),
+            'moodleComponent' => $this->component,
         );
 
         return $settings;
index d29ffc1..3601436 100644 (file)
@@ -36,9 +36,11 @@ $config->copyright = optional_param('copyright', 0, PARAM_INT);
 
 $preventredirect = optional_param('preventredirect', true, PARAM_BOOL);
 
+$component = optional_param('component', '', PARAM_COMPONENT);
+
 $PAGE->set_url(new \moodle_url('/h5p/embed.php', array('url' => $url)));
 try {
-    $h5pplayer = new \core_h5p\player($url, $config, $preventredirect);
+    $h5pplayer = new \core_h5p\player($url, $config, $preventredirect, $component);
     $messages = $h5pplayer->get_messages();
 
 } catch (\Exception $e) {
@@ -92,4 +94,4 @@ if (empty($messages->error) && empty($messages->exception)) {
     echo $OUTPUT->render_from_template('core_h5p/h5perror', $messages);
 }
 
-echo $OUTPUT->footer();
\ No newline at end of file
+echo $OUTPUT->footer();
index 6135db2..87cb996 100644 (file)
@@ -71,6 +71,27 @@ H5PEmbedCommunicator = (function() {
             // Parent origin can be anything.
             window.parent.postMessage(data, '*');
         };
+
+        /**
+         * Send a xAPI statement to LMS.
+         *
+         * @param {string} component
+         * @param {Object} statements
+         */
+        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
+                   }
+                ]);
+            });
+        };
     }
 
     return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
@@ -150,6 +171,38 @@ document.onreadystatechange = function() {
         }, 0);
     });
 
+    // Get emitted xAPI data.
+    H5P.externalDispatcher.on('xAPI', function(event) {
+        var moodlecomponent = H5P.getMoodleComponent();
+        if (moodlecomponent == undefined) {
+            return;
+        }
+        // Skip malformed events.
+        var hasStatement = event && event.data && event.data.statement;
+        if (!hasStatement) {
+            return;
+        }
+
+        var statement = event.data.statement;
+        var validVerb = statement.verb && statement.verb.id;
+        if (!validVerb) {
+            return;
+        }
+
+        var isCompleted = statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered'
+                    || statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed';
+
+        var isChild = statement.context && statement.context.contextActivities &&
+        statement.context.contextActivities.parent &&
+        statement.context.contextActivities.parent[0] &&
+        statement.context.contextActivities.parent[0].id;
+
+        if (isCompleted && !isChild) {
+            var statements = H5P.getXAPIStatements(this.contentId, statement);
+            H5PEmbedCommunicator.post(moodlecomponent, statements);
+        }
+    });
+
     // Trigger initial resize for instance.
     H5P.trigger(instance, 'resize');
 };
index 10679e0..eea05b4 100644 (file)
@@ -7,3 +7,43 @@ H5P.getLibraryPath = function (library) {
     }
     return H5P._getLibraryPath(library);
 };
+H5P.findInstanceFromId = function (contentId) {
+    if (!contentId) {
+        return H5P.instances[0];
+    }
+    if (H5P.instances !== undefined) {
+        for (var i = 0; i < H5P.instances.length; i++) {
+            if (H5P.instances[i].contentId === contentId) {
+                return H5P.instances[i];
+            }
+        }
+    }
+    return undefined;
+};
+H5P.getXAPIStatements = function (contentId, statement) {
+    var statements = [];
+    var instance = H5P.findInstanceFromId(contentId);
+    if (!instance){
+        return statements;
+    }
+    if (instance.getXAPIData == undefined) {
+        var xAPIData = {
+            statement: statement
+        };
+    } else {
+        var xAPIData = instance.getXAPIData();
+    }
+    if (xAPIData.statement != undefined) {
+        statements.push(xAPIData.statement);
+    }
+    if (xAPIData.children != undefined) {
+        statements = statements.concat(xAPIData.children.map(a => a.statement));
+    }
+    return statements;
+};
+H5P.getMoodleComponent = function () {
+    if (H5PIntegration.moodleComponent) {
+        return H5PIntegration.moodleComponent;
+    }
+    return undefined;
+};