Merge branch 'MDL-68379-master' of git://github.com/ferranrecio/moodle
authorSara Arjona <sara@moodle.com>
Mon, 4 May 2020 18:02:06 +0000 (20:02 +0200)
committerSara Arjona <sara@moodle.com>
Mon, 4 May 2020 18:02:06 +0000 (20:02 +0200)
12 files changed:
lib/xapi/classes/local/statement.php
lib/xapi/classes/local/statement/item_attachment.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_context.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_result.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_score.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_attachment_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_context_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_result_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_score_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement_test.php
mod/h5pactivity/tests/local/attempt_test.php
mod/h5pactivity/tests/xapi/handler_test.php

index 7314d4b..af69b5a 100644 (file)
@@ -28,6 +28,9 @@ use core_xapi\local\statement\item;
 use core_xapi\local\statement\item_actor;
 use core_xapi\local\statement\item_object;
 use core_xapi\local\statement\item_verb;
+use core_xapi\local\statement\item_result;
+use core_xapi\local\statement\item_attachment;
+use core_xapi\local\statement\item_context;
 use core_xapi\xapi_exception;
 use JsonSerializable;
 use stdClass;
@@ -42,39 +45,39 @@ defined('MOODLE_INTERNAL') || die();
  */
 class statement implements JsonSerializable {
 
-    /** @var actor The statement actor. */
+    /** @var item_actor The statement actor. */
     protected $actor = null;
 
-    /** @var verb The statement verb. */
+    /** @var item_verb The statement verb. */
     protected $verb = null;
 
-    /** @var object The statement object. */
+    /** @var item_object The statement object. */
     protected $object = null;
 
-    /** @var result The statement result. */
+    /** @var item_result The statement result. */
     protected $result = null;
 
-    /** @var context The statement context. */
+    /** @var item_context The statement context. */
     protected $context = null;
 
-    /** @var timestamp The statement timestamp. */
+    /** @var string The statement timestamp. */
     protected $timestamp = null;
 
-    /** @var stored The statement stored. */
+    /** @var string The statement stored. */
     protected $stored = null;
 
     /** @var authority The statement authority. */
     protected $authority = null;
 
-    /** @var version The statement version. */
+    /** @var string The statement version. */
     protected $version = null;
 
-    /** @var attachments The statement attachments. */
+    /** @var item_attachment[] The statement attachments. */
     protected $attachments = null;
 
     /** @var additionalfields list of additional fields. */
     private static $additionalsfields = [
-        'context', 'result', 'timestamp', 'stored', 'authority', 'version', 'attachments'
+        'timestamp', 'stored', 'version'
     ];
 
     /**
@@ -97,11 +100,32 @@ class statement implements JsonSerializable {
         $result->set_verb(item_verb::create_from_data($data->verb));
         $result->set_object(item_object::create_from_data($data->object));
 
+        if (isset($data->result)) {
+            $result->set_result(item_result::create_from_data($data->result));
+        }
+
+        if (!empty($data->attachments)) {
+            if (!is_array($data->attachments)) {
+                throw new xapi_exception("Attachments must be an array");
+            }
+            foreach ($data->attachments as $attachment) {
+                $result->add_attachment(item_attachment::create_from_data($attachment));
+            }
+        }
+
+        if (isset($data->context)) {
+            $result->set_context(item_context::create_from_data($data->context));
+        }
+
+        if (isset($data->authority)) {
+            $result->set_authority(item_actor::create_from_data($data->authority));
+        }
+
         // Store other generic xAPI statement fields.
         foreach (self::$additionalsfields as $additional) {
             if (isset($data->$additional)) {
                 $method = 'set_'.$additional;
-                $result->$method(item::create_from_data($data->$additional));
+                $result->$method($data->$additional);
             }
         }
         return $result;
@@ -118,6 +142,18 @@ class statement implements JsonSerializable {
             'verb' => $this->verb,
             'object' => $this->object,
         ];
+        if (!empty($this->result)) {
+            $result->result = $this->result;
+        }
+        if (!empty($this->context)) {
+            $result->context = $this->context;
+        }
+        if (!empty($this->authority)) {
+            $result->authority = $this->authority;
+        }
+        if (!empty($this->attachments)) {
+            $result->attachments = $this->attachments;
+        }
         foreach (self::$additionalsfields as $additional) {
             if (!empty($this->$additional)) {
                 $result->$additional = $this->$additional;
@@ -179,36 +215,36 @@ class statement implements JsonSerializable {
     /**
      * Set the statement context.
      *
-     * @param item $context context item element
+     * @param item_context $context context item element
      */
-    public function set_context(item $context): void {
+    public function set_context(item_context $context): void {
         $this->context = $context;
     }
 
     /**
      * Set the statement result.
      *
-     * @param item $result result item element
+     * @param item_result $result result item element
      */
-    public function set_result(item $result): void {
+    public function set_result(item_result $result): void {
         $this->result = $result;
     }
 
     /**
      * Set the statement timestamp.
      *
-     * @param item $timestamp timestamp item element
+     * @param string $timestamp timestamp element
      */
-    public function set_timestamp(item $timestamp): void {
+    public function set_timestamp(string $timestamp): void {
         $this->timestamp = $timestamp;
     }
 
     /**
      * Set the statement stored.
      *
-     * @param item $stored stored item element
+     * @param string $stored stored element
      */
-    public function set_stored(item $stored): void {
+    public function set_stored(string $stored): void {
         $this->stored = $stored;
     }
 
@@ -217,26 +253,29 @@ class statement implements JsonSerializable {
      *
      * @param item $authority authority item element
      */
-    public function set_authority(item $authority): void {
+    public function set_authority(item_actor $authority): void {
         $this->authority = $authority;
     }
 
     /**
      * Set the statement version.
      *
-     * @param item $version version item element
+     * @param string $version version element
      */
-    public function set_version(item $version): void {
+    public function set_version(string $version): void {
         $this->version = $version;
     }
 
     /**
-     * Set the statement attachments.
+     * Adds and attachment to the statement.
      *
      * @param item $attachments attachments item element
      */
-    public function set_attachments(item $attachments): void {
-        $this->attachments = $attachments;
+    public function add_attachment(item_attachment $attachment): void {
+        if ($this->attachments === null) {
+            $this->attachments = [];
+        }
+        $this->attachments[] = $attachment;
     }
 
     /**
@@ -341,7 +380,7 @@ class statement implements JsonSerializable {
      *
      * @return item|null
      */
-    public function get_context(): ?item {
+    public function get_context(): ?item_context {
         return $this->context;
     }
 
@@ -350,25 +389,25 @@ class statement implements JsonSerializable {
      *
      * @return item|null
      */
-    public function get_result(): ?item {
+    public function get_result(): ?item_result {
         return $this->result;
     }
 
     /**
      * Return the statement timestamp if it is defined.
      *
-     * @return item|null
+     * @return string|null
      */
-    public function get_timestamp(): ?item {
+    public function get_timestamp(): ?string {
         return $this->timestamp;
     }
 
     /**
      * Return the statement stored if it is defined.
      *
-     * @return item|null
+     * @return string|null
      */
-    public function get_stored(): ?item {
+    public function get_stored(): ?string {
         return $this->stored;
     }
 
@@ -377,25 +416,25 @@ class statement implements JsonSerializable {
      *
      * @return item|null
      */
-    public function get_authority(): ?item {
+    public function get_authority(): ?item_actor {
         return $this->authority;
     }
 
     /**
      * Return the statement version if it is defined.
      *
-     * @return item|null
+     * @return string|null
      */
-    public function get_version(): ?item {
+    public function get_version(): ?string {
         return $this->version;
     }
 
     /**
      * Return the statement attachments if it is defined.
      *
-     * @return item|null
+     * @return item_attachment[]|null
      */
-    public function get_attachments(): ?item {
+    public function get_attachments(): ?array {
         return $this->attachments;
     }
 }
diff --git a/lib/xapi/classes/local/statement/item_attachment.php b/lib/xapi/classes/local/statement/item_attachment.php
new file mode 100644 (file)
index 0000000..9994ce6
--- /dev/null
@@ -0,0 +1,73 @@
+<?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/>.
+
+/**
+ * Statement attachment for xAPI structure checking and usage.
+ *
+ * @package    core_xapi
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_xapi\local\statement;
+
+use core_xapi\xapi_exception;
+use core_xapi\iri;
+use stdClass;
+
+/**
+ * Abstract xAPI attachment class.
+ *
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class item_attachment extends item {
+
+    /**
+     * Function to create an item from part of the xAPI statement.
+     *
+     * @param stdClass $data the original xAPI element
+     * @return item item_attachment xAPI generated
+     */
+    public static function create_from_data(stdClass $data): item {
+
+        if (empty($data->usageType)) {
+            throw new xapi_exception("missing attachment usageType");
+        }
+        if (!iri::check($data->usageType)) {
+            throw new xapi_exception("attachment usageType $data->usageType is not a valid IRI");
+        }
+        if (empty($data->display)) {
+            throw new xapi_exception("missing attachment display");
+        }
+        if (empty($data->contentType)) {
+            throw new xapi_exception("missing attachment contentType");
+        }
+        if (empty($data->length)) {
+            throw new xapi_exception("missing attachment length");
+        }
+        if (!is_numeric($data->length)) {
+            throw new xapi_exception("invalid attachment length format");
+        }
+        if (empty($data->sha2)) {
+            throw new xapi_exception("missing attachment sha2");
+        }
+
+        // More required property checks will appear here in the future.
+
+        return new self($data);
+    }
+}
diff --git a/lib/xapi/classes/local/statement/item_context.php b/lib/xapi/classes/local/statement/item_context.php
new file mode 100644 (file)
index 0000000..80a774a
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Statement context for xAPI structure checking and usage.
+ *
+ * @package    core_xapi
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_xapi\local\statement;
+
+use stdClass;
+
+/**
+ * Abstract xAPI context class.
+ *
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class item_context extends item {
+
+    /**
+     * Function to create an item from part of the xAPI statement.
+     *
+     * @param stdClass $data the original xAPI element
+     * @return item item_context xAPI generated
+     */
+    public static function create_from_data(stdClass $data): item {
+
+        // Required property checks will appear here in the future.
+
+        return new self($data);
+    }
+}
diff --git a/lib/xapi/classes/local/statement/item_result.php b/lib/xapi/classes/local/statement/item_result.php
new file mode 100644 (file)
index 0000000..3136103
--- /dev/null
@@ -0,0 +1,107 @@
+<?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/>.
+
+/**
+ * Statement result for xAPI structure checking and usage.
+ *
+ * @package    core_xapi
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_xapi\local\statement;
+
+use core_xapi\xapi_exception;
+use DateInterval;
+use Exception;
+use stdClass;
+
+/**
+ * Abstract xAPI result class.
+ *
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class item_result extends item {
+
+    /** @var int The second of duration if present. */
+    protected $duration;
+
+    /** @var item_score the result score if present. */
+    protected $score;
+
+    /**
+     * Function to create a result from part of the xAPI statement.
+     *
+     * @param stdClass $data the original xAPI element
+     * @param int $duration duration in seconds
+     * @param item_score $score the provided score
+     */
+    protected function __construct(stdClass $data, int $duration = null, item_score $score = null) {
+        parent::__construct($data);
+        $this->duration = $duration;
+        $this->score = $score;
+    }
+
+    /**
+     * Function to create an item from part of the xAPI statement.
+     *
+     * @param stdClass $data the original xAPI element
+     * @return item item_result xAPI generated
+     */
+    public static function create_from_data(stdClass $data): item {
+
+        $duration = null;
+        if (!empty($data->duration)) {
+            try {
+                // Duration uses ISO 8601 format which is ALMOST compatible with PHP DateInterval.
+                // Because we are mesuring human time we get rid of milliseconds, which are not
+                // compatible with DateInterval (More info: https://bugs.php.net/bug.php?id=53831),
+                // all other fractions like "P1.5Y" will throw an exception.
+                $value = preg_replace('/[.,][0-9]*S/', 'S', $data->duration);
+                $interval = new DateInterval($value);
+                $duration = date_create('@0')->add($interval)->getTimestamp();
+            } catch (Exception $e) {
+                throw new xapi_exception('Invalid duration format.');
+            }
+        }
+
+        $score = null;
+        if (!empty($data->score)) {
+            $score = item_score::create_from_data($data->score);
+        }
+
+        return new self($data, $duration, $score);
+    }
+
+    /**
+     * Returns the duration in seconds (if present).
+     *
+     * @return int|null duration in seconds
+     */
+    public function get_duration(): ?int {
+        return $this->duration;
+    }
+
+    /**
+     * Returns the score.
+     *
+     * @return item_score|null the score item
+     */
+    public function get_score(): ?item_score {
+        return $this->score;
+    }
+}
diff --git a/lib/xapi/classes/local/statement/item_score.php b/lib/xapi/classes/local/statement/item_score.php
new file mode 100644 (file)
index 0000000..07e2a2b
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Statement score for xAPI structure checking and usage.
+ *
+ * @package    core_xapi
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_xapi\local\statement;
+
+use stdClass;
+
+/**
+ * Abstract xAPI score class.
+ *
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class item_score extends item {
+
+    /**
+     * Function to create an item from part of the xAPI statement.
+     *
+     * @param stdClass $data the original xAPI element
+     * @return item item_score xAPI generated
+     */
+    public static function create_from_data(stdClass $data): item {
+
+        // Required property checks will appear here in the future.
+
+        return new self($data);
+    }
+}
diff --git a/lib/xapi/tests/local/statement/item_attachment_test.php b/lib/xapi/tests/local/statement/item_attachment_test.php
new file mode 100644 (file)
index 0000000..22483a4
--- /dev/null
@@ -0,0 +1,122 @@
+<?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/>.
+
+/**
+ * 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\local\statement;
+
+use advanced_testcase;
+use core_xapi\iri;
+use core_xapi\xapi_exception;
+
+/**
+ * Contains test cases for testing statement attachment class.
+ *
+ * @package    core_xapi
+ * @since      Moodle 3.9
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class item_attachment_testcase extends advanced_testcase {
+
+    /**
+     * Test item creation.
+     */
+    public function test_create() {
+
+        $data = $this->get_generic_data();
+        $item = item_attachment::create_from_data($data);
+
+        $this->assertEquals(json_encode($item), json_encode($data));
+    }
+
+    /**
+     * return a generic data to create a valid item.
+     *
+     * @return sdtClass the creation data
+     */
+    private function get_generic_data(): \stdClass {
+        return (object) [
+            'usageType' => iri::generate('example', 'attachment'),
+            'display' => (object) [
+                'en-US' => 'Example',
+            ],
+            'description' => (object) [
+                'en-US' => 'Description example',
+            ],
+            "contentType" => "image/jpg",
+            "length" => 1234,
+            "sha2" => "b94c0f1cffb77475c6f1899111a0181efe1d6177"
+        ];
+    }
+
+    /**
+     * Test for invalid values.
+     *
+     * @dataProvider invalid_values_data
+     * @param string $attr attribute to modify
+     * @param mixed $newvalue new value (null means unset)
+     */
+    public function test_invalid_values(string $attr, $newvalue): void {
+
+        $data = $this->get_generic_data();
+        if ($newvalue === null) {
+            unset($data->$attr);
+        } else {
+            $data->$attr = $newvalue;
+        }
+
+        $this->expectException(xapi_exception::class);
+        $item = item_attachment::create_from_data($data);
+    }
+
+    /**
+     * Data provider for the test_invalid_values tests.
+     *
+     * @return  array
+     */
+    public function invalid_values_data() : array {
+        return [
+            'No usageType attachment' => [
+                'usageType', null
+            ],
+            'Invalid usageType attachment' => [
+                'usageType', 'Invalid IRI'
+            ],
+            'No display attachment' => [
+                'display', null
+            ],
+            'No contentType attachment' => [
+                'contentType', null
+            ],
+            'No length attachment' => [
+                'length', null
+            ],
+            'Invalid length attachment' => [
+                'length', 'Invalid'
+            ],
+            'No sha2 attachment' => [
+                'sha2', null
+            ],
+        ];
+    }
+}
diff --git a/lib/xapi/tests/local/statement/item_context_test.php b/lib/xapi/tests/local/statement/item_context_test.php
new file mode 100644 (file)
index 0000000..a12b1c5
--- /dev/null
@@ -0,0 +1,62 @@
+<?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/>.
+
+/**
+ * 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\local\statement;
+
+use advanced_testcase;
+use core_xapi\xapi_exception;
+
+/**
+ * Contains test cases for testing statement context class.
+ *
+ * @package    core_xapi
+ * @since      Moodle 3.9
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class item_context_testcase extends advanced_testcase {
+
+    /**
+     * Test item creation.
+     */
+    public function test_create() {
+
+        $data = $this->get_generic_data();
+        $item = item_context::create_from_data($data);
+
+        $this->assertEquals(json_encode($item), json_encode($data));
+    }
+
+    /**
+     * Return a generic data to create a valid item.
+     *
+     * @return sdtClass the creation data
+     */
+    private function get_generic_data(): \stdClass {
+        // For now context has no data validation so a generic data is enough.
+        return (object) [
+            'usageType' => '51a6f860-1997-11e3-8ffd-0800200c9a66',
+        ];
+    }
+}
diff --git a/lib/xapi/tests/local/statement/item_result_test.php b/lib/xapi/tests/local/statement/item_result_test.php
new file mode 100644 (file)
index 0000000..a75ea9f
--- /dev/null
@@ -0,0 +1,138 @@
+<?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/>.
+
+/**
+ * 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\local\statement;
+
+use advanced_testcase;
+use core_xapi\xapi_exception;
+
+/**
+ * Contains test cases for testing statement result class.
+ *
+ * @package    core_xapi
+ * @since      Moodle 3.9
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class item_result_testcase extends advanced_testcase {
+
+    /**
+     * Test item creation.
+     */
+    public function test_creation(): void {
+
+        $data = $this->get_generic_data();
+        $item = item_result::create_from_data($data);
+
+        $this->assertEquals(json_encode($item), json_encode($data));
+        $this->assertNull($item->get_duration());
+
+        $score = $item->get_score();
+        $this->assertEquals(json_encode($score), json_encode($data->score));
+
+    }
+
+    /**
+     * Return a generic data to create a valid item.
+     *
+     * @return sdtClass the creation data
+     */
+    private function get_generic_data(): \stdClass {
+        return (object) [
+            'score' => (object)[
+                'min' => 0,
+                'max' => 100,
+                'raw' => 50,
+                'scaled' => 0.5,
+            ],
+            'completion' => true,
+            'success' => true,
+        ];
+    }
+
+    /**
+     * Test for duration values.
+     *
+     * @dataProvider duration_values_data
+     * @param string|null $duration specified duration
+     * @param int|null $seconds calculated seconds
+     * @param bool $exception if exception is expected
+     */
+    public function test_duration_values(?string $duration, ?int $seconds, bool $exception): void {
+
+        if ($exception) {
+            $this->expectException(xapi_exception::class);
+        }
+
+        $data = $this->get_generic_data();
+        if ($duration !== null) {
+            $data->duration = $duration;
+        }
+        $item = item_result::create_from_data($data);
+        $this->assertEquals($seconds, $item->get_duration());
+    }
+
+    /**
+     * Data provider for the test_duration_values tests.
+     *
+     * @return array
+     */
+    public function duration_values_data() : array {
+        return [
+            'No duration' => [
+                null, null, false
+            ],
+            'Empty duration' => [
+                '', null, false
+            ],
+            '1 minute duration' => [
+                'PT1M', 60, false
+            ],
+            '1 hour duration' => [
+                'PT1H', 3600, false
+            ],
+            '1 second duration' => [
+                'PT1S', 1, false
+            ],
+            '1.11 second duration (dot variant)' => [
+                'PT1.11S', 1, false
+            ],
+            '1,11 second duration (comma variant)' => [
+                'PT1.11S', 1, false
+            ],
+            '90 minutes 5 seconds duration' => [
+                'PT1H30M5S', 5405, false
+            ],
+            '90 minutes 05 seconds duration' => [
+                'PT1H30M05S', 5405, false
+            ],
+            'Half year duration' => [
+                'P0.5Y', null, true
+            ],
+            'Incorrect format' => [
+                'INVALID', null, true
+            ],
+        ];
+    }
+}
diff --git a/lib/xapi/tests/local/statement/item_score_test.php b/lib/xapi/tests/local/statement/item_score_test.php
new file mode 100644 (file)
index 0000000..73515e0
--- /dev/null
@@ -0,0 +1,56 @@
+<?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/>.
+
+/**
+ * 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\local\statement;
+
+use advanced_testcase;
+use core_xapi\xapi_exception;
+
+/**
+ * Contains test cases for testing statement score class.
+ *
+ * @package    core_xapi
+ * @since      Moodle 3.9
+ * @copyright  2020 Ferran Recio
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class item_score_testcase extends advanced_testcase {
+
+    /**
+     * Test item creation.
+     */
+    public function test_create() {
+
+        $data = (object) [
+            'scaled' => 0.5,
+            'raw' => 5,
+            'min' => 0,
+            'max' => 10,
+        ];
+        $item = item_score::create_from_data($data);
+
+        $this->assertEquals(json_encode($item), json_encode($data));
+
+    }
+}
index 04317b7..84314d1 100644 (file)
@@ -31,6 +31,10 @@ use core_xapi\local\statement\item_activity;
 use core_xapi\local\statement\item_verb;
 use core_xapi\local\statement\item_agent;
 use core_xapi\local\statement\item_group;
+use core_xapi\local\statement\item_result;
+use core_xapi\local\statement\item_attachment;
+use core_xapi\local\statement\item_context;
+use core_xapi\iri;
 use core_xapi\xapi_exception;
 use advanced_testcase;
 use stdClass;
@@ -47,14 +51,63 @@ defined('MOODLE_INTERNAL') || die();
  */
 class statement_testcase extends advanced_testcase {
 
+    /**
+     * Returns a valid item for a specific attribute.
+     *
+     * @param string $itemname statement item name
+     * @return item the resulting item
+     */
+    private function get_valid_item(string $itemname): item {
+        global $USER, $CFG;
+        switch ($itemname) {
+            case 'attachments':
+            case 'attachment':
+                $data = (object) [
+                    'usageType' => iri::generate('example', 'attachment'),
+                    'display' => (object) [
+                        'en-US' => 'Example',
+                    ],
+                    'description' => (object) [
+                        'en-US' => 'Description example',
+                    ],
+                    "contentType" => "image/jpg",
+                    "length" => 1234,
+                    "sha2" => "b94c0f1cffb77475c6f1899111a0181efe1d6177"
+                ];
+                return item_attachment::create_from_data($data);
+            case 'authority':
+                $data = (object) [
+                    'objectType' => 'Agent',
+                    'account' => (object) [
+                        'homePage' => $CFG->wwwroot,
+                        'name' => $USER->id,
+                    ],
+                ];
+                return item_agent::create_from_data($data);
+        }
+        // For now, the rest of the optional properties have no validation
+        // so we create a standard stdClass for all of them.
+        $data = (object)[
+            'some' => 'data',
+        ];
+        $classname = 'core_xapi\local\statement\item_'.$itemname;
+        if (class_exists($classname)) {
+            $item = $classname::create_from_data($data);
+        } else {
+            $item = item::create_from_data($data);
+        }
+        return $item;
+    }
+
     /**
      * Test statement creation.
      *
      * @dataProvider create_provider
      * @param bool $useagent if use agent as actor (or group if false)
-     * @param array $extras
+     * @param array $extras extra item elements
+     * @param array $extravalues extra string values
      */
-    public function test_create(bool $useagent, array $extras) {
+    public function test_create(bool $useagent, array $extras, array $extravalues) {
 
         $this->resetAfterTest();
 
@@ -65,6 +118,8 @@ class statement_testcase extends advanced_testcase {
         $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
         $this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user->id));
 
+        $this->setUser($user);
+
         // Our statement.
         $statement = new statement();
 
@@ -76,14 +131,17 @@ class statement_testcase extends advanced_testcase {
         }
         $statement->set_verb(item_verb::create_from_id('cook'));
         $statement->set_object(item_activity::create_from_id('paella'));
-        // For now, the rest of the optional properties have no validation
-        // so we create a standard stdClass for all of them.
-        $data = (object)[
-            'some' => 'data',
-        ];
+
         foreach ($extras as $extra) {
             $method = 'set_'.$extra;
-            $statement->$method(item::create_from_data($data));
+            $item = $this->get_valid_item($extra);
+            $statement->$method($item);
+        }
+
+        // For now extra values have no validation.
+        foreach ($extravalues as $extra) {
+            $method = 'set_'.$extra;
+            $statement->$method('Example');
         }
 
         // Check resulting statement.
@@ -110,10 +168,11 @@ class statement_testcase extends advanced_testcase {
         $this->assertNotEmpty($data->verb);
         $this->assertNotEmpty($data->object);
         $allextras = ['context', 'result', 'timestamp', 'stored', 'authority', 'version', 'attachments'];
+        $alldefined = array_merge($extras, $extravalues);
         foreach ($allextras as $extra) {
-            if (in_array($extra, $extras)) {
+            if (in_array($extra, $alldefined)) {
                 $this->assertObjectHasAttribute($extra, $data);
-                $this->assertNotEmpty($data->object);
+                $this->assertNotEmpty($data->$extra);
             } else {
                 $this->assertObjectNotHasAttribute($extra, $data);
             }
@@ -128,52 +187,46 @@ class statement_testcase extends advanced_testcase {
     public function create_provider() : array {
         return [
             'Agent statement with no extras' => [
-                true, []
+                true, [], []
             ],
             'Agent statement with context' => [
-                true, ['context']
+                true, ['context'], []
             ],
             'Agent statement with result' => [
-                true, ['result']
+                true, ['result'], []
             ],
             'Agent statement with timestamp' => [
-                true, ['timestamp']
+                true, [], ['timestamp']
             ],
             'Agent statement with stored' => [
-                true, ['stored']
+                true, [], ['stored']
             ],
             'Agent statement with authority' => [
-                true, ['authority']
+                true, ['authority'], []
             ],
             'Agent statement with version' => [
-                true, ['version']
-            ],
-            'Agent statement with attachments' => [
-                true, ['attachments']
+                true, [], ['version']
             ],
             'Group statement with no extras' => [
-                false, []
+                false, [], []
             ],
             'Group statement with context' => [
-                false, ['context']
+                false, ['context'], []
             ],
             'Group statement with result' => [
-                false, ['result']
+                false, ['result'], []
             ],
             'Group statement with timestamp' => [
-                false, ['timestamp']
+                false, [], ['timestamp']
             ],
             'Group statement with stored' => [
-                false, ['stored']
+                false, [], ['stored']
             ],
             'Group statement with authority' => [
-                false, ['authority']
+                false, ['authority'], []
             ],
             'Group statement with version' => [
-                false, ['version']
-            ],
-            'Group statement with attachments' => [
-                false, ['attachments']
+                false, [], ['version']
             ],
         ];
     }
@@ -183,9 +236,10 @@ class statement_testcase extends advanced_testcase {
      *
      * @dataProvider create_provider
      * @param bool $useagent if use agent as actor (or group if false)
-     * @param array $extras
+     * @param array $extras extra item elements
+     * @param array $extravalues extra string values
      */
-    public function test_create_from_data(bool $useagent, array $extras) {
+    public function test_create_from_data(bool $useagent, array $extras, array $extravalues) {
         $this->resetAfterTest();
 
         // Create one course with a group.
@@ -195,6 +249,8 @@ class statement_testcase extends advanced_testcase {
         $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
         $this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user->id));
 
+        $this->setUser($user);
+
         // Populate data.
         if ($useagent) {
             $actor = item_agent::create_from_user($user);
@@ -209,13 +265,15 @@ class statement_testcase extends advanced_testcase {
             'verb' => $verb->get_data(),
             'object' => $object->get_data(),
         ];
-        // For now, the rest of the optional properties have no validation
-        // so we create a standard stdClass for all of them.
-        $info = (object)[
-            'some' => 'data',
-        ];
+
         foreach ($extras as $extra) {
-            $data->$extra = $info;
+            $item = $this->get_valid_item($extra);
+            $data->$extra = $item->get_data();
+        }
+
+        // For now extra values have no validation.
+        foreach ($extravalues as $extra) {
+            $data->$extra = 'Example';
         }
 
         $statement = statement::create_from_data($data);
@@ -244,8 +302,9 @@ class statement_testcase extends advanced_testcase {
         $this->assertNotEmpty($data->verb);
         $this->assertNotEmpty($data->object);
         $allextras = ['context', 'result', 'timestamp', 'stored', 'authority', 'version', 'attachments'];
+        $alldefined = array_merge($extras, $extravalues);
         foreach ($allextras as $extra) {
-            if (in_array($extra, $extras)) {
+            if (in_array($extra, $alldefined)) {
                 $this->assertObjectHasAttribute($extra, $data);
                 $this->assertNotEmpty($data->object);
             } else {
@@ -254,6 +313,85 @@ class statement_testcase extends advanced_testcase {
         }
     }
 
+    /**
+     * Test adding attachments to statement.
+     *
+     */
+    public function test_add_attachment() {
+
+        // Our statement.
+        $statement = new statement();
+
+        $attachments = $statement->get_attachments();
+        $this->assertNull($attachments);
+
+        $item = $this->get_valid_item('attachment');
+        $itemdata = $item->get_data();
+        $statement->add_attachment($item);
+
+        $attachments = $statement->get_attachments();
+        $this->assertNotNull($attachments);
+        $this->assertCount(1, $attachments);
+
+        $attachment = current($attachments);
+        $attachmentdata = $attachment->get_data();
+        $this->assertEquals($itemdata->usageType, $attachmentdata->usageType);
+        $this->assertEquals($itemdata->length, $attachmentdata->length);
+
+        // Check resulting json.
+        $statementdata = json_decode(json_encode($statement));
+        $this->assertObjectHasAttribute('attachments', $statementdata);
+        $this->assertNotEmpty($statementdata->attachments);
+        $this->assertCount(1, $statementdata->attachments);
+    }
+
+    /**
+     * Test adding attachments to statement.
+     *
+     */
+    public function test_add_attachment_from_data() {
+
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        $actor = item_agent::create_from_user($user);
+        $verb = item_verb::create_from_id('cook');
+        $object = item_activity::create_from_id('paella');
+
+        $data = (object) [
+            'actor' => $actor->get_data(),
+            'verb' => $verb->get_data(),
+            'object' => $object->get_data(),
+        ];
+
+        $item = $this->get_valid_item('attachment');
+        $itemdata = $item->get_data();
+        $data->attachments = [$itemdata];
+
+        $statement = statement::create_from_data($data);
+
+        $attachments = $statement->get_attachments();
+        $this->assertNotNull($attachments);
+        $this->assertCount(1, $attachments);
+
+        $attachment = current($attachments);
+        $attachmentdata = $attachment->get_data();
+        $this->assertEquals($itemdata->usageType, $attachmentdata->usageType);
+        $this->assertEquals($itemdata->length, $attachmentdata->length);
+
+        $statementdata = json_decode(json_encode($statement));
+        $this->assertObjectHasAttribute('attachments', $statementdata);
+        $this->assertNotEmpty($statementdata->attachments);
+        $this->assertCount(1, $statementdata->attachments);
+
+        // Now try to send an invalid attachments.
+        $this->expectException(xapi_exception::class);
+        $data->attachments = 'Invalid data';
+        $statement = statement::create_from_data($data);
+    }
+
     /**
      * Test all getters into a not set statement.
      *
@@ -415,6 +553,8 @@ class statement_testcase extends advanced_testcase {
 
         $user = $this->getDataGenerator()->create_user();
 
+        $this->setUser($user);
+
         // Our statement.
         $statement = new statement();
 
@@ -422,16 +562,13 @@ class statement_testcase extends advanced_testcase {
         $statement->set_actor(item_agent::create_from_user($user));
         $statement->set_verb(item_verb::create_from_id('cook'));
         $statement->set_object(item_activity::create_from_id('paella'));
-        // For now, the rest of the optional properties have no validation
-        // so we create a standard stdClass for all of them.
-        $data = (object)[
-            'some' => 'data',
-        ];
-        $extras = ['context', 'result', 'timestamp', 'stored', 'authority', 'version', 'attachments'];
-        foreach ($extras as $extra) {
-            $method = 'set_'.$extra;
-            $statement->$method(item::create_from_data($data));
-        }
+        $statement->set_result($this->get_valid_item('result'));
+        $statement->set_context($this->get_valid_item('context'));
+        $statement->set_authority($this->get_valid_item('authority'));
+        $statement->add_attachment($this->get_valid_item('attachment'));
+        $statement->set_version('Example');
+        $statement->set_timestamp('Example');
+        $statement->set_stored('Example');
 
         $min = $statement->minify();
 
index b414806..43dfa1b 100644 (file)
@@ -31,6 +31,7 @@ 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 stdClass;
 
 defined('MOODLE_INTERNAL') || die();
@@ -335,7 +336,7 @@ class attempt_testcase extends \advanced_testcase {
         }
         $statement->set_object(item_activity::create_from_id('something', $definition));
         if ($hasresult) {
-            $statement->set_result(item::create_from_data((object)[
+            $statement->set_result(item_result::create_from_data((object)[
                 'completion' => true,
                 'success' => true,
                 'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1],
index c661e24..0594566 100644 (file)
@@ -31,6 +31,7 @@ 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 stdClass;
 
@@ -176,7 +177,7 @@ class handler_testcase extends \advanced_testcase {
             $statement->set_object(item_activity::create_from_id('paella', $definition));
         }
         if ($hasresult) {
-            $statement->set_result(item::create_from_data((object)[
+            $statement->set_result(item_result::create_from_data((object)[
                 'completion' => true,
                 'success' => true,
                 'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1],
@@ -302,7 +303,7 @@ class handler_testcase extends \advanced_testcase {
             'correctResponsesPattern' => '1',
         ]);
         $statement->set_object(item_activity::create_from_id($context->id, $definition));
-        $statement->set_result(item::create_from_data((object)[
+        $statement->set_result(item_result::create_from_data((object)[
             'completion' => true,
             'success' => true,
             'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1],
@@ -317,7 +318,7 @@ class handler_testcase extends \advanced_testcase {
             'correctResponsesPattern' => '1',
         ]);
         $statement->set_object(item_activity::create_from_id($context->id.'?subContentId=111-222-333', $definition));
-        $statement->set_result(item::create_from_data((object)[
+        $statement->set_result(item_result::create_from_data((object)[
             'completion' => true,
             'success' => true,
             'score' => (object) ['min' => 0, 'max' => 1, 'raw' => 0, 'scaled' => 0],