From 70446a537325008709dd22dd8779033ae4d687bd Mon Sep 17 00:00:00 2001 From: Milton Reder Date: Mon, 4 Nov 2024 11:53:32 -0500 Subject: [PATCH] Mod choice (#50) * mod choice first crack needs slugged ids tho * slug choice IDs * correct doc --- .../events/mod_choice/answer_created.php | 74 +++++++++++++++ src/transformer/get_event_function_map.php | 1 + .../utils/get_activity/course_module.php | 26 +++-- .../utils/get_activity/definition/choice.php | 81 ++++++++++++++++ src/transformer/utils/slugify.php | 48 ++++++++++ .../answer_created/answer_created_test.php | 70 ++++++++++++++ tests/mod_choice/answer_created/data.json | 34 +++++++ tests/mod_choice/answer_created/event.json | 10 ++ .../mod_choice/answer_created/statements.json | 95 +++++++++++++++++++ .../existing_module/data.json | 22 ++++- .../existing_module/statements.json | 25 ++++- 11 files changed, 475 insertions(+), 11 deletions(-) create mode 100644 src/transformer/events/mod_choice/answer_created.php create mode 100644 src/transformer/utils/get_activity/definition/choice.php create mode 100644 src/transformer/utils/slugify.php create mode 100644 tests/mod_choice/answer_created/answer_created_test.php create mode 100644 tests/mod_choice/answer_created/data.json create mode 100644 tests/mod_choice/answer_created/event.json create mode 100644 tests/mod_choice/answer_created/statements.json diff --git a/src/transformer/events/mod_choice/answer_created.php b/src/transformer/events/mod_choice/answer_created.php new file mode 100644 index 000000000..bed1a0667 --- /dev/null +++ b/src/transformer/events/mod_choice/answer_created.php @@ -0,0 +1,74 @@ +. + +/** + * Transformer fn for answer created event. + * + * @package logstore_xapi + * @copyright Milt Reder + * + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace src\transformer\events\mod_choice; + +use src\transformer\utils as utils; + +/** + * Transformer fn for answer created event. + * + * @param array $config The transformer config settings. + * @param \stdClass $event The event to be transformed. + * @return array + */ + +function answer_created(array $config, \stdClass $event) { + $repo = $config['repo']; + $user = $repo->read_record_by_id('user', $event->userid); + $course = $repo->read_record_by_id('course', $event->courseid); + $answer = $repo->read_record_by_id('choice_answers', $event->objectid); + $option = $repo->read_record_by_id('choice_options', $answer->optionid); + $lang = utils\get_course_lang($course); + + return [[ + 'actor' => utils\get_user($config, $user), + 'verb' => [ + 'id' => 'http://adlnet.gov/expapi/verbs/answered', + 'display' => [ + $lang => 'Answered' + ], + ], + 'object' => utils\get_activity\course_module( + $config, $course, $event->contextinstanceid + ), + 'result' => [ + 'response' => $option->text, + ], + 'context' => [ + 'language' => $lang, + 'extensions' => utils\extensions\base($config, $event, null), + 'contextActivities' => [ + 'parent' => utils\context_activities\get_parent( + $config, + $event->contextinstanceid + ), + 'category' => [ + utils\get_activity\site($config), + ], + ], + ] + ]]; +} diff --git a/src/transformer/get_event_function_map.php b/src/transformer/get_event_function_map.php index 4aee3b81c..fea5d9247 100644 --- a/src/transformer/get_event_function_map.php +++ b/src/transformer/get_event_function_map.php @@ -88,6 +88,7 @@ function get_event_function_map() { '\mod_book\event\chapter_created' => 'mod_book\chapter_created', '\mod_chat\event\course_module_viewed' => 'all\course_module_viewed', '\mod_choice\event\course_module_viewed' => 'all\course_module_viewed', + '\mod_choice\event\answer_created' => 'mod_choice\answer_created', '\mod_data\event\course_module_viewed' => 'all\course_module_viewed', '\mod_facetoface\event\cancel_booking' => 'mod_facetoface\cancel_booking', '\mod_facetoface\event\course_module_viewed' => 'all\course_module_viewed', diff --git a/src/transformer/utils/get_activity/course_module.php b/src/transformer/utils/get_activity/course_module.php index 410a4da71..b832ef388 100644 --- a/src/transformer/utils/get_activity/course_module.php +++ b/src/transformer/utils/get_activity/course_module.php @@ -52,16 +52,26 @@ function course_module(array $config, \stdClass $course, int $cmid) { utils\is_enabled_config($config, 'send_jisc_data') ); - // TODO: Some objects (like mod_choice CMI interactions) will need more - // dispatch and add those here + // default definition + $def = [ + 'type' => $activitytype, + 'name' => [ + $courselang => $instancename, + ], + ]; + + // process special cases + + // Choice + if ($module->name === 'choice') { + $def = utils\get_activity\definition\choice\get_choice_definition( + $config, $instance, $courselang + ); + } + $object = [ 'id' => $coursemoduleurl, - 'definition' => [ - 'type' => $activitytype, - 'name' => [ - $courselang => $instancename, - ], - ], + 'definition' => $def, ]; if (utils\is_enabled_config($config, 'send_course_and_module_idnumber')) { diff --git a/src/transformer/utils/get_activity/definition/choice.php b/src/transformer/utils/get_activity/definition/choice.php new file mode 100644 index 000000000..50280b527 --- /dev/null +++ b/src/transformer/utils/get_activity/definition/choice.php @@ -0,0 +1,81 @@ +. + +/** + * Transformer utilities for creating Choice xAPI Activity object definitions. + * + * @package logstore_xapi + * @copyright Milt Reder + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace src\transformer\utils\get_activity\definition\choice; + +use src\transformer\utils as utils; + +/** + * Transformer util for creating choice definitions + * + * @param array $config The transformer config settings. + * @param \stdClass $choice The choice object. + * @param string $lang The language. + */ +function get_choice_definition( + array $config, + \stdClass $choice, + string $lang +) { + $repo = $config['repo']; + $options = $repo->read_records( + 'choice_options', ['choiceid' => $choice->id], 'id ASC' + ); + + return [ + 'type' => 'http://adlnet.gov/expapi/activities/cmi.interaction', + 'name' => [ + $lang => $choice->name, + ], + 'description' => [ + $lang => utils\get_string_html_removed($choice->intro), + ], + 'interactionType' => 'choice', + 'correctResponsesPattern' => [ + implode( + '[,]', + array_map( + function($option) { + return $option->text; + }, + $options + ) + ), + ], + // use array values because this sometimes comes out associative + 'choices' => array_values( + array_map( + function($option) use ($lang) { + return [ + 'id' => utils\slugify($option->text), + 'description' => [ + $lang => $option->text, + ], + ]; + }, + $options + ) + ) + ]; +} diff --git a/src/transformer/utils/slugify.php b/src/transformer/utils/slugify.php new file mode 100644 index 000000000..1ad720257 --- /dev/null +++ b/src/transformer/utils/slugify.php @@ -0,0 +1,48 @@ +. + +/** + * Utility to make human-readable but id-safe strings. + * + * @package logstore_xapi + * @copyright Milt Reder + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace src\transformer\utils; + +/** + * Transformer utility that converts a given string into a URL-friendly "slug". + * + * @param string $string The input string to be converted into a slug. + * @return string The URL-friendly slug created from the input string. + */ + +function slugify($string) { + // Convert the string to lowercase + $string = strtolower($string); + + // Replace spaces and consecutive whitespace with a single dash + $string = preg_replace('/\s+/', '-', $string); + + // Remove any non-alphanumeric characters except dashes + $string = preg_replace('/[^a-z0-9-]/', '', $string); + + // Trim any trailing or leading dashes + $string = trim($string, '-'); + + return $string; +} diff --git a/tests/mod_choice/answer_created/answer_created_test.php b/tests/mod_choice/answer_created/answer_created_test.php new file mode 100644 index 000000000..5a9d77626 --- /dev/null +++ b/tests/mod_choice/answer_created/answer_created_test.php @@ -0,0 +1,70 @@ +. + +namespace logstore_xapi\mod_choice\answer_created; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/admin/tool/log/store/xapi/tests/xapi_test_case.php'); + +/** + * Unit test for answer_created event + * + * @package logstore_xapi + * @copyright Milt Reder + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class answer_created_test extends \logstore_xapi\xapi_test_case { + + /** + * Retrieve the directory of the unit test. + * + * @return string + */ + protected function get_test_dir() { + return __DIR__; + } + + /** + * Retrieve the plugin type being tested. + * + * @return string + */ + protected function get_plugin_type() { + return "core"; + } + + /** + * Retrieve the plugin name being tested. + * + * @return string + */ + protected function get_plugin_name() { + return "mod_choice"; + } + + /** + * Appease auto-detecting of test cases. xapi_test_case has default test cases. + * + * @covers ::answer_created + * @return void + */ + public function test_init() { + + } +} diff --git a/tests/mod_choice/answer_created/data.json b/tests/mod_choice/answer_created/data.json new file mode 100644 index 000000000..e38bf7d5f --- /dev/null +++ b/tests/mod_choice/answer_created/data.json @@ -0,0 +1,34 @@ +{ + "modules": [ + { + "id": 1, + "name": "choice" + } + ], + "choice": [ + { + "id": 1, + "name": "To be or not to be?", + "intro": "

That is the question.

" + } + ], + "choice_options": [ + { + "id": 1, + "choiceid": 1, + "text": "To be" + }, + { + "id": 2, + "choiceid": 1, + "text": "Not to be" + } + ], + "choice_answers": [ + { + "id": 1, + "choiceid": 1, + "optionid": 1 + } + ] +} diff --git a/tests/mod_choice/answer_created/event.json b/tests/mod_choice/answer_created/event.json new file mode 100644 index 000000000..1e24970d0 --- /dev/null +++ b/tests/mod_choice/answer_created/event.json @@ -0,0 +1,10 @@ +{ + "courseid": 1, + "contextinstanceid": 1, + "eventname": "\\mod_choice\\event\\answer_created", + "id": 1, + "objectid": 1, + "objecttable": "choice_answers", + "timecreated": 1433946701, + "userid": 1 +} diff --git a/tests/mod_choice/answer_created/statements.json b/tests/mod_choice/answer_created/statements.json new file mode 100644 index 000000000..b3b3c6be6 --- /dev/null +++ b/tests/mod_choice/answer_created/statements.json @@ -0,0 +1,95 @@ +[ + { + "actor": { + "account": { + "homePage": "http://www.example.org", + "name": "1" + }, + "name": "test_fullname" + }, + "verb": { + "display": { + "en": "Answered" + }, + "id": "http://adlnet.gov/expapi/verbs/answered" + }, + "object": { + "id": "http://www.example.org/mod/choice/view.php?id=1", + "definition": { + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "name": { + "en": "To be or not to be?" + }, + "description": { + "en": "That is the question." + }, + "interactionType": "choice", + "correctResponsesPattern": [ + "To be[,]Not to be" + ], + "choices": [ + { + "id": "to-be", + "description": { + "en": "To be" + } + }, + { + "id": "not-to-be", + "description": { + "en": "Not to be" + } + } + ] + } + }, + "result": { + "response": "To be" + }, + "context": { + "contextActivities": { + "category": [ + { + "id": "http://www.example.org", + "definition": { + "type": "http://id.tincanapi.com/activitytype/lms", + "name": { + "en": "test_name" + } + } + } + ], + "parent": [ + { + "id": "http://www.example.org/course/section.php?id=1", + "objectType": "Activity", + "definition": { + "name": { + "en": "test_name Section 0" + }, + "type": "http://id.tincanapi.com/activitytype/section" + } + }, + { + "id": "http://www.example.org/course/view.php?id=1", + "definition": { + "type": "https://w3id.org/xapi/cmi5/activitytype/course", + "name": { + "en": "test_name" + } + } + } + ] + }, + "extensions": { + "http://lrs.learninglocker.net/define/extensions/info": { + "event_function": "\\src\\transformer\\events\\mod_choice\\answer_created", + "event_name": "\\mod_choice\\event\\answer_created", + "http://moodle.org": "1.0.0", + "https://github.com/xAPI-vle/moodle-logstore_xapi": "0.0.0-development" + } + }, + "language": "en" + } + } +] diff --git a/tests/mod_choice/course_module_viewed/existing_module/data.json b/tests/mod_choice/course_module_viewed/existing_module/data.json index 8870fbdc3..e38bf7d5f 100644 --- a/tests/mod_choice/course_module_viewed/existing_module/data.json +++ b/tests/mod_choice/course_module_viewed/existing_module/data.json @@ -8,7 +8,27 @@ "choice": [ { "id": 1, - "name": "test_name" + "name": "To be or not to be?", + "intro": "

That is the question.

" + } + ], + "choice_options": [ + { + "id": 1, + "choiceid": 1, + "text": "To be" + }, + { + "id": 2, + "choiceid": 1, + "text": "Not to be" + } + ], + "choice_answers": [ + { + "id": 1, + "choiceid": 1, + "optionid": 1 } ] } diff --git a/tests/mod_choice/course_module_viewed/existing_module/statements.json b/tests/mod_choice/course_module_viewed/existing_module/statements.json index faa115259..52d04a848 100644 --- a/tests/mod_choice/course_module_viewed/existing_module/statements.json +++ b/tests/mod_choice/course_module_viewed/existing_module/statements.json @@ -18,8 +18,29 @@ "definition": { "type": "http://adlnet.gov/expapi/activities/cmi.interaction", "name": { - "en": "test_name" - } + "en": "To be or not to be?" + }, + "description": { + "en": "That is the question." + }, + "interactionType": "choice", + "correctResponsesPattern": [ + "To be[,]Not to be" + ], + "choices": [ + { + "id": "to-be", + "description": { + "en": "To be" + } + }, + { + "id": "not-to-be", + "description": { + "en": "Not to be" + } + } + ] } }, "context": {