From 72a2335151ae9cd4ca3e8c13bc1d6959ae2a5de5 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Wed, 8 Apr 2020 07:06:01 +0200 Subject: [PATCH] MDL-68379 core_xapi: adding result, attachments and context to statement --- lib/xapi/classes/local/statement.php | 111 ++++++--- .../local/statement/item_attachment.php | 73 ++++++ .../classes/local/statement/item_context.php | 49 ++++ .../classes/local/statement/item_result.php | 107 ++++++++ .../classes/local/statement/item_score.php | 49 ++++ .../local/statement/item_attachment_test.php | 122 +++++++++ .../local/statement/item_context_test.php | 62 +++++ .../local/statement/item_result_test.php | 138 ++++++++++ .../tests/local/statement/item_score_test.php | 56 +++++ lib/xapi/tests/local/statement_test.php | 235 ++++++++++++++---- 10 files changed, 917 insertions(+), 85 deletions(-) create mode 100644 lib/xapi/classes/local/statement/item_attachment.php create mode 100644 lib/xapi/classes/local/statement/item_context.php create mode 100644 lib/xapi/classes/local/statement/item_result.php create mode 100644 lib/xapi/classes/local/statement/item_score.php create mode 100644 lib/xapi/tests/local/statement/item_attachment_test.php create mode 100644 lib/xapi/tests/local/statement/item_context_test.php create mode 100644 lib/xapi/tests/local/statement/item_result_test.php create mode 100644 lib/xapi/tests/local/statement/item_score_test.php diff --git a/lib/xapi/classes/local/statement.php b/lib/xapi/classes/local/statement.php index 7314d4b8c3c..af69b5a5587 100644 --- a/lib/xapi/classes/local/statement.php +++ b/lib/xapi/classes/local/statement.php @@ -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 index 00000000000..9994ce646cc --- /dev/null +++ b/lib/xapi/classes/local/statement/item_attachment.php @@ -0,0 +1,73 @@ +. + +/** + * 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 index 00000000000..80a774acc97 --- /dev/null +++ b/lib/xapi/classes/local/statement/item_context.php @@ -0,0 +1,49 @@ +. + +/** + * 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 index 00000000000..31361039c8f --- /dev/null +++ b/lib/xapi/classes/local/statement/item_result.php @@ -0,0 +1,107 @@ +. + +/** + * 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 index 00000000000..07e2a2b0156 --- /dev/null +++ b/lib/xapi/classes/local/statement/item_score.php @@ -0,0 +1,49 @@ +. + +/** + * 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 index 00000000000..22483a44807 --- /dev/null +++ b/lib/xapi/tests/local/statement/item_attachment_test.php @@ -0,0 +1,122 @@ +. + +/** + * 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 index 00000000000..a12b1c5b187 --- /dev/null +++ b/lib/xapi/tests/local/statement/item_context_test.php @@ -0,0 +1,62 @@ +. + +/** + * 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 index 00000000000..a75ea9ff5e9 --- /dev/null +++ b/lib/xapi/tests/local/statement/item_result_test.php @@ -0,0 +1,138 @@ +. + +/** + * 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 index 00000000000..73515e006ce --- /dev/null +++ b/lib/xapi/tests/local/statement/item_score_test.php @@ -0,0 +1,56 @@ +. + +/** + * 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)); + + } +} diff --git a/lib/xapi/tests/local/statement_test.php b/lib/xapi/tests/local/statement_test.php index 04317b79ce4..84314d1af41 100644 --- a/lib/xapi/tests/local/statement_test.php +++ b/lib/xapi/tests/local/statement_test.php @@ -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();