From 0b01f6f391598a85ed46ac208c2c3a7226ca7887 Mon Sep 17 00:00:00 2001 From: sarjona Date: Mon, 23 Apr 2018 07:58:33 +0200 Subject: [PATCH 1/5] MDL-61959 scormreport_basic: Implement privacy API --- .../report/basic/classes/privacy/provider.php | 92 +++++++++++++++++++ .../basic/lang/en/scormreport_basic.php | 2 + mod/scorm/report/basic/tests/privacy_test.php | 85 +++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 mod/scorm/report/basic/classes/privacy/provider.php create mode 100644 mod/scorm/report/basic/tests/privacy_test.php diff --git a/mod/scorm/report/basic/classes/privacy/provider.php b/mod/scorm/report/basic/classes/privacy/provider.php new file mode 100644 index 00000000000..1f0c72c6c0a --- /dev/null +++ b/mod/scorm/report/basic/classes/privacy/provider.php @@ -0,0 +1,92 @@ +. + +/** + * Privacy Subsystem implementation for scormreport_basic. + * + * @package scormreport_basic + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace scormreport_basic\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\transform; +use \core_privacy\local\request\writer; + +/** + * Privacy Subsystem for scormreport_basic. + * + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\user_preference_provider { + + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised item collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) : collection { + // User preferences shared between different scorm reports. + $collection->add_user_preference('scorm_report_pagesize', 'privacy:metadata:preference:scorm_report_pagesize'); + + // User preferences specific for this scorm report. + $collection->add_user_preference('scorm_report_detailed', 'privacy:metadata:preference:scorm_report_detailed'); + + return $collection; + } + + /** + * Store all user preferences for the plugin. + * + * @param int $userid The userid of the user whose data is to be exported. + */ + public static function export_user_preferences(int $userid) { + static::get_and_export_user_preference($userid, 'scorm_report_pagesize'); + static::get_and_export_user_preference($userid, 'scorm_report_detailed', true); + } + + /** + * Get and export a user preference. + * + * @param int $userid The userid of the user whose data is to be exported. + * @param string $userpreference The user preference to export. + * @param boolean $transform If true, transform value to yesno. + */ + protected static function get_and_export_user_preference(int $userid, string $userpreference, $transform = false) { + $prefvalue = get_user_preferences($userpreference, null, $userid); + if ($prefvalue !== null) { + if ($transform) { + $transformedvalue = transform::yesno($prefvalue); + } else { + $transformedvalue = $prefvalue; + } + writer::export_user_preference( + 'scormreport_basic', + $userpreference, + $transformedvalue, + get_string('privacy:metadata:preference:'.$userpreference, 'scormreport_basic') + ); + } + } +} diff --git a/mod/scorm/report/basic/lang/en/scormreport_basic.php b/mod/scorm/report/basic/lang/en/scormreport_basic.php index 7a031521aba..55171eb9778 100644 --- a/mod/scorm/report/basic/lang/en/scormreport_basic.php +++ b/mod/scorm/report/basic/lang/en/scormreport_basic.php @@ -24,3 +24,5 @@ */ $string['pluginname'] = 'Basic report'; +$string['privacy:metadata:preference:scorm_report_detailed'] = 'Whether to track details in the SCORM basic report'; +$string['privacy:metadata:preference:scorm_report_pagesize'] = 'Number of users to display in the SCORM reports'; diff --git a/mod/scorm/report/basic/tests/privacy_test.php b/mod/scorm/report/basic/tests/privacy_test.php new file mode 100644 index 00000000000..ff06ce57658 --- /dev/null +++ b/mod/scorm/report/basic/tests/privacy_test.php @@ -0,0 +1,85 @@ +. + +/** + * Unit tests for the scormreport_basic implementation of the privacy API. + * + * @package scormreport_basic + * @category test + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_privacy\local\request\writer; +use scormreport_basic\privacy\provider; + +/** + * Unit tests for the scormreport_basic implementation of the privacy API. + * + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scormreport_basic_privacy_testcase extends \core_privacy\tests\provider_testcase { + + /** + * Basic setup for these tests. + */ + public function setUp() { + $this->resetAfterTest(true); + } + + /** + * Ensure that export_user_preferences returns no data if the user has no data. + */ + public function test_export_user_preferences_not_defined() { + $user = \core_user::get_user_by_username('admin'); + provider::export_user_preferences($user->id); + + $writer = writer::with_context(\context_system::instance()); + $this->assertFalse($writer->has_any_data()); + } + + /** + * Ensure that export_user_preferences returns single preferences. + */ + public function test_export_user_preferences_single() { + // Define a user preference. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + set_user_preference('scorm_report_detailed', 1); + set_user_preference('scorm_report_pagesize', 50); + + // Validate exported data. + provider::export_user_preferences($user->id); + $context = \context_user::instance($user->id); + $writer = writer::with_context($context); + $this->assertTrue($writer->has_any_data()); + $prefs = $writer->get_user_preferences('scormreport_basic'); + $this->assertCount(2, (array) $prefs); + $this->assertEquals( + get_string('privacy:metadata:preference:scorm_report_detailed', 'scormreport_basic'), + $prefs->scorm_report_detailed->description + ); + $this->assertEquals(get_string('yes'), $prefs->scorm_report_detailed->value); + $this->assertEquals( + get_string('privacy:metadata:preference:scorm_report_pagesize', 'scormreport_basic'), + $prefs->scorm_report_pagesize->description + ); + $this->assertEquals(50, $prefs->scorm_report_pagesize->value); + } +} From 353f1bfb37595a52dee5909a4dcb9bd79773ed91 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Mon, 23 Apr 2018 13:09:43 +0200 Subject: [PATCH 2/5] MDL-61959 scormreport_graphs: Implement privacy API --- .../graphs/classes/privacy/provider.php | 46 +++++++++++++++++++ .../graphs/lang/en/scormreport_graphs.php | 7 +-- 2 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 mod/scorm/report/graphs/classes/privacy/provider.php diff --git a/mod/scorm/report/graphs/classes/privacy/provider.php b/mod/scorm/report/graphs/classes/privacy/provider.php new file mode 100644 index 00000000000..d68c521c8e7 --- /dev/null +++ b/mod/scorm/report/graphs/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for scormreport_graphs. + * + * @package scormreport_graphs + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace scormreport_graphs\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for scormreport_graphs implementing null_provider. + * + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/mod/scorm/report/graphs/lang/en/scormreport_graphs.php b/mod/scorm/report/graphs/lang/en/scormreport_graphs.php index 81bdbd7a233..5e463d36d58 100644 --- a/mod/scorm/report/graphs/lang/en/scormreport_graphs.php +++ b/mod/scorm/report/graphs/lang/en/scormreport_graphs.php @@ -22,10 +22,11 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['participants'] = 'Number of participants'; -$string['pluginname'] = 'Graph report'; -$string['percent'] = 'Percent(%) secured'; $string['invaliddata'] = 'Not enough data'; +$string['participants'] = 'Number of participants'; +$string['percent'] = 'Percent(%) secured'; +$string['pluginname'] = 'Graph report'; +$string['privacy:metadata'] = 'The Graph report only shows data stored in other locations.'; From 3127efef0cf492b564eb15dd3b2522d2a7652d1c Mon Sep 17 00:00:00 2001 From: sarjona Date: Mon, 23 Apr 2018 07:59:12 +0200 Subject: [PATCH 3/5] MDL-61959 scormreport_interactions: Implement privacy API --- .../interactions/classes/privacy/provider.php | 110 ++++++++++++++++++ .../lang/en/scormreport_interactions.php | 5 + .../interactions/tests/privacy_test.php | 89 ++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 mod/scorm/report/interactions/classes/privacy/provider.php create mode 100644 mod/scorm/report/interactions/tests/privacy_test.php diff --git a/mod/scorm/report/interactions/classes/privacy/provider.php b/mod/scorm/report/interactions/classes/privacy/provider.php new file mode 100644 index 00000000000..06c10dbb0cb --- /dev/null +++ b/mod/scorm/report/interactions/classes/privacy/provider.php @@ -0,0 +1,110 @@ +. + +/** + * Privacy Subsystem implementation for scormreport_interactions. + * + * @package scormreport_interactions + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace scormreport_interactions\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\transform; +use \core_privacy\local\request\writer; + +/** + * Privacy Subsystem for scormreport_interactions. + * + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\user_preference_provider { + + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised item collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) : collection { + // User preferences shared between different scorm reports. + $collection->add_user_preference('scorm_report_pagesize', 'privacy:metadata:preference:scorm_report_pagesize'); + + // User preferences specific for this scorm report. + $collection->add_user_preference( + 'scorm_report_interactions_qtext', + 'privacy:metadata:preference:scorm_report_interactions_qtext' + ); + $collection->add_user_preference( + 'scorm_report_interactions_resp', + 'privacy:metadata:preference:scorm_report_interactions_resp' + ); + $collection->add_user_preference( + 'scorm_report_interactions_right', + 'privacy:metadata:preference:scorm_report_interactions_right' + ); + $collection->add_user_preference( + 'scorm_report_interactions_result', + 'privacy:metadata:preference:scorm_report_interactions_result' + ); + + return $collection; + } + + /** + * Store all user preferences for the plugin. + * + * @param int $userid The userid of the user whose data is to be exported. + */ + public static function export_user_preferences(int $userid) { + static::get_and_export_user_preference($userid, 'scorm_report_pagesize'); + static::get_and_export_user_preference($userid, 'scorm_report_interactions_qtext', true); + static::get_and_export_user_preference($userid, 'scorm_report_interactions_resp', true); + static::get_and_export_user_preference($userid, 'scorm_report_interactions_right', true); + static::get_and_export_user_preference($userid, 'scorm_report_interactions_result', true); + } + + /** + * Get and export a user preference. + * + * @param int $userid The userid of the user whose data is to be exported. + * @param string $userpreference The user preference to export. + * @param boolean $transform If true, transform value to yesno. + */ + protected static function get_and_export_user_preference(int $userid, string $userpreference, $transform = false) { + $prefvalue = get_user_preferences($userpreference, null, $userid); + if ($prefvalue !== null) { + if ($transform) { + $transformedvalue = transform::yesno($prefvalue); + } else { + $transformedvalue = $prefvalue; + } + writer::export_user_preference( + 'scormreport_interactions', + $userpreference, + $transformedvalue, + get_string('privacy:metadata:preference:'.$userpreference, 'scormreport_interactions') + ); + } + } +} diff --git a/mod/scorm/report/interactions/lang/en/scormreport_interactions.php b/mod/scorm/report/interactions/lang/en/scormreport_interactions.php index ed66f59fa65..5cd1450a291 100644 --- a/mod/scorm/report/interactions/lang/en/scormreport_interactions.php +++ b/mod/scorm/report/interactions/lang/en/scormreport_interactions.php @@ -26,6 +26,11 @@ defined('MOODLE_INTERNAL') || die(); $string['pluginname'] = 'Interactions report'; +$string['privacy:metadata:preference:scorm_report_interactions_qtext'] = 'Whether to display the summary of questions in the SCORM interactions report'; +$string['privacy:metadata:preference:scorm_report_interactions_resp'] = 'Whether to display the summary of responses in the SCORM interactions report'; +$string['privacy:metadata:preference:scorm_report_interactions_result'] = 'Whether to display the summary of results in the SCORM interactions report'; +$string['privacy:metadata:preference:scorm_report_interactions_right'] = 'Whether to display the summary of right anwers in the SCORM interactions report'; +$string['privacy:metadata:preference:scorm_report_pagesize'] = 'Number of users to display in the SCORM reports'; $string['questionx'] = 'Question {$a}'; $string['responsex'] = 'Response {$a}'; $string['rightanswerx'] = 'Right answer {$a}'; diff --git a/mod/scorm/report/interactions/tests/privacy_test.php b/mod/scorm/report/interactions/tests/privacy_test.php new file mode 100644 index 00000000000..80c63d2ce46 --- /dev/null +++ b/mod/scorm/report/interactions/tests/privacy_test.php @@ -0,0 +1,89 @@ +. + +/** + * Unit tests for the scormreport_interactions implementation of the privacy API. + * + * @package scormreport_interactions + * @category test + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_privacy\local\request\writer; +use scormreport_interactions\privacy\provider; + +/** + * Unit tests for the scormreport_interactions implementation of the privacy API. + * + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scormreport_interactions_privacy_testcase extends \core_privacy\tests\provider_testcase { + + /** + * Basic setup for these tests. + */ + public function setUp() { + $this->resetAfterTest(true); + } + + /** + * Ensure that export_user_preferences returns no data if the user has no data. + */ + public function test_export_user_preferences_not_defined() { + $user = \core_user::get_user_by_username('admin'); + provider::export_user_preferences($user->id); + + $writer = writer::with_context(\context_system::instance()); + $this->assertFalse($writer->has_any_data()); + } + + /** + * Ensure that export_user_preferences returns single preferences. + */ + public function test_export_user_preferences_single() { + // Define a user preference. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + set_user_preference('scorm_report_pagesize', 50); + set_user_preference('scorm_report_interactions_qtext', 1); + set_user_preference('scorm_report_interactions_resp', 0); + set_user_preference('scorm_report_interactions_right', 1); + set_user_preference('scorm_report_interactions_result', 1); + + // Validate exported data. + provider::export_user_preferences($user->id); + $context = \context_user::instance($user->id); + $writer = writer::with_context($context); + $this->assertTrue($writer->has_any_data()); + $prefs = $writer->get_user_preferences('scormreport_interactions'); + $this->assertCount(5, (array) $prefs); + $this->assertEquals( + get_string('privacy:metadata:preference:scorm_report_pagesize', 'scormreport_interactions'), + $prefs->scorm_report_pagesize->description + ); + $this->assertEquals(50, $prefs->scorm_report_pagesize->value); + $this->assertEquals( + get_string('privacy:metadata:preference:scorm_report_interactions_qtext', 'scormreport_interactions'), + $prefs->scorm_report_interactions_qtext->description + ); + $this->assertEquals(get_string('yes'), $prefs->scorm_report_interactions_qtext->value); + $this->assertEquals(get_string('no'), $prefs->scorm_report_interactions_resp->value); + } +} From b078358fa7fc7f2693b2ad077c5ec216dc526e0d Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Mon, 23 Apr 2018 12:45:29 +0200 Subject: [PATCH 4/5] MDL-61959 scormreport_objectives: Implement privacy API --- .../objectives/classes/privacy/provider.php | 95 +++++++++++++++++++ .../lang/en/scormreport_objectives.php | 2 + .../report/objectives/tests/privacy_test.php | 85 +++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 mod/scorm/report/objectives/classes/privacy/provider.php create mode 100644 mod/scorm/report/objectives/tests/privacy_test.php diff --git a/mod/scorm/report/objectives/classes/privacy/provider.php b/mod/scorm/report/objectives/classes/privacy/provider.php new file mode 100644 index 00000000000..48465c7a35c --- /dev/null +++ b/mod/scorm/report/objectives/classes/privacy/provider.php @@ -0,0 +1,95 @@ +. + +/** + * Privacy Subsystem implementation for scormreport_objectives. + * + * @package scormreport_objectives + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace scormreport_objectives\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\transform; +use \core_privacy\local\request\writer; + +/** + * Privacy Subsystem for scormreport_objectives. + * + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\user_preference_provider { + + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised item collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) : collection { + // User preferences shared between different scorm reports. + $collection->add_user_preference('scorm_report_pagesize', 'privacy:metadata:preference:scorm_report_pagesize'); + + // User preferences specific for this scorm report. + $collection->add_user_preference( + 'scorm_report_objectives_score', + 'privacy:metadata:preference:scorm_report_objectives_score' + ); + + return $collection; + } + + /** + * Store all user preferences for the plugin. + * + * @param int $userid The userid of the user whose data is to be exported. + */ + public static function export_user_preferences(int $userid) { + static::get_and_export_user_preference($userid, 'scorm_report_pagesize'); + static::get_and_export_user_preference($userid, 'scorm_report_objectives_score', true); + } + + /** + * Get and export a user preference. + * + * @param int $userid The userid of the user whose data is to be exported. + * @param string $userpreference The user preference to export. + * @param boolean $transform If true, transform value to yesno. + */ + protected static function get_and_export_user_preference(int $userid, string $userpreference, $transform = false) { + $prefvalue = get_user_preferences($userpreference, null, $userid); + if ($prefvalue !== null) { + if ($transform) { + $transformedvalue = transform::yesno($prefvalue); + } else { + $transformedvalue = $prefvalue; + } + writer::export_user_preference( + 'scormreport_objectives', + $userpreference, + $transformedvalue, + get_string('privacy:metadata:preference:'.$userpreference, 'scormreport_objectives') + ); + } + } +} diff --git a/mod/scorm/report/objectives/lang/en/scormreport_objectives.php b/mod/scorm/report/objectives/lang/en/scormreport_objectives.php index 7f2d17f8a3d..efbcfcb1be9 100644 --- a/mod/scorm/report/objectives/lang/en/scormreport_objectives.php +++ b/mod/scorm/report/objectives/lang/en/scormreport_objectives.php @@ -26,6 +26,8 @@ defined('MOODLE_INTERNAL') || die(); $string['pluginname'] = 'Objectives report'; +$string['privacy:metadata:preference:scorm_report_objectives_score'] = 'Whether to display the objective score in the SCORM report'; +$string['privacy:metadata:preference:scorm_report_pagesize'] = 'Number of users to display in the SCORM reports'; $string['objectivex'] = 'Objective {$a}'; $string['objectivescore'] = 'Show objective score'; $string['score'] = 'score'; diff --git a/mod/scorm/report/objectives/tests/privacy_test.php b/mod/scorm/report/objectives/tests/privacy_test.php new file mode 100644 index 00000000000..90ae77e8eb7 --- /dev/null +++ b/mod/scorm/report/objectives/tests/privacy_test.php @@ -0,0 +1,85 @@ +. + +/** + * Unit tests for the scormreport_objectives implementation of the privacy API. + * + * @package scormreport_objectives + * @category test + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_privacy\local\request\writer; +use scormreport_objectives\privacy\provider; + +/** + * Unit tests for the scormreport_objectives implementation of the privacy API. + * + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scormreport_objectives_privacy_testcase extends \core_privacy\tests\provider_testcase { + + /** + * Basic setup for these tests. + */ + public function setUp() { + $this->resetAfterTest(true); + } + + /** + * Ensure that export_user_preferences returns no data if the user has no data. + */ + public function test_export_user_preferences_not_defined() { + $user = \core_user::get_user_by_username('admin'); + provider::export_user_preferences($user->id); + + $writer = writer::with_context(\context_system::instance()); + $this->assertFalse($writer->has_any_data()); + } + + /** + * Ensure that export_user_preferences returns single preferences. + */ + public function test_export_user_preferences_single() { + // Define a user preference. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + set_user_preference('scorm_report_pagesize', 50); + set_user_preference('scorm_report_objectives_score', 1); + + // Validate exported data. + provider::export_user_preferences($user->id); + $context = \context_user::instance($user->id); + $writer = writer::with_context($context); + $this->assertTrue($writer->has_any_data()); + $prefs = $writer->get_user_preferences('scormreport_objectives'); + $this->assertCount(2, (array) $prefs); + $this->assertEquals( + get_string('privacy:metadata:preference:scorm_report_pagesize', 'scormreport_objectives'), + $prefs->scorm_report_pagesize->description + ); + $this->assertEquals(50, $prefs->scorm_report_pagesize->value); + $this->assertEquals( + get_string('privacy:metadata:preference:scorm_report_objectives_score', 'scormreport_objectives'), + $prefs->scorm_report_objectives_score->description + ); + $this->assertEquals(get_string('yes'), $prefs->scorm_report_objectives_score->value); + } +} From df7cc2cc7f4b7049b76636514dddddc588acba5c Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Mon, 23 Apr 2018 17:53:12 +0200 Subject: [PATCH 5/5] MDL-61959 mod_scorm: Implement privacy API --- mod/scorm/classes/privacy/provider.php | 295 +++++++++++++++++++++++++ mod/scorm/lang/en/scorm.php | 14 ++ mod/scorm/tests/privacy_test.php | 243 ++++++++++++++++++++ 3 files changed, 552 insertions(+) create mode 100644 mod/scorm/classes/privacy/provider.php create mode 100644 mod/scorm/tests/privacy_test.php diff --git a/mod/scorm/classes/privacy/provider.php b/mod/scorm/classes/privacy/provider.php new file mode 100644 index 00000000000..855499ed998 --- /dev/null +++ b/mod/scorm/classes/privacy/provider.php @@ -0,0 +1,295 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package mod_scorm + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scorm\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; + +/** + * Privacy class for requesting user data. + * + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\plugin\provider { + + /** + * Return the fields which contain personal data. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) : collection { + $collection->add_database_table('scorm_scoes_track', [ + 'userid' => 'privacy:metadata:userid', + 'attempt' => 'privacy:metadata:attempt', + 'element' => 'privacy:metadata:scoes_track:element', + 'value' => 'privacy:metadata:scoes_track:value', + 'timemodified' => 'privacy:metadata:timemodified' + ], 'privacy:metadata:scorm_scoes_track'); + + $collection->add_database_table('scorm_aicc_session', [ + 'userid' => 'privacy:metadata:userid', + 'scormmode' => 'privacy:metadata:aicc_session:scormmode', + 'scormstatus' => 'privacy:metadata:aicc_session:scormstatus', + 'attempt' => 'privacy:metadata:attempt', + 'lessonstatus' => 'privacy:metadata:aicc_session:lessonstatus', + 'sessiontime' => 'privacy:metadata:aicc_session:sessiontime', + 'timecreated' => 'privacy:metadata:aicc_session:timecreated', + 'timemodified' => 'privacy:metadata:timemodified', + ], 'privacy:metadata:scorm_aicc_session'); + + $collection->add_external_location_link('aicc', [ + 'data' => 'privacy:metadata:aicc:data' + ], 'privacy:metadata:aicc:externalpurpose'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $sql = "SELECT ctx.id + FROM {%s} ss + JOIN {modules} m + ON m.name = 'scorm' + JOIN {course_modules} cm + ON cm.instance = ss.scormid + AND cm.module = m.id + JOIN {context} ctx + ON ctx.instanceid = cm.id + AND ctx.contextlevel = :modlevel + WHERE ss.userid = :userid"; + + $params = ['modlevel' => CONTEXT_MODULE, 'userid' => $userid]; + $contextlist = new contextlist(); + $contextlist->add_from_sql(sprintf($sql, 'scorm_scoes_track'), $params); + $contextlist->add_from_sql(sprintf($sql, 'scorm_aicc_session'), $params); + + return $contextlist; + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + // Remove contexts different from COURSE_MODULE. + $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_MODULE) { + $carry[] = $context->id; + } + return $carry; + }, []); + + if (empty($contexts)) { + return; + } + + $userid = $contextlist->get_user()->id; + list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED); + + // Get scoes_track data. + $sql = "SELECT ss.id, + ss.attempt, + ss.element, + ss.value, + ss.timemodified, + ctx.id as contextid + FROM {scorm_scoes_track} ss + JOIN {course_modules} cm + ON cm.instance = ss.scormid + JOIN {context} ctx + ON ctx.instanceid = cm.id + WHERE ctx.id $insql + AND ss.userid = :userid"; + $params = array_merge($inparams, ['userid' => $userid]); + + $alldata = []; + $scoestracks = $DB->get_recordset_sql($sql, $params); + foreach ($scoestracks as $track) { + $alldata[$track->contextid][$track->attempt][] = (object)[ + 'element' => $track->element, + 'value' => $track->value, + 'timemodified' => transform::datetime($track->timemodified), + ]; + } + $scoestracks->close(); + + // The scoes_track data is organised in: {Course name}/{SCORM activity name}/attempt-X.json. + // where X is the attempt number. + array_walk($alldata, function($attemptsdata, $contextid) { + $context = \context::instance_by_id($contextid); + array_walk($attemptsdata, function($data, $attempt) use ($context) { + writer::with_context($context)->export_related_data( + [], + 'attempt-'.$attempt, + (object)['scoestrack' => $data] + ); + }); + }); + + // Get aicc_session data. + $sql = "SELECT ss.id, + ss.scormmode, + ss.scormstatus, + ss.attempt, + ss.lessonstatus, + ss.sessiontime, + ss.timecreated, + ss.timemodified, + ctx.id as contextid + FROM {scorm_aicc_session} ss + JOIN {course_modules} cm + ON cm.instance = ss.scormid + JOIN {context} ctx + ON ctx.instanceid = cm.id + WHERE ctx.id $insql + AND ss.userid = :userid"; + $params = array_merge($inparams, ['userid' => $userid]); + + $alldata = []; + $aiccsessions = $DB->get_recordset_sql($sql, $params); + foreach ($aiccsessions as $aiccsession) { + $alldata[$aiccsession->contextid][] = (object)[ + 'scormmode' => $aiccsession->scormmode, + 'scormstatus' => $aiccsession->scormstatus, + 'lessonstatus' => $aiccsession->lessonstatus, + 'attempt' => $aiccsession->attempt, + 'sessiontime' => $aiccsession->sessiontime, + 'timecreated' => transform::datetime($aiccsession->timecreated), + 'timemodified' => transform::datetime($aiccsession->timemodified), + ]; + } + $aiccsessions->close(); + + // The aicc_session data is organised in: {Course name}/{SCORM activity name}/aiccsession.json. + // In this case, the attempt hasn't been included in the json file because it can be null. + array_walk($alldata, function($data, $contextid) { + $context = \context::instance_by_id($contextid); + writer::with_context($context)->export_related_data( + [], + 'aiccsession', + (object)['sessions' => $data] + ); + }); + } + + /** + * Delete all user data which matches the specified context. + * + * @param context $context A user context. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + // This should not happen, but just in case. + if ($context->contextlevel != CONTEXT_MODULE) { + return; + } + + // Prepare SQL to gather all IDs to delete. + $sql = "SELECT ss.id + FROM {%s} ss + JOIN {modules} m + ON m.name = 'scorm' + JOIN {course_modules} cm + ON cm.instance = ss.scormid + AND cm.module = m.id + WHERE cm.id = :cmid"; + $params = ['cmid' => $context->instanceid]; + + static::delete_data('scorm_scoes_track', $sql, $params); + static::delete_data('scorm_aicc_session', $sql, $params); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + // Remove contexts different from COURSE_MODULE. + $contextids = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_MODULE) { + $carry[] = $context->id; + } + return $carry; + }, []); + + if (empty($contextids)) { + return; + } + $userid = $contextlist->get_user()->id; + // Prepare SQL to gather all completed IDs. + list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); + $sql = "SELECT ss.id + FROM {%s} ss + JOIN {modules} m + ON m.name = 'scorm' + JOIN {course_modules} cm + ON cm.instance = ss.scormid + AND cm.module = m.id + JOIN {context} ctx + ON ctx.instanceid = cm.id + WHERE ss.userid = :userid + AND ctx.id $insql"; + $params = array_merge($inparams, ['userid' => $userid]); + + static::delete_data('scorm_scoes_track', $sql, $params); + static::delete_data('scorm_aicc_session', $sql, $params); + } + + /** + * Delete data from $tablename with the IDs returned by $sql query. + * + * @param string $tablename Table name where executing the SQL query. + * @param string $sql SQL query for getting the IDs of the scoestrack entries to delete. + * @param array $params SQL params for the query. + */ + protected static function delete_data(string $tablename, string $sql, array $params) { + global $DB; + + $scoestracksids = $DB->get_fieldset_sql(sprintf($sql, $tablename), $params); + if (!empty($scoestracksids)) { + list($insql, $inparams) = $DB->get_in_or_equal($scoestracksids, SQL_PARAMS_NAMED); + $DB->delete_records_select($tablename, "id $insql", $inparams); + } + } +} diff --git a/mod/scorm/lang/en/scorm.php b/mod/scorm/lang/en/scorm.php index fc7ada119df..c541a076ee0 100644 --- a/mod/scorm/lang/en/scorm.php +++ b/mod/scorm/lang/en/scorm.php @@ -332,6 +332,20 @@ $string['position_error'] = 'The {$a->tag} tag can\'t be child of {$a->parent} t $string['preferencesuser'] = 'Preferences for this report'; $string['preferencespage'] = 'Preferences just for this page'; $string['prev'] = 'Previous'; +$string['privacy:metadata:aicc:data'] = 'Personal data passed through from the AICC/SCORM subsystem.'; +$string['privacy:metadata:aicc:externalpurpose'] = 'This plugin sends data externally using the AICC HACP.'; +$string['privacy:metadata:aicc_session:lessonstatus'] = 'The lesson status to be tracked'; +$string['privacy:metadata:aicc_session:scormmode'] = 'The mode of the element to be tracked'; +$string['privacy:metadata:aicc_session:scormstatus'] = 'The status of the element to be tracked'; +$string['privacy:metadata:aicc_session:sessiontime'] = 'The session time to be tracked'; +$string['privacy:metadata:aicc_session:timecreated'] = 'The time when the tracked element was created'; +$string['privacy:metadata:attempt'] = 'The attempt number'; +$string['privacy:metadata:scoes_track:element'] = 'The name of the element to be tracked'; +$string['privacy:metadata:scoes_track:value'] = 'The value of the given element'; +$string['privacy:metadata:scorm_aicc_session'] = 'The session information of the AICC HACP'; +$string['privacy:metadata:scorm_scoes_track'] = 'The tracked data of the SCOes belonging to the activity'; +$string['privacy:metadata:timemodified'] = 'The time when the tracked element was last modified'; +$string['privacy:metadata:userid'] = 'The ID of the user who accessed the SCORM activity'; $string['protectpackagedownloads'] = 'Protect package downloads'; $string['protectpackagedownloads_desc'] = 'If enabled, SCORM packages can be downloaded only if the user has the course:manageactivities capability. If disabled, SCORM packages can always be downloaded (by mobile or other means).'; $string['raw'] = 'Raw score'; diff --git a/mod/scorm/tests/privacy_test.php b/mod/scorm/tests/privacy_test.php new file mode 100644 index 00000000000..096988d3afa --- /dev/null +++ b/mod/scorm/tests/privacy_test.php @@ -0,0 +1,243 @@ +. + +/** + * Base class for unit tests for mod_scorm. + * + * @package mod_scorm + * @category test + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use mod_scorm\privacy\provider; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\writer; +use core_privacy\tests\provider_testcase; + +/** + * Unit tests for mod\scorm\classes\privacy\provider.php + * + * @copyright 2018 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_scorm_testcase extends provider_testcase { + + /** @var stdClass User without any AICC/SCORM attempt. */ + protected $student0; + + /** @var stdClass User with some AICC/SCORM attempt. */ + protected $student1; + + /** @var stdClass User with some AICC/SCORM attempt. */ + protected $student2; + + /** @var context context_module of the SCORM activity. */ + protected $context; + + /** + * Test getting the context for the user ID related to this plugin. + */ + public function test_get_contexts_for_userid() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $this->scorm_setup_test_scenario_data(); + + // The student0 hasn't any attempt. + $contextlist = provider::get_contexts_for_userid($this->student0->id); + $this->assertCount(0, (array) $contextlist->get_contextids()); + + // The student1 has data in the SCORM context. + $contextlist = provider::get_contexts_for_userid($this->student1->id); + $this->assertCount(1, (array) $contextlist->get_contextids()); + $this->assertContains($this->context->id, $contextlist->get_contextids()); + } + + /** + * Test that data is exported correctly for this plugin. + */ + public function test_export_user_data() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $this->scorm_setup_test_scenario_data(); + + // Validate exported data for student0 (without any AICC/SCORM attempt). + $this->setUser($this->student0); + $writer = writer::with_context($this->context); + $this->export_context_data_for_user($this->student0->id, $this->context, 'mod_scorm'); + $data = $writer->get_related_data([], 'attempt-1'); + $this->assertEmpty($data); + $this->export_context_data_for_user($this->student0->id, $this->context, 'mod_scorm'); + $data = $writer->get_related_data([], 'aiccsession'); + $this->assertEmpty($data); + + // Validate exported data for student1. + writer::reset(); + $this->setUser($this->student1); + $writer = writer::with_context($this->context); + $this->assertFalse($writer->has_any_data()); + $this->export_context_data_for_user($this->student1->id, $this->context, 'mod_scorm'); + $data = $writer->get_related_data([], 'attempt-1'); + $this->assertCount(1, (array) $data); + $this->assertCount(2, (array) reset($data)); + $data = $writer->get_related_data([], 'attempt-2'); + $this->assertCount(2, (array) reset($data)); + // The student1 has only 2 scoes_track attempts. + $data = $writer->get_related_data([], 'attempt-3'); + $this->assertEmpty($data); + // The student1 has only 1 aicc_session. + $this->export_context_data_for_user($this->student1->id, $this->context, 'mod_scorm'); + $data = $writer->get_related_data([], 'aiccsession'); + $this->assertCount(1, (array) $data); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $this->scorm_setup_test_scenario_data(); + + // Before deletion, we should have 8 entries in the scorm_scoes_track table. + $count = $DB->count_records('scorm_scoes_track'); + $this->assertEquals(8, $count); + // Before deletion, we should have 4 entries in the scorm_aicc_session table. + $count = $DB->count_records('scorm_aicc_session'); + $this->assertEquals(4, $count); + + // Delete data based on the context. + provider::delete_data_for_all_users_in_context($this->context); + + // After deletion, the scorm_scoes_track entries should have been deleted. + $count = $DB->count_records('scorm_scoes_track'); + $this->assertEquals(0, $count); + // After deletion, the scorm_aicc_session entries should have been deleted. + $count = $DB->count_records('scorm_aicc_session'); + $this->assertEquals(0, $count); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $this->scorm_setup_test_scenario_data(); + + // Before deletion, we should have 8 entries in the scorm_scoes_track table. + $count = $DB->count_records('scorm_scoes_track'); + $this->assertEquals(8, $count); + // Before deletion, we should have 4 entries in the scorm_aicc_session table. + $count = $DB->count_records('scorm_aicc_session'); + $this->assertEquals(4, $count); + + $approvedcontextlist = new approved_contextlist($this->student1, 'scorm', [$this->context->id]); + provider::delete_data_for_user($approvedcontextlist); + + // After deletion, the scorm_scoes_track entries for the first student should have been deleted. + $count = $DB->count_records('scorm_scoes_track', ['userid' => $this->student1->id]); + $this->assertEquals(0, $count); + $count = $DB->count_records('scorm_scoes_track'); + $this->assertEquals(4, $count); + // After deletion, the scorm_aicc_session entries for the first student should have been deleted. + $count = $DB->count_records('scorm_aicc_session', ['userid' => $this->student1->id]); + $this->assertEquals(0, $count); + $count = $DB->count_records('scorm_aicc_session'); + $this->assertEquals(2, $count); + + // Confirm that the SCORM hasn't been removed. + $scormcount = $DB->get_records('scorm'); + $this->assertCount(1, (array) $scormcount); + + // Delete scoes_track for student0 (nothing has to be removed). + $approvedcontextlist = new approved_contextlist($this->student0, 'scorm', [$this->context->id]); + provider::delete_data_for_user($approvedcontextlist); + $count = $DB->count_records('scorm_scoes_track'); + $this->assertEquals(4, $count); + $count = $DB->count_records('scorm_aicc_session'); + $this->assertEquals(2, $count); + } + + /** + * Helper function to setup 3 users and 2 SCORM attempts for student1 and student2. + * $this->student0 is always created withot any attempt. + */ + protected function scorm_setup_test_scenario_data() { + global $DB; + + set_config('allowaicchacp', 1, 'scorm'); + + // Setup test data. + $course = $this->getDataGenerator()->create_course(); + $scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $course->id)); + $this->context = \context_module::instance($scorm->cmid); + + // Users enrolments. + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + + // Create student0 withot any SCORM attempt. + $this->student0 = self::getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($this->student0->id, $course->id, $studentrole->id, 'manual'); + + // Create student1 with 2 SCORM attempts and 1 AICC session. + $this->student1 = self::getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($this->student1->id, $course->id, $studentrole->id, 'manual'); + static::scorm_insert_attempt($scorm, $this->student1->id, 1); + static::scorm_insert_attempt($scorm, $this->student1->id, 2); + + // Create student2 with 2 SCORM attempts and 1 AICC session. + $this->student2 = self::getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($this->student2->id, $course->id, $studentrole->id, 'manual'); + static::scorm_insert_attempt($scorm, $this->student2->id, 1); + static::scorm_insert_attempt($scorm, $this->student2->id, 2); + } + + /** + * Create a SCORM attempt. + * + * @param object $scorm SCORM activity. + * @param int $userid Userid who is doing the attempt. + * @param int $attempt Number of attempt. + */ + protected function scorm_insert_attempt($scorm, $userid, $attempt) { + global $DB; + + $newattempt = 'on'; + $mode = 'normal'; + scorm_check_mode($scorm, $newattempt, $attempt, $userid, $mode); + $scoes = scorm_get_scoes($scorm->id); + $sco = array_pop($scoes); + scorm_insert_track($userid, $scorm->id, $sco->id, $attempt, 'cmi.core.lesson_status', 'completed'); + scorm_insert_track($userid, $scorm->id, $sco->id, $attempt, 'cmi.score.min', '0'); + $now = time(); + $hacpsession = [ + 'scormid' => $scorm->id, + 'attempt' => $attempt, + 'hacpsession' => random_string(20), + 'userid' => $userid, + 'timecreated' => $now, + 'timemodified' => $now + ]; + $DB->insert_record('scorm_aicc_session', $hacpsession); + } +}