diff --git a/lang/en/error.php b/lang/en/error.php index 2faf5302ead..f8c7ef0abfd 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -427,6 +427,8 @@ $string['moduledoesnotexist'] = 'This module does not exist'; $string['moduleinstancedoesnotexist'] = 'The instance of this module does not exist'; $string['modulemissingcode'] = 'Module {$a} is missing the code needed to perform this function'; $string['moodlenet:invalidshareformat'] = 'Invalid MoodleNet share format'; +$string['moodlenet:invalidsharestatus'] = 'Invalid MoodleNet share status'; +$string['moodlenet:invalidsharetype'] = 'Invalid MoodleNet share type'; $string['moodlenet:usernotconfigured'] = 'You do not have permission to share content to MoodleNet, or your account is incorrectly configured.'; $string['movecatcontentstoroot'] = 'Moving the category content to root is not allowed. You must move the contents to an existing category!'; $string['movecatcontentstoselected'] = 'Some category content cannot be moved into the selected category.'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index afa41793668..163fab0c05b 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1745,6 +1745,14 @@ $string['privacy:metadata:log:module'] = 'module'; $string['privacy:metadata:log:time'] = 'The time when the action took place'; $string['privacy:metadata:log:url'] = 'The URL related to the event'; $string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action'; +$string['privacy:metadata:moodlenet_share_progress'] = 'MoodleNet share progress details'; +$string['privacy:metadata:moodlenet_share_progress:type'] = 'The type of share that was performed'; +$string['privacy:metadata:moodlenet_share_progress:courseid'] = 'The associated course ID'; +$string['privacy:metadata:moodlenet_share_progress:cmid'] = 'The associated course module ID'; +$string['privacy:metadata:moodlenet_share_progress:userid'] = 'The user that performed the share'; +$string['privacy:metadata:moodlenet_share_progress:timecreated'] = 'The time the share was performed'; +$string['privacy:metadata:moodlenet_share_progress:resourceurl'] = 'The returned url from MoodleNet after a successful share'; +$string['privacy:metadata:moodlenet_share_progress:status'] = 'The resulting status of the share'; $string['privacy:metadata:oauth2_refresh_token'] = 'Refresh token used in OAuth 2.0 communication'; $string['privacy:metadata:oauth2_refresh_token:issuerid'] = 'The ID of the issuer to which the token corresponds'; $string['privacy:metadata:oauth2_refresh_token:scopehash'] = 'The ID of the user to whom the token corresponds'; diff --git a/lib/classes/external/moodlenet_send_activity.php b/lib/classes/external/moodlenet_send_activity.php index 10b5040d56d..edb093eb25f 100644 --- a/lib/classes/external/moodlenet_send_activity.php +++ b/lib/classes/external/moodlenet_send_activity.php @@ -21,6 +21,7 @@ use core\http_client; use core\moodlenet\activity_sender; use core\moodlenet\moodlenet_client; use core\moodlenet\utilities; +use core\moodlenet\share_recorder; use core\oauth2\api; use core_external\external_api; use core_external\external_function_parameters; @@ -114,17 +115,28 @@ class moodlenet_send_activity extends external_api { // Share activity. try { + // Record activity share progress. + $shareid = share_recorder::insert_share_progress(share_recorder::TYPE_ACTIVITY, $USER->id, $course->id, $cmid); + $moodlenetclient = new moodlenet_client($client, $oauthclient); $activitysender = new activity_sender($cmid, $USER->id, $moodlenetclient, $oauthclient, $shareformat); $result = $activitysender->share_resource(); if (empty($result['drafturl'])) { + + share_recorder::update_share_progress($shareid, share_recorder::STATUS_ERROR); + return self::return_errors($result['responsecode'], 'errorsendingactivity', get_string('moodlenet:cannotconnecttoserver', 'moodle')); } } catch (\moodle_exception $e) { + + share_recorder::update_share_progress($shareid, share_recorder::STATUS_ERROR); + return self::return_errors(0, 'errorsendingactivity', $e->getMessage()); } + share_recorder::update_share_progress($shareid, share_recorder::STATUS_SENT, $result['drafturl']); + return [ 'status' => true, 'resourceurl' => $result['drafturl'], diff --git a/lib/classes/external/moodlenet_send_course.php b/lib/classes/external/moodlenet_send_course.php index ac82cf4190c..7d6ce05a8ee 100644 --- a/lib/classes/external/moodlenet_send_course.php +++ b/lib/classes/external/moodlenet_send_course.php @@ -20,6 +20,7 @@ use context_course; use core\http_client; use core\moodlenet\course_sender; use core\moodlenet\moodlenet_client; +use core\moodlenet\share_recorder; use core\moodlenet\utilities; use core\oauth2\api; use core_external\external_api; @@ -134,10 +135,16 @@ class moodlenet_send_course extends external_api { // Share course. try { + // Record course share progress. + $shareid = share_recorder::insert_share_progress(share_recorder::TYPE_COURSE, $USER->id, $courseid); + $moodlenetclient = new moodlenet_client($client, $oauthclient); $coursesender = new course_sender($courseid, $USER->id, $moodlenetclient, $oauthclient, $shareformat); $result = $coursesender->share_resource(); if (empty($result['drafturl'])) { + + share_recorder::update_share_progress($shareid, share_recorder::STATUS_ERROR); + return self::return_errors( $result['responsecode'], 'errorsendingcourse', @@ -145,6 +152,9 @@ class moodlenet_send_course extends external_api { ); } } catch (\moodle_exception | \JsonException $e) { + + share_recorder::update_share_progress($shareid, share_recorder::STATUS_ERROR); + return self::return_errors( 0, 'errorsendingcourse', @@ -152,6 +162,8 @@ class moodlenet_send_course extends external_api { ); } + share_recorder::update_share_progress($shareid, share_recorder::STATUS_SENT, $result['drafturl']); + return [ 'status' => true, 'resourceurl' => $result['drafturl'], diff --git a/lib/classes/moodlenet/activity_sender.php b/lib/classes/moodlenet/activity_sender.php index b39d57dc9bf..137f36d48f1 100644 --- a/lib/classes/moodlenet/activity_sender.php +++ b/lib/classes/moodlenet/activity_sender.php @@ -123,8 +123,6 @@ class activity_sender extends resource_sender { $responsebody = json_decode($response->getBody()); $resourceurl = $responsebody->homepage ?? ''; - // TODO: Store consumable information about completed share - to be completed in MDL-77296. - // Delete the generated file now it is no longer required. // (It has either been sent, or failed - retries not currently supported). $filedata->delete(); diff --git a/lib/classes/moodlenet/share_recorder.php b/lib/classes/moodlenet/share_recorder.php new file mode 100644 index 00000000000..22d2318b663 --- /dev/null +++ b/lib/classes/moodlenet/share_recorder.php @@ -0,0 +1,132 @@ +. + +namespace core\moodlenet; + +use moodle_exception; +use stdClass; + +/** + * Record the sharing of content to MoodleNet. + * + * @package core + * @copyright 2023 David Woloszyn + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class share_recorder { + + /** + * @var int The content being shared is a course. + */ + public const TYPE_COURSE = 1; + + /** + * @var int The content being shared is an activity. + */ + public const TYPE_ACTIVITY = 2; + + /** + * @var int The status of the share is 'sent'. + */ + public const STATUS_SENT = 1; + + /** + * @var int The status of the share is 'in progress'. + */ + public const STATUS_IN_PROGRESS = 2; + + /** + * @var int The status of the share is 'error'. + */ + public const STATUS_ERROR = 3; + + /** + * Get all allowed share types. + * + * @return array + */ + protected static function get_allowed_share_types(): array { + + return [ + self::TYPE_ACTIVITY, + self::TYPE_COURSE + ]; + } + + /** + * Get all allowed share statuses. + * Note that the particular status values aid in sorting. + * + * @return array + */ + protected static function get_allowed_share_statuses(): array { + + return [ + self::STATUS_SENT, + self::STATUS_IN_PROGRESS, + self::STATUS_ERROR, + ]; + } + + /** + * Create a new share progress record in the DB. + * + * @param int $sharetype The type of share (e.g. TYPE_COURSE). + * @param int $userid The ID of the user performing the share. + * @param int $courseid The associated course id. + * @param int|null $cmid The associated course module id (when sharing activity). + * @return int Returns the inserted record id. + */ + public static function insert_share_progress(int $sharetype, int $userid, int $courseid, ?int $cmid = null): int { + global $DB, $USER; + + if (!in_array($sharetype, self::get_allowed_share_types())) { + throw new moodle_exception('moodlenet:invalidsharetype'); + } + + $data = new stdClass(); + $data->type = $sharetype; + $data->courseid = $courseid; + $data->cmid = $cmid; + $data->userid = $userid; + $data->timecreated = time(); + $data->status = self::STATUS_IN_PROGRESS; + + return $DB->insert_record('moodlenet_share_progress', $data); + } + + /** + * Update the share progress record in the DB. + * + * @param int $shareid The id of the share progress row being updated. + * @param int $status The status of the share progress (e.g. STATUS_SENT). + * @param string|null $resourceurl The resource url returned from MoodleNet. + */ + public static function update_share_progress(int $shareid, int $status, ?string $resourceurl = null): void { + global $DB; + + if (!in_array($status, self::get_allowed_share_statuses())) { + throw new moodle_exception('moodlenet:invalidsharestatus'); + } + + $data = new stdClass(); + $data->id = $shareid; + $data->resourceurl = $resourceurl; + $data->status = $status; + + $DB->update_record('moodlenet_share_progress', $data); + } +} diff --git a/lib/classes/privacy/provider.php b/lib/classes/privacy/provider.php index eea881ec933..48b1e8a0b11 100644 --- a/lib/classes/privacy/provider.php +++ b/lib/classes/privacy/provider.php @@ -32,6 +32,7 @@ use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\userlist; +use core_privacy\local\request\writer; /** * Privacy class for requesting user data. @@ -52,7 +53,7 @@ class provider implements * @return collection The collection object filled out with information about this component. */ public static function get_metadata(collection $collection) : collection { - // These tables are really data about site configuration and not user data. + // Except for moodlenet_share_progress, these tables are really data about site configuration and not user data. // The config_log includes information about which user performed a configuration change. // The value and oldvalue may contain sensitive information such as accounts for service passwords.. @@ -125,6 +126,22 @@ class provider implements 'scopehash' => 'privacy:metadata:oauth2_refresh_token:scopehash' ], 'privacy:metadata:oauth2_refresh_token'); + // The moodlenet_share_progress includes details of an attempted share of a resource to MoodleNet. + $collection->add_database_table('moodlenet_share_progress', [ + 'type' => 'privacy:metadata:moodlenet_share_progress:type', + 'courseid' => 'privacy:metadata:moodlenet_share_progress:courseid', + 'cmid' => 'privacy:metadata:moodlenet_share_progress:cmid', + 'userid' => 'privacy:metadata:moodlenet_share_progress:userid', + 'timecreated' => 'privacy:metadata:moodlenet_share_progress:timecreated', + 'resourceurl' => 'privacy:metadata:moodlenet_share_progress:resourceurl', + 'status' => 'privacy:metadata:moodlenet_share_progress:status', + ], 'privacy:metadata:moodlenet_share_progress'); + + // This resourceurl field is an external link from MoodleNet. + $collection->add_external_location_link('moodlenet_share_progress', [ + 'resourceurl' => 'privacy:metadata:moodlenet_share_progress:resourceurl', + ], 'privacy:metadata:moodlenet_share_progress'); + return $collection; } @@ -135,7 +152,18 @@ class provider implements * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : contextlist { - return new contextlist(); + $contextlist = new contextlist(); + + // MoodleNet share progress uses the user context. + $sql = "SELECT ctx.id + FROM {context} ctx + JOIN {moodlenet_share_progress} msp ON ctx.instanceid = msp.userid + AND ctx.contextlevel = :contextlevel + WHERE msp.userid = :userid"; + $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER]; + $contextlist->add_from_sql($sql, $params); + + return $contextlist; } /** @@ -144,7 +172,18 @@ class provider implements * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { - // Don't add any user. + // Except for moodlenet_share_progress, don't add any users. + $context = $userlist->get_context(); + + // MoodleNet share progress uses the user context. + if ($context->contextlevel == CONTEXT_USER) { + // Get all distinct userids from the table. + $sql = "SELECT DISTINCT userid + FROM {moodlenet_share_progress} + WHERE userid = :userid"; + $params = ['userid' => $context->instanceid]; + $userlist->add_from_sql('userid', $sql, $params); + } } /** @@ -153,7 +192,18 @@ class provider implements * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { - // None of the core tables should be exported. + // Except for moodlenet_share_progress, none of the core tables should be exported. + global $DB; + + foreach ($contextlist as $context) { + // MoodleNet share progress uses the user context. + if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $contextlist->get_user()->id) { + // Get the user's MoodleNet share progress data. + $sharedata = $DB->get_records('moodlenet_share_progress', ['userid' => $context->instanceid]); + $subcontext = get_string('privacy:metadata:moodlenet_share_progress', 'moodle'); + writer::with_context($context)->export_data([$subcontext], (object) $sharedata); + } + } } /** @@ -162,7 +212,13 @@ class provider implements * @param \context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { - // None of the the data from these tables should be deleted. + // Except for moodlenet_share_progress, none of the the data from these tables should be deleted. + global $DB; + + // MoodleNet share progress uses the user context. + if ($context->contextlevel == CONTEXT_USER) { + $DB->delete_records('moodlenet_share_progress', ['userid' => $context->instanceid]); + } } /** @@ -171,9 +227,17 @@ class provider implements * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { - // None of the the data from these tables should be deleted. + // Except for moodlenet_share_progress, none of the the data from these tables should be deleted. // Note: Although it may be tempting to delete the adhoc task data, do not do so. // The delete process is run as an adhoc task. + global $DB; + + foreach ($contextlist as $context) { + // MoodleNet share progress uses the user context. + if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $contextlist->get_user()->id) { + $DB->delete_records('moodlenet_share_progress', ['userid' => $context->instanceid]); + } + } } /** @@ -182,8 +246,20 @@ class provider implements * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { - // None of the the data from these tables should be deleted. + // Except for moodlenet_share_progress, none of the the data from these tables should be deleted. // Note: Although it may be tempting to delete the adhoc task data, do not do so. // The delete process is run as an adhoc task. + global $DB; + + $context = $userlist->get_context(); + + if (!in_array($context->instanceid, $userlist->get_userids())) { + return; + } + + // MoodleNet share progress uses the user context. + if ($context->contextlevel == CONTEXT_USER) { + $DB->delete_records('moodlenet_share_progress', ['userid' => $context->instanceid]); + } } } diff --git a/lib/db/install.xml b/lib/db/install.xml index 4db36cf9eae..fb8b3447c73 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -4820,5 +4820,20 @@ + + + + + + + + + + + + + + +
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 2e1c8c0fb39..d7c933474a9 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -3567,5 +3567,32 @@ privatefiles,moodle|/user/files.php'; upgrade_main_savepoint(true, 2023090100.00); } + if ($oldversion < 2023090200.01) { + + // Define table moodlenet_share_progress to be created. + $table = new xmldb_table('moodlenet_share_progress'); + + // Adding fields to table moodlenet_share_progress. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('type', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('cmid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('resourceurl', XMLDB_TYPE_CHAR, '255', null, null, null, null); + $table->add_field('status', XMLDB_TYPE_INTEGER, '2', null, null, null, null); + + // Adding keys to table moodlenet_share_progress. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Conditionally launch create table for moodlenet_share_progress. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2023090200.01); + } + return true; } diff --git a/lib/tests/moodlenet/share_recorder_test.php b/lib/tests/moodlenet/share_recorder_test.php new file mode 100644 index 00000000000..a1d0cf1fd66 --- /dev/null +++ b/lib/tests/moodlenet/share_recorder_test.php @@ -0,0 +1,134 @@ +. + +namespace core\moodlenet; + +use core\moodlenet\share_recorder; + +/** + * Test coverage for moodlenet share recorder. + * + * @package core + * @copyright 2023 David Woloszyn + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core\moodlenet\share_recorder + */ +class share_recorder_test extends \advanced_testcase { + + /** + * Test inserting and updating an activity share progress to MoodleNet. + * + * @covers ::insert_share_progress + * @covers ::update_share_progress + */ + public function test_activity_share_progress(): void { + global $DB, $USER; + $this->resetAfterTest(); + + $courseid = 10; + $cmid = 20; + $resourceurl = 'https://moodlenet.test/files/testresource.mbz'; + + // Insert the activity share progress and test the returned id. + $shareid = share_recorder::insert_share_progress(share_recorder::TYPE_ACTIVITY, $USER->id, $courseid, $cmid); + $this->assertNotNull($shareid); + + // Test we have set the fields correctly. + $record = $DB->get_record('moodlenet_share_progress', ['id' => $shareid]); + $this->assertEquals(share_recorder::TYPE_ACTIVITY, $record->type); + $this->assertEquals(share_recorder::STATUS_IN_PROGRESS, $record->status); + $this->assertEquals($courseid, $record->courseid); + $this->assertEquals($cmid, $record->cmid); + $this->assertTimeCurrent($record->timecreated); + $this->assertEquals($USER->id, $record->userid); + + // Update the record with the returned data from MoodleNet. + share_recorder::update_share_progress($shareid, share_recorder::STATUS_SENT, $resourceurl); + + // Test we have set the fields correctly. + $record = $DB->get_record('moodlenet_share_progress', ['id' => $shareid]); + $this->assertEquals($resourceurl, $record->resourceurl); + $this->assertEquals(share_recorder::STATUS_SENT, $record->status); + } + + /** + * Test inserting and updating a course share progress to MoodleNet. + * We will also force an error status and test that too. + * + * @covers ::insert_share_progress + * @covers ::update_share_progress + */ + public function test_course_share_progress(): void { + global $DB, $USER; + $this->resetAfterTest(); + + $courseid = 10; + + // Insert the course share progress and test the returned id. + $shareid = share_recorder::insert_share_progress(share_recorder::TYPE_COURSE, $USER->id, $courseid); + $this->assertNotNull($shareid); + + // Test we have set the fields correctly (we expect cmid to be null for course shares). + $record = $DB->get_record('moodlenet_share_progress', ['id' => $shareid]); + $this->assertEquals(share_recorder::TYPE_COURSE, $record->type); + $this->assertEquals(share_recorder::STATUS_IN_PROGRESS, $record->status); + $this->assertEquals($courseid, $record->courseid); + $this->assertNull($record->cmid); + $this->assertTimeCurrent($record->timecreated); + $this->assertEquals($USER->id, $record->userid); + + // Update the record, but let's test with an error status. + share_recorder::update_share_progress($shareid, share_recorder::STATUS_ERROR); + + // Test we have set the field correctly. + $record = $DB->get_record('moodlenet_share_progress', ['id' => $shareid]); + $this->assertEquals(share_recorder::STATUS_ERROR, $record->status); + } + + /** + * Tests the share type is one of the allowed values. + * + * @covers ::get_allowed_share_types + */ + public function test_invalid_share_type(): void { + global $USER; + $this->resetAfterTest(); + + $courseid = 10; + $invalidsharetype = 99; + + $this->expectException(\moodle_exception::class); + share_recorder::insert_share_progress($invalidsharetype, $USER->id, $courseid); + } + + /** + * Tests the share status is one of the allowed values. + * + * @covers ::get_allowed_share_statuses + */ + public function test_invalid_share_status(): void { + global $USER; + $this->resetAfterTest(); + + $courseid = 10; + $invalidsharestatus = 66; + + $recordid = share_recorder::insert_share_progress(share_recorder::TYPE_COURSE, $USER->id, $courseid); + + $this->expectException(\moodle_exception::class); + share_recorder::update_share_progress($recordid, $invalidsharestatus); + } +} diff --git a/lib/tests/privacy/provider_test.php b/lib/tests/privacy/provider_test.php new file mode 100644 index 00000000000..b6ce639eb2a --- /dev/null +++ b/lib/tests/privacy/provider_test.php @@ -0,0 +1,257 @@ +. + +namespace core\privacy; + +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\writer; +use core_privacy\tests\provider_testcase; +use core_privacy\local\request\approved_userlist; +use core\moodlenet\share_recorder; + +/** + * Privacy provider tests class. + * + * @package core + * @category test + * @copyright 2023 David Woloszyn + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider_test extends provider_testcase { + + /** + * Check that a user context is returned if there is any user data for this user. + * + * @covers ::get_contexts_for_userid + */ + public function test_get_contexts_for_userid() { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + + // Check that there are no contexts used for the user yet. + $this->assertEmpty(provider::get_contexts_for_userid($user->id)); + + // Insert a record. + $this->insert_dummy_moodlenet_share_progress_record($user->id); + + // Check that we only get back one context. + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(1, $contextlist); + + // Check that the context returned is the expected one. + $usercontext = \context_user::instance($user->id); + $this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]); + } + + /** + * Test that only users within a user context are fetched. + * + * @covers ::get_users_in_context + */ + public function test_get_users_in_context() { + $this->resetAfterTest(); + + // Create some users. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $usercontext1 = \context_user::instance($user1->id); + $usercontext2 = \context_user::instance($user2->id); + + // Get userlists and check they are empty for now. + $userlist1 = new \core_privacy\local\request\userlist($usercontext1, 'core'); + provider::get_users_in_context($userlist1); + $this->assertCount(0, $userlist1); + + $userlist2 = new \core_privacy\local\request\userlist($usercontext2, 'core'); + provider::get_users_in_context($userlist2); + $this->assertCount(0, $userlist2); + + // Insert records for both users. + $this->insert_dummy_moodlenet_share_progress_record($user1->id); + $this->insert_dummy_moodlenet_share_progress_record($user2->id); + + // Check the userlists contain the correct users. + $userlist1 = new \core_privacy\local\request\userlist($usercontext1, 'core'); + provider::get_users_in_context($userlist1); + $this->assertCount(1, $userlist1); + $this->assertEquals($user1->id, $userlist1->get_userids()[0]); + + $userlist2 = new \core_privacy\local\request\userlist($usercontext2, 'core'); + provider::get_users_in_context($userlist2); + $this->assertCount(1, $userlist2); + $this->assertEquals($user2->id, $userlist2->get_userids()[0]); + } + + /** + * Test that user data is exported correctly. + * + * @covers ::export_user_data + */ + public function test_export_user_data() { + global $DB; + $this->resetAfterTest(); + + // Create some users. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + // Insert a record for each user. + $this->insert_dummy_moodlenet_share_progress_record($user1->id); + $this->insert_dummy_moodlenet_share_progress_record($user2->id); + + $subcontexts = [ + get_string('privacy:metadata:moodlenet_share_progress', 'moodle') + ]; + + // Check if user1 has any exported data yet. + $usercontext1 = \context_user::instance($user1->id); + $writer = writer::with_context($usercontext1); + $this->assertFalse($writer->has_any_data()); + + // Export user1's data and check the count. + $approvedlist = new approved_contextlist($user1, 'core', [$usercontext1->id]); + provider::export_user_data($approvedlist); + $data = (array)$writer->get_data($subcontexts); + $this->assertCount(1, $data); + + // Get the inserted data. + $userdata = $DB->get_record('moodlenet_share_progress', ['userid' => $user1->id]); + + // Check exported data against the inserted data. + $this->assertEquals($userdata->id, reset($data)->id); + $this->assertEquals($userdata->type, reset($data)->type); + $this->assertEquals($userdata->courseid, reset($data)->courseid); + $this->assertEquals($userdata->cmid, reset($data)->cmid); + $this->assertEquals($userdata->userid, reset($data)->userid); + $this->assertEquals($userdata->timecreated, reset($data)->timecreated); + $this->assertEquals($userdata->resourceurl, reset($data)->resourceurl); + $this->assertEquals($userdata->status, reset($data)->status); + } + + /** + * Test deleting all user data for a specific context. + * + * @covers ::delete_data_for_all_users_in_context + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + $this->resetAfterTest(); + + // Create some users. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + // Insert a record for each user. + $this->insert_dummy_moodlenet_share_progress_record($user1->id); + $this->insert_dummy_moodlenet_share_progress_record($user2->id); + + // Get all users' data. + $usersdata = $DB->get_records('moodlenet_share_progress', []); + $this->assertCount(2, $usersdata); + + // Delete everything for a user1 in context. + $usercontext1 = \context_user::instance($user1->id); + provider::delete_data_for_all_users_in_context($usercontext1); + + // Check what is remaining belongs to user2. + $usersdata = $DB->get_records('moodlenet_share_progress', []); + $this->assertCount(1, $usersdata); + $this->assertEquals($user2->id, reset($usersdata)->userid); + } + + /** + * Test deleting a user's data for a specific context. + * + * @covers ::delete_data_for_user + */ + public function test_delete_data_for_user() { + global $DB; + $this->resetAfterTest(); + + // Create some users. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + // Insert a record for each user. + $this->insert_dummy_moodlenet_share_progress_record($user1->id); + $this->insert_dummy_moodlenet_share_progress_record($user2->id); + + // Get all users' data. + $usersdata = $DB->get_records('moodlenet_share_progress', []); + $this->assertCount(2, $usersdata); + + // Delete everything for user1. + $usercontext1 = \context_user::instance($user1->id); + $approvedlist = new approved_contextlist($user1, 'core', [$usercontext1->id]); + provider::delete_data_for_user($approvedlist); + + // Check what is remaining belongs to user2. + $usersdata = $DB->get_records('moodlenet_share_progress', []); + $this->assertCount(1, $usersdata); + $this->assertEquals($user2->id, reset($usersdata)->userid); + } + + /** + * Test that data for users in an approved userlist is deleted. + * + * @covers ::delete_data_for_users + */ + public function test_delete_data_for_users() { + global $DB; + $this->resetAfterTest(); + + // Create some users. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $usercontext1 = \context_user::instance($user1->id); + $usercontext2 = \context_user::instance($user2->id); + + // Insert a record for each user. + $this->insert_dummy_moodlenet_share_progress_record($user1->id); + $this->insert_dummy_moodlenet_share_progress_record($user2->id); + + // Check the count on all user's data. + $usersdata = $DB->get_records('moodlenet_share_progress', []); + $this->assertCount(2, $usersdata); + + // Attempt to delete data for user1 using user2's context (should have no effect). + $approvedlist = new approved_userlist($usercontext2, 'core', [$user1->id]); + provider::delete_data_for_users($approvedlist); + $usersdata = $DB->get_records('moodlenet_share_progress', []); + $this->assertCount(2, $usersdata); + + // Delete data for user1 using its correct context. + $approvedlist = new approved_userlist($usercontext1, 'core', [$user1->id]); + provider::delete_data_for_users($approvedlist); + + // Check what is remaining belongs to user2. + $usersdata = $DB->get_records('moodlenet_share_progress', []); + $this->assertCount(1, $usersdata); + $this->assertEquals($user2->id, reset($usersdata)->userid); + } + + /** + * Helper function to insert a MoodleNet share progress record for use in the tests. + * + * @param int $userid The ID of the user to link the record to. + */ + protected function insert_dummy_moodlenet_share_progress_record(int $userid): void { + $sharetype = share_recorder::TYPE_ACTIVITY; + $courseid = 123; + $cmid = 456; + share_recorder::insert_share_progress($sharetype, $userid, $courseid, $cmid); + } +} diff --git a/version.php b/version.php index f3ab2feb90b..3a0e476ceae 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2023090200.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2023090200.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.3dev+ (Build: 20230902)'; // Human-friendly version name