diff --git a/badges/classes/privacy/provider.php b/badges/classes/privacy/provider.php new file mode 100644 index 00000000000..42c6e9d06e4 --- /dev/null +++ b/badges/classes/privacy/provider.php @@ -0,0 +1,511 @@ +. + +/** + * Data provider. + * + * @package core_badges + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_badges\privacy; +defined('MOODLE_INTERNAL') || die(); + +use badge; +use context; +use context_course; +use context_helper; +use context_system; +use context_user; +use core_text; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; + +require_once($CFG->libdir . '/badgeslib.php'); + +/** + * Data provider class. + * + * @package core_badges + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @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\subsystem\provider { + + /** + * Returns metadata. + * + * @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->add_database_table('badge', [ + 'usercreated' => 'privacy:metadata:badge:usercreated', + 'usermodified' => 'privacy:metadata:badge:usermodified', + 'timecreated' => 'privacy:metadata:badge:timecreated', + 'timemodified' => 'privacy:metadata:badge:timemodified', + ], 'privacy:metadata:badge'); + + $collection->add_database_table('badge_issued', [ + 'userid' => 'privacy:metadata:issued:userid', + 'dateissued' => 'privacy:metadata:issued:dateissued', + 'dateexpire' => 'privacy:metadata:issued:dateexpire', + ], 'privacy:metadata:issued'); + + $collection->add_database_table('badge_criteria_met', [ + 'userid' => 'privacy:metadata:criteriamet:userid', + 'datemet' => 'privacy:metadata:criteriamet:datemet', + ], 'privacy:metadata:criteriamet'); + + $collection->add_database_table('badge_manual_award', [ + 'recipientid' => 'privacy:metadata:manualaward:recipientid', + 'issuerid' => 'privacy:metadata:manualaward:issuerid', + 'issuerrole' => 'privacy:metadata:manualaward:issuerrole', + 'datemet' => 'privacy:metadata:manualaward:datemet', + ], 'privacy:metadata:manualaward'); + + $collection->add_database_table('badge_backpack', [ + 'userid' => 'privacy:metadata:backpack:userid', + 'email' => 'privacy:metadata:backpack:email', + 'backpackurl' => 'privacy:metadata:backpack:backpackurl', + 'backpackuid' => 'privacy:metadata:backpack:backpackuid', + // The columns autosync and password are not used. + ], 'privacy:metadata:backpack'); + + $collection->add_external_location_link('backpacks', [ + 'name' => 'privacy:metadata:external:backpacks:badge', + 'description' => 'privacy:metadata:external:backpacks:description', + 'image' => 'privacy:metadata:external:backpacks:image', + 'url' => 'privacy:metadata:external:backpacks:url', + 'issuer' => 'privacy:metadata:external:backpacks:issuer', + ], 'privacy:metadata:external:backpacks'); + + 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($userid) { + $contextlist = new \core_privacy\local\request\contextlist(); + + // Find the modifications we made on badges (course & system). + $sql = " + SELECT ctx.id + FROM {badge} b + JOIN {context} ctx + ON (b.type = :typecourse AND b.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel) + OR (b.type = :typesite AND ctx.id = :syscontextid) + WHERE b.usermodified = :userid1 + OR b.usercreated = :userid2"; + $params = [ + 'courselevel' => CONTEXT_COURSE, + 'syscontextid' => SYSCONTEXTID, + 'typecourse' => BADGE_TYPE_COURSE, + 'typesite' => BADGE_TYPE_SITE, + 'userid1' => $userid, + 'userid2' => $userid, + ]; + $contextlist->add_from_sql($sql, $params); + + // Find where we've manually awarded a badge (recipient user context). + $sql = " + SELECT ctx.id + FROM {badge_manual_award} bma + JOIN {context} ctx + ON ctx.instanceid = bma.recipientid + AND ctx.contextlevel = :userlevel + WHERE bma.issuerid = :userid"; + $params = [ + 'userlevel' => CONTEXT_USER, + 'userid' => $userid, + ]; + $contextlist->add_from_sql($sql, $params); + + // Now find where there is real user data (user context). + $sql = " + SELECT ctx.id + FROM {context} ctx + LEFT JOIN {badge_manual_award} bma + ON bma.recipientid = ctx.instanceid + LEFT JOIN {badge_issued} bi + ON bi.userid = ctx.instanceid + LEFT JOIN {badge_criteria_met} bcm + ON bcm.userid = ctx.instanceid + LEFT JOIN {badge_backpack} bb + ON bb.userid = ctx.instanceid + WHERE ctx.contextlevel = :userlevel + AND ctx.instanceid = :userid + AND (bma.id IS NOT NULL + OR bi.id IS NOT NULL + OR bcm.id IS NOT NULL + OR bb.id IS NOT NULL)"; + $params = [ + 'userlevel' => CONTEXT_USER, + 'userid' => $userid, + ]; + $contextlist->add_from_sql($sql, $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; + + $userid = $contextlist->get_user()->id; + $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) { + $level = $context->contextlevel; + if ($level == CONTEXT_USER || $level == CONTEXT_COURSE) { + $carry[$level][] = $context->instanceid; + } else if ($level == CONTEXT_SYSTEM) { + $carry[$level] = SYSCONTEXTID; + } + return $carry; + }, [ + CONTEXT_COURSE => [], + CONTEXT_USER => [], + CONTEXT_SYSTEM => null, + ]); + + $path = [get_string('badges', 'core_badges')]; + $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); + + // Export the badges we've created or modified. + if (!empty($contexts[CONTEXT_SYSTEM]) || !empty($contexts[CONTEXT_COURSE])) { + $sqls = []; + $params = []; + + if (!empty($contexts[CONTEXT_SYSTEM])) { + $sqls[] = "type = :typesite"; + $params['typesite'] = BADGE_TYPE_SITE; + } + + if (!empty($contexts[CONTEXT_COURSE])) { + list($insql, $inparams) = $DB->get_in_or_equal($contexts[CONTEXT_COURSE], SQL_PARAMS_NAMED); + $sqls[] = "(type = :typecourse AND courseid $insql)"; + $params = array_merge($params, ['typecourse' => BADGE_TYPE_COURSE], $inparams); + } + + $sqlwhere = '(' . implode(' OR ', $sqls) . ')'; + $sql = " + SELECT *, COALESCE(courseid, 0) AS normalisedcourseid + FROM {badge} + WHERE (usermodified = :userid1 OR usercreated = :userid2) + AND $sqlwhere + ORDER BY courseid, id"; + $params = array_merge($params, ['userid1' => $userid, 'userid2' => $userid]); + $recordset = $DB->get_recordset_sql($sql, $params); + static::recordset_loop_and_export($recordset, 'normalisedcourseid', [], function($carry, $record) use ($userid) { + $carry[] = [ + 'name' => $record->name, + 'created_on' => transform::datetime($record->timecreated), + 'created_by_you' => transform::yesno($record->usercreated == $userid), + 'modified_on' => transform::datetime($record->timemodified), + 'modified_by_you' => transform::yesno($record->usermodified == $userid), + ]; + return $carry; + }, function($courseid, $data) use ($path) { + $context = $courseid ? context_course::instance($courseid) : context_system::instance(); + writer::with_context($context)->export_data($path, (object) ['badges' => $data]); + }); + } + + // Export the badges we've manually awarded. + if (!empty($contexts[CONTEXT_USER])) { + list($insql, $inparams) = $DB->get_in_or_equal($contexts[CONTEXT_USER], SQL_PARAMS_NAMED); + $sql = " + SELECT bma.id, bma.recipientid, bma.datemet, b.name, b.courseid, + r.id AS roleid, + r.name AS rolename, + r.shortname AS roleshortname, + r.archetype AS rolearchetype, + $ctxfields + FROM {badge_manual_award} bma + JOIN {badge} b + ON b.id = bma.badgeid + JOIN {role} r + ON r.id = bma.issuerrole + JOIN {context} ctx + ON (COALESCE(b.courseid, 0) > 0 AND ctx.instanceid = b.courseid AND ctx.contextlevel = :courselevel) + OR (COALESCE(b.courseid, 0) = 0 AND ctx.id = :syscontextid) + WHERE bma.recipientid $insql + AND bma.issuerid = :userid + ORDER BY bma.recipientid, bma.id"; + $params = array_merge($inparams, [ + 'courselevel' => CONTEXT_COURSE, + 'syscontextid' => SYSCONTEXTID, + 'userid' => $userid + ]); + $recordset = $DB->get_recordset_sql($sql, $params); + static::recordset_loop_and_export($recordset, 'recipientid', [], function($carry, $record) use ($userid) { + + // The only reason we fetch the context and role is to format the name of the role, which could be + // different to the standard name if the badge was created in a course. + context_helper::preload_from_record($record); + $context = $record->courseid ? context_course::instance($record->courseid) : context_system::instance(); + $role = (object) [ + 'id' => $record->roleid, + 'name' => $record->rolename, + 'shortname' => $record->roleshortname, + 'archetype' => $record->rolearchetype, + // Mock those two fields as they do not matter. + 'sortorder' => 0, + 'description' => '' + ]; + + $carry[] = [ + 'name' => $record->name, + 'issued_by_you' => transform::yesno(true), + 'issued_on' => transform::datetime($record->datemet), + 'issuer_role' => role_get_name($role, $context), + ]; + return $carry; + }, function($userid, $data) use ($path) { + $context = context_user::instance($userid); + writer::with_context($context)->export_related_data($path, 'manual_awards', (object) ['badges' => $data]); + }); + } + + // Export our data. + if (in_array($userid, $contexts[CONTEXT_USER])) { + + // Export the badges. + $uid = $DB->sql_concat_join("'-'", ['b.id', 'COALESCE(bc.id, 0)', 'COALESCE(bi.id, 0)', + 'COALESCE(bma.id, 0)', 'COALESCE(bcm.id, 0)']); + $sql = " + SELECT $uid AS uid, b.id, + bi.id AS biid, bi.dateissued, bi.dateexpire, bi.uniquehash, + bma.id AS bmaid, bma.datemet, bma.issuerid, + bcm.id AS bcmid, + c.fullname AS coursename, + $ctxfields + FROM {badge} b + LEFT JOIN {badge_issued} bi + ON bi.badgeid = b.id + AND bi.userid = :userid1 + LEFT JOIN {badge_manual_award} bma + ON bma.badgeid = b.id + AND bma.recipientid = :userid2 + LEFT JOIN {badge_criteria} bc + ON bc.badgeid = b.id + LEFT JOIN {badge_criteria_met} bcm + ON bcm.critid = bc.id + AND bcm.userid = :userid3 + LEFT JOIN {course} c + ON c.id = b.courseid + AND b.type = :typecourse + LEFT JOIN {context} ctx + ON ctx.instanceid = c.id + AND ctx.contextlevel = :courselevel + WHERE bi.id IS NOT NULL + OR bma.id IS NOT NULL + OR bcm.id IS NOT NULL + ORDER BY b.id"; + $params = [ + 'userid1' => $userid, + 'userid2' => $userid, + 'userid3' => $userid, + 'courselevel' => CONTEXT_COURSE, + 'typecourse' => BADGE_TYPE_COURSE, + ]; + $recordset = $DB->get_recordset_sql($sql, $params); + static::recordset_loop_and_export($recordset, 'id', null, function($carry, $record) use ($userid) { + $badge = new badge($record->id); + + // Export details of the badge. + if ($carry === null) { + $carry = [ + 'name' => $badge->name, + 'issued' => null, + 'manual_award' => null, + 'criteria_met' => [] + ]; + + if ($badge->type == BADGE_TYPE_COURSE) { + context_helper::preload_from_record($record); + $carry['course'] = format_string($record->coursename, true, ['context' => $badge->get_context()]); + } + + if (!empty($record->biid)) { + $carry['issued'] = [ + 'issued_on' => transform::datetime($record->dateissued), + 'expires_on' => $record->dateexpire ? transform::datetime($record->dateexpire) : null, + 'unique_hash' => $record->uniquehash, + ]; + } + + if (!empty($record->bmaid)) { + $carry['manual_award'] = [ + 'awarded_on' => transform::datetime($record->datemet), + 'issuer' => transform::user($record->issuerid) + ]; + } + } + + // Export the details of the criteria met. + // We only do that once, when we find that a least one criteria was met. + // This is heavily based on the logic present in core_badges_renderer::render_issued_badge. + if (!empty($record->bcmid) && empty($carry['criteria_met'])) { + + $agg = $badge->get_aggregation_methods(); + $evidenceids = array_map(function($record) { + return $record->critid; + }, $badge->get_criteria_completions($userid)); + + $criteria = $badge->criteria; + unset($criteria[BADGE_CRITERIA_TYPE_OVERALL]); + + $items = []; + foreach ($criteria as $type => $c) { + if (in_array($c->id, $evidenceids)) { + $details = $c->get_details(true); + if (count($c->params) == 1) { + $items[] = get_string('criteria_descr_single_' . $type , 'core_badges') . ' ' . $details; + } else { + $items[] = get_string('criteria_descr_' . $type , 'core_badges', + core_text::strtoupper($agg[$badge->get_aggregation_method($type)])) . ' ' . $details; + } + } + } + $carry['criteria_met'] = $items; + } + return $carry; + }, function($badgeid, $data) use ($path, $userid) { + $path = array_merge($path, ["{$data['name']} ({$badgeid})"]); + $writer = writer::with_context(context_user::instance($userid)); + $writer->export_data($path, (object) $data); + $writer->export_area_files($path, 'badges', 'userbadge', $badgeid); + }); + + // Export the backpacks. + $data = []; + $recordset = $DB->get_recordset_select('badge_backpack', 'userid = :userid', ['userid' => $userid]); + foreach ($recordset as $record) { + $data[] = [ + 'email' => $record->email, + 'url' => $record->backpackurl, + 'uid' => $record->backpackuid + ]; + } + $recordset->close(); + if (!empty($data)) { + writer::with_context(context_user::instance($userid))->export_related_data($path, 'backpacks', + (object) ['backpacks' => $data]); + } + } + } + + /** + * Delete all data for all users in the specified context. + * + * @param context $context The specific context to delete data for. + */ + public static function delete_data_for_all_users_in_context(context $context) { + // We cannot delete the course or system data as it is needed by the system. + if ($context->contextlevel != CONTEXT_USER) { + return; + } + + // Delete all the user data. + static::delete_user_data($context->instanceid); + } + + /** + * 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) { + $userid = $contextlist->get_user()->id; + foreach ($contextlist->get_contexts() as $context) { + if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) { + // We can only delete our own data in the user context, nothing in course or system. + static::delete_user_data($userid); + break; + } + } + } + + /** + * Delete all the data for a user. + * + * @param int $userid The user ID. + * @return void + */ + protected static function delete_user_data($userid) { + global $DB; + + // Delete the stuff. + $DB->delete_records('badge_manual_award', ['recipientid' => $userid]); + $DB->delete_records('badge_criteria_met', ['userid' => $userid]); + $DB->delete_records('badge_issued', ['userid' => $userid]); + + // Delete the backpacks and related stuff. + $backpackids = $DB->get_fieldset_select('badge_backpack', 'id', 'userid = :userid', ['userid' => $userid]); + if (!empty($backpackids)) { + list($insql, $inparams) = $DB->get_in_or_equal($backpackids, SQL_PARAMS_NAMED); + $DB->delete_records_select('badge_external', "backpackid $insql", $inparams); + $DB->delete_records_select('badge_backpack', "id $insql", $inparams); + } + } + + /** + * Loop and export from a recordset. + * + * @param \moodle_recordset $recordset The recordset. + * @param string $splitkey The record key to determine when to export. + * @param mixed $initial The initial data to reduce from. + * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. + * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. + * @return void + */ + protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial, + callable $reducer, callable $export) { + + $data = $initial; + $lastid = null; + + foreach ($recordset as $record) { + if ($lastid !== null && $record->{$splitkey} != $lastid) { + $export($lastid, $data); + $data = $initial; + } + $data = $reducer($data, $record); + $lastid = $record->{$splitkey}; + } + $recordset->close(); + + if ($lastid !== null) { + $export($lastid, $data); + } + } +} diff --git a/badges/tests/privacy_test.php b/badges/tests/privacy_test.php new file mode 100644 index 00000000000..bf3b084d381 --- /dev/null +++ b/badges/tests/privacy_test.php @@ -0,0 +1,557 @@ +. + +/** + * Data provider tests. + * + * @package core_badges + * @category test + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +use core_privacy\tests\provider_testcase; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; +use core_badges\privacy\provider; + +require_once($CFG->libdir . '/badgeslib.php'); + +/** + * Data provider testcase class. + * + * @package core_badges + * @category test + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_badges_privacy_testcase extends provider_testcase { + + public function setUp() { + $this->resetAfterTest(); + } + + public function test_get_contexts_for_userid_for_badge_editing() { + $dg = $this->getDataGenerator(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $u4 = $dg->create_user(); + $u5 = $dg->create_user(); + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $sysctx = context_system::instance(); + $c1ctx = context_course::instance($c1->id); + $c2ctx = context_course::instance($c2->id); + + // Assert that we find contexts where we created/modified a badge. + $this->create_badge(['usercreated' => $u1->id, 'usermodified' => $u5->id]); + $this->create_badge(['usercreated' => $u2->id, 'type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]); + $this->create_badge(['usermodified' => $u3->id]); + $this->create_badge(['usermodified' => $u4->id, 'type' => BADGE_TYPE_COURSE, 'courseid' => $c2->id, + 'usercreated' => $u5->id]); + + $contexts = provider::get_contexts_for_userid($u1->id)->get_contextids(); + $this->assertCount(1, $contexts); + $this->assertEquals($sysctx->id, $contexts[0]); + + $contexts = provider::get_contexts_for_userid($u2->id)->get_contextids(); + $this->assertCount(1, $contexts); + $this->assertEquals($c1ctx->id, $contexts[0]); + + $contexts = provider::get_contexts_for_userid($u3->id)->get_contextids(); + $this->assertCount(1, $contexts); + $this->assertEquals($sysctx->id, $contexts[0]); + + $contexts = provider::get_contexts_for_userid($u4->id)->get_contextids(); + $this->assertCount(1, $contexts); + $this->assertEquals($c2ctx->id, $contexts[0]); + + $contexts = provider::get_contexts_for_userid($u5->id)->get_contextids(); + $this->assertCount(2, $contexts); + $this->assertTrue(in_array($sysctx->id, $contexts)); + $this->assertTrue(in_array($c2ctx->id, $contexts)); + } + + public function test_get_contexts_for_userid_for_manual_award() { + global $DB; + + $dg = $this->getDataGenerator(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $u4 = $dg->create_user(); + $c1 = $dg->create_course(); + $sysctx = context_system::instance(); + $c1ctx = context_course::instance($c1->id); + $u1ctx = context_user::instance($u1->id); + $u2ctx = context_user::instance($u2->id); + $u3ctx = context_user::instance($u3->id); + $u4ctx = context_user::instance($u4->id); + $b1 = $this->create_badge(); + $b2 = $this->create_badge(['type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]); + + $this->create_manual_award(['recipientid' => $u4->id, 'issuerid' => $u1->id, 'badgeid' => $b1->id]); + $this->create_manual_award(['recipientid' => $u3->id, 'issuerid' => $u2->id, 'badgeid' => $b1->id]); + $this->create_manual_award(['recipientid' => $u3->id, 'issuerid' => $u2->id, 'badgeid' => $b2->id]); + + $contexts = provider::get_contexts_for_userid($u1->id)->get_contextids(); + $this->assertCount(1, $contexts); + $this->assertEquals($u4ctx->id, $contexts[0]); + + $contexts = provider::get_contexts_for_userid($u2->id)->get_contextids(); + $this->assertCount(1, $contexts); + $this->assertEquals($u3ctx->id, $contexts[0]); + } + + public function test_get_contexts_for_userid_for_my_stuff() { + global $DB; + + $dg = $this->getDataGenerator(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $u4 = $dg->create_user(); + $c1 = $dg->create_course(); + $sysctx = context_system::instance(); + $c1ctx = context_course::instance($c1->id); + $u1ctx = context_user::instance($u1->id); + $u2ctx = context_user::instance($u2->id); + $u3ctx = context_user::instance($u3->id); + $u4ctx = context_user::instance($u4->id); + $b1 = $this->create_badge(); + $b2 = $this->create_badge(['type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]); + + $this->create_backpack(['userid' => $u1->id]); + $this->create_manual_award(['recipientid' => $u2->id, 'badgeid' => $b1->id]); + $this->create_issued(['badgeid' => $b2->id, 'userid' => $u3->id]); + + $crit = $this->create_criteria_manual($b1->id); + $crit->mark_complete($u4->id); + + $contexts = provider::get_contexts_for_userid($u1->id)->get_contextids(); + $this->assertCount(1, $contexts); + $this->assertEquals($u1ctx->id, $contexts[0]); + + $contexts = provider::get_contexts_for_userid($u2->id)->get_contextids(); + $this->assertCount(1, $contexts); + $this->assertEquals($u2ctx->id, $contexts[0]); + + $contexts = provider::get_contexts_for_userid($u3->id)->get_contextids(); + $this->assertCount(1, $contexts); + $this->assertEquals($u3ctx->id, $contexts[0]); + + $contexts = provider::get_contexts_for_userid($u4->id)->get_contextids(); + $this->assertCount(1, $contexts); + $this->assertEquals($u4ctx->id, $contexts[0]); + } + + public function test_delete_data_for_user() { + global $DB; + + $dg = $this->getDataGenerator(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $c1 = $dg->create_course(); + $sysctx = context_system::instance(); + $c1ctx = context_course::instance($c1->id); + $u1ctx = context_user::instance($u1->id); + $u2ctx = context_user::instance($u2->id); + + $b1 = $this->create_badge(['usercreated' => $u1->id, 'usermodified' => $u2->id]); + $b2 = $this->create_badge(['usercreated' => $u2->id, 'usermodified' => $u1->id, + 'type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]); + + $this->create_backpack(['userid' => $u1->id]); + $this->create_backpack(['userid' => $u2->id]); + $this->create_manual_award(['recipientid' => $u1->id, 'badgeid' => $b1->id]); + $this->create_manual_award(['recipientid' => $u2->id, 'badgeid' => $b1->id, 'issuerid' => $u1->id]); + $this->create_issued(['badgeid' => $b2->id, 'userid' => $u1->id]); + $this->create_issued(['badgeid' => $b2->id, 'userid' => $u2->id]); + + $crit = $this->create_criteria_manual($b1->id); + $crit->mark_complete($u2->id); + $crit = $this->create_criteria_manual($b2->id); + $crit->mark_complete($u1->id); + + $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u2->id])); + $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u1->id])); + $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u1->id])); + $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u2->id])); + $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u2->id])); + $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u2->id])); + $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u2->id])); + $this->assertTrue($DB->record_exists('badge_criteria_met', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_criteria_met', ['userid' => $u2->id])); + + provider::delete_data_for_user(new approved_contextlist($u1, 'core_badges', [$sysctx->id, $c1ctx->id, + $u1ctx->id, $u2ctx->id])); + + $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u1->id])); + $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u1->id])); + $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u2->id])); + $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u2->id])); + $this->assertFalse($DB->record_exists('badge_backpack', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u2->id])); + $this->assertFalse($DB->record_exists('badge_manual_award', ['recipientid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u2->id])); + $this->assertFalse($DB->record_exists('badge_issued', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u2->id])); + $this->assertFalse($DB->record_exists('badge_criteria_met', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_criteria_met', ['userid' => $u2->id])); + } + + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $dg = $this->getDataGenerator(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $c1 = $dg->create_course(); + $sysctx = context_system::instance(); + $c1ctx = context_course::instance($c1->id); + $u1ctx = context_user::instance($u1->id); + $u2ctx = context_user::instance($u2->id); + + $b1 = $this->create_badge(['usercreated' => $u1->id, 'usermodified' => $u2->id]); + $b2 = $this->create_badge(['usercreated' => $u2->id, 'usermodified' => $u1->id, + 'type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]); + + $this->create_backpack(['userid' => $u1->id]); + $this->create_backpack(['userid' => $u2->id]); + $this->create_manual_award(['recipientid' => $u1->id, 'badgeid' => $b1->id]); + $this->create_manual_award(['recipientid' => $u2->id, 'badgeid' => $b1->id, 'issuerid' => $u1->id]); + $this->create_issued(['badgeid' => $b2->id, 'userid' => $u1->id]); + $this->create_issued(['badgeid' => $b2->id, 'userid' => $u2->id]); + + $crit = $this->create_criteria_manual($b1->id); + $crit->mark_complete($u2->id); + $crit = $this->create_criteria_manual($b2->id); + $crit->mark_complete($u1->id); + + $assertnochange = function() use ($DB, $u1, $u2) { + $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u2->id])); + $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u1->id])); + $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u1->id])); + $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u2->id])); + $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u2->id])); + $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u2->id])); + $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u2->id])); + $this->assertTrue($DB->record_exists('badge_criteria_met', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_criteria_met', ['userid' => $u2->id])); + }; + $assertnochange(); + + provider::delete_data_for_all_users_in_context($sysctx); + $assertnochange(); + + provider::delete_data_for_all_users_in_context($c1ctx); + $assertnochange(); + + provider::delete_data_for_all_users_in_context($u1ctx); + $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u1->id])); + $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u1->id])); + $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u2->id])); + $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u2->id])); + $this->assertFalse($DB->record_exists('badge_backpack', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u2->id])); + $this->assertFalse($DB->record_exists('badge_manual_award', ['recipientid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u2->id])); + $this->assertFalse($DB->record_exists('badge_issued', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u2->id])); + $this->assertFalse($DB->record_exists('badge_criteria_met', ['userid' => $u1->id])); + $this->assertTrue($DB->record_exists('badge_criteria_met', ['userid' => $u2->id])); + } + + public function test_export_data_for_user() { + global $DB; + + $yes = transform::yesno(true); + $no = transform::yesno(false); + + $dg = $this->getDataGenerator(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $c1 = $dg->create_course(); + $sysctx = context_system::instance(); + $c1ctx = context_course::instance($c1->id); + $u1ctx = context_user::instance($u1->id); + $u2ctx = context_user::instance($u2->id); + + $b1 = $this->create_badge(['usercreated' => $u3->id]); + $b2 = $this->create_badge(['type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id, 'usermodified' => $u3->id]); + $b3 = $this->create_badge(); + $b3crit = $this->create_criteria_manual($b3->id); + $b4 = $this->create_badge(); + + // Create things for user 2, to check it's not exported it. + $this->create_issued(['badgeid' => $b4->id, 'userid' => $u2->id]); + $this->create_backpack(['userid' => $u2->id, 'email' => $u2->email]); + $this->create_manual_award(['badgeid' => $b1->id, 'recipientid' => $u2->id, 'issuerid' => $u3->id]); + + // Create a set of stuff for u1. + $this->create_issued(['badgeid' => $b1->id, 'userid' => $u1->id, 'uniquehash' => 'yoohoo']); + $this->create_manual_award(['badgeid' => $b2->id, 'recipientid' => $u1->id, 'issuerid' => $u3->id]); + $b3crit->mark_complete($u1->id); + $this->create_backpack(['userid' => $u1->id, 'email' => $u1->email]); + + // Check u1. + writer::reset(); + provider::export_user_data(new approved_contextlist($u1, 'core_badges', [$u1ctx->id, $u2ctx->id, + $sysctx->id, $c1ctx->id])); + $this->assertFalse(writer::with_context($u2ctx)->has_any_data()); + $this->assertFalse(writer::with_context($sysctx)->has_any_data()); + $this->assertFalse(writer::with_context($c1ctx)->has_any_data()); + + $path = [get_string('badges', 'core_badges'), "{$b1->name} ({$b1->id})"]; + $data = writer::with_context($u1ctx)->get_data($path); + $this->assertEquals($b1->name, $data->name); + $this->assertNotEmpty($data->issued); + $this->assertEmpty($data->manual_award); + $this->assertEmpty($data->criteria_met); + $this->assertFalse(isset($data->course)); + $this->assertEquals('yoohoo', $data->issued['unique_hash']); + $this->assertNull($data->issued['expires_on']); + + $path = [get_string('badges', 'core_badges'), "{$b2->name} ({$b2->id})"]; + $data = writer::with_context($u1ctx)->get_data($path); + $this->assertEquals($b2->name, $data->name); + $this->assertEmpty($data->issued); + $this->assertNotEmpty($data->manual_award); + $this->assertEmpty($data->criteria_met); + $this->assertEquals($c1->fullname, $data->course); + $this->assertEquals($u3->id, $data->manual_award['issuer']); + + $path = [get_string('badges', 'core_badges'), "{$b3->name} ({$b3->id})"]; + $data = writer::with_context($u1ctx)->get_data($path); + $this->assertEquals($b3->name, $data->name); + $this->assertEmpty($data->issued); + $this->assertEmpty($data->manual_award); + $this->assertNotEmpty($data->criteria_met); + $this->assertNotFalse(strpos($data->criteria_met[0], get_string('criteria_descr_2', 'core_badges', 'ALL'))); + + $path = [get_string('badges', 'core_badges')]; + $data = writer::with_context($u1ctx)->get_related_data($path, 'backpacks'); + $this->assertCount(1, $data->backpacks); + $this->assertEquals($u1->email, $data->backpacks[0]['email']); + + // Confirm we do not have u2. + $path = [get_string('badges', 'core_badges'), "{$b4->name} ({$b4->id})"]; + $data = writer::with_context($u1ctx)->get_data($path); + $this->assertEmpty($data); + $data = writer::with_context($u2ctx)->get_data($path); + $this->assertEmpty($data); + + // Export for u3. + writer::reset(); + $path = [get_string('badges', 'core_badges')]; + provider::export_user_data(new approved_contextlist($u3, 'core_badges', [$u1ctx->id, $u2ctx->id, + $sysctx->id, $c1ctx->id])); + + $data = writer::with_context($u2ctx)->get_related_data($path, 'manual_awards'); + $this->assertCount(1, $data->badges); + $this->assertEquals($b1->name, $data->badges[0]['name']); + $this->assertEquals($yes, $data->badges[0]['issued_by_you']); + $this->assertEquals('Manager', $data->badges[0]['issuer_role']); + + $data = writer::with_context($sysctx)->get_data($path); + $this->assertCount(1, $data->badges); + $this->assertEquals($b1->name, $data->badges[0]['name']); + $this->assertEquals($yes, $data->badges[0]['created_by_you']); + $this->assertEquals($no, $data->badges[0]['modified_by_you']); + + $data = writer::with_context($c1ctx)->get_data($path); + $this->assertCount(1, $data->badges); + $this->assertEquals($b2->name, $data->badges[0]['name']); + $this->assertEquals($no, $data->badges[0]['created_by_you']); + $this->assertEquals($yes, $data->badges[0]['modified_by_you']); + + $data = writer::with_context($u1ctx)->get_related_data($path, 'manual_awards'); + $this->assertCount(1, $data->badges); + $this->assertEquals($b3->name, $data->badges[0]['name']); + $this->assertEquals($yes, $data->badges[0]['issued_by_you']); + $this->assertEquals('Manager', $data->badges[0]['issuer_role']); + } + + /** + * Create a badge. + * + * @param array $params Parameters. + * @return object + */ + protected function create_badge(array $params = []) { + global $DB, $USER; + $record = (object) array_merge([ + 'name' => "Test badge with 'apostrophe' and other friends (<>&@#)", + 'description' => "Testing badges", + 'timecreated' => time(), + 'timemodified' => time(), + 'usercreated' => $USER->id, + 'usermodified' => $USER->id, + 'issuername' => "Test issuer", + 'issuerurl' => "http://issuer-url.domain.co.nz", + 'issuercontact' => "issuer@example.com", + 'expiredate' => null, + 'expireperiod' => null, + 'type' => BADGE_TYPE_SITE, + 'courseid' => null, + 'messagesubject' => "Test message subject", + 'message' => "Test message body", + 'attachment' => 1, + 'notification' => 0, + 'status' => BADGE_STATUS_ACTIVE, + ], $params); + $record->id = $DB->insert_record('badge', $record); + + return $record; + } + + /** + * Create a backpack. + * + * @param array $params Parameters. + * @return object + */ + protected function create_backpack(array $params = []) { + global $DB; + $record = (object) array_merge([ + 'userid' => null, + 'email' => 'test@example.com', + 'backpackurl' => "http://here.there.com", + 'backpackuid' => "12345", + 'autosync' => 0, + 'password' => '', + ], $params); + $record->id = $DB->insert_record('badge_backpack', $record); + return $record; + } + + /** + * Create a criteria of type badge. + * + * @param int $badgeid The badge ID. + * @param array $params Parameters. + * @return object + */ + protected function create_criteria_badge($badgeid, array $params = []) { + $badge = new badge($badgeid); + if (empty($badge->criteria)) { + $overall = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id]); + $overall->save(['agg' => BADGE_CRITERIA_AGGREGATION_ALL]); + } + + $criteria = award_criteria::build([ + 'badgeid' => $badge->id, + 'criteriatype' => BADGE_CRITERIA_TYPE_BADGE, + ]); + + if (isset($params['badgeid'])) { + $params['badge_' . $params['badgeid']] = $params['badgeid']; + unset($params['badgeid']); + } + + $criteria->save($params); + $badge = new badge($badgeid); + return $badge->criteria[BADGE_CRITERIA_TYPE_BADGE]; + } + + /** + * Create a criteria of type manual. + * + * @param int $badgeid The badge ID. + * @param array $params Parameters. + * @return object + */ + protected function create_criteria_manual($badgeid, array $params = []) { + global $DB; + + $badge = new badge($badgeid); + if (empty($badge->criteria)) { + $overall = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id]); + $overall->save(['agg' => BADGE_CRITERIA_AGGREGATION_ALL]); + } + + $criteria = award_criteria::build([ + 'badgeid' => $badge->id, + 'criteriatype' => BADGE_CRITERIA_TYPE_MANUAL, + ]); + + $managerroleid = $DB->get_field_select('role', 'id', 'shortname = ?', ['manager'], IGNORE_MULTIPLE); + if (empty($params)) { + $params = [ + 'role_' . $managerroleid = $managerroleid + ]; + } + + $criteria->save($params); + $badge = new badge($badgeid); + return $badge->criteria[BADGE_CRITERIA_TYPE_MANUAL]; + } + + /** + * Create a badge issued. + * + * @param array $params Parameters. + * @return object + */ + protected function create_issued(array $params = []) { + global $DB, $USER; + $record = (object) array_merge([ + 'badgeid' => null, + 'userid' => null, + 'uniquehash' => random_string(40), + 'dateissued' => time(), + 'dateexpire' => null, + 'visible' => 1, + 'issuernotified' => null, + ], $params); + $record->id = $DB->insert_record('badge_issued', $record); + return $record; + } + + /** + * Create a manual award. + * + * @param array $params Parameters. + * @return object + */ + protected function create_manual_award(array $params = []) { + global $DB, $USER; + $record = (object) array_merge([ + 'badgeid' => null, + 'recipientid' => null, + 'issuerid' => $USER->id, + 'issuerrole' => $DB->get_field_select('role', 'id', 'shortname = ?', ['manager'], IGNORE_MULTIPLE), + 'datemet' => time() + ], $params); + $record->id = $DB->insert_record('badge_manual_award', $record); + return $record; + } + +} diff --git a/lang/en/badges.php b/lang/en/badges.php index 59833af5331..48c1a9e6be8 100644 --- a/lang/en/badges.php +++ b/lang/en/badges.php @@ -362,6 +362,34 @@ $string['numawardstat'] = 'This badge has been issued {$a} user(s).'; $string['overallcrit'] = 'of the selected criteria are complete.'; $string['potentialrecipients'] = 'Potential badge recipients'; $string['preferences'] = 'Badge preferences'; +$string['privacy:metadata:backpack'] = 'A record of user\'s backpacks'; +$string['privacy:metadata:backpack:backpackuid'] = 'The backpack unique identifier'; +$string['privacy:metadata:backpack:backpackurl'] = 'The backpack URL'; +$string['privacy:metadata:backpack:email'] = 'The email associated with the backpack'; +$string['privacy:metadata:backpack:userid'] = 'The ID of the user whose backpack it is'; +$string['privacy:metadata:badge'] = 'A collection of badges'; +$string['privacy:metadata:badge:timecreated'] = 'The date at which the badge was created'; +$string['privacy:metadata:badge:timemodified'] = 'The date at which the badge was last modified'; +$string['privacy:metadata:badge:usercreated'] = 'The ID of the user who created the badge'; +$string['privacy:metadata:badge:usermodified'] = 'The ID of the user who modified the badge'; +$string['privacy:metadata:criteriamet'] = 'A collection of criteria met'; +$string['privacy:metadata:criteriamet:datemet'] = 'The date at which the criteria was met'; +$string['privacy:metadata:criteriamet:userid'] = 'The ID of the user whose criteria is met'; +$string['privacy:metadata:external:backpacks'] = 'Information shared when users submit their badges to an external backpack'; +$string['privacy:metadata:external:backpacks:badge'] = 'The name of the badge'; +$string['privacy:metadata:external:backpacks:description'] = 'The description of the badge'; +$string['privacy:metadata:external:backpacks:image'] = 'The image of the badge'; +$string['privacy:metadata:external:backpacks:issuer'] = 'Some information about the issuer'; +$string['privacy:metadata:external:backpacks:url'] = 'The Moodle URL where the issued badge information can be seen'; +$string['privacy:metadata:issued'] = 'A record of badges awarded'; +$string['privacy:metadata:issued:dateexpire'] = 'The date at which the award expires'; +$string['privacy:metadata:issued:dateissued'] = 'The date of the award'; +$string['privacy:metadata:issued:userid'] = 'The ID of the user who was awarded a badge'; +$string['privacy:metadata:manualaward'] = 'A record of manual awards'; +$string['privacy:metadata:manualaward:datemet'] = 'The date at which the manual award was met'; +$string['privacy:metadata:manualaward:issuerid'] = 'The ID of the user awarding the badge'; +$string['privacy:metadata:manualaward:issuerrole'] = 'The role of the user awarding the badge'; +$string['privacy:metadata:manualaward:recipientid'] = 'The ID of the user who is manually awarded a badge'; $string['recipients'] = 'Badge recipients'; $string['recipientdetails'] = 'Recipient details'; $string['recipientidentificationproblem'] = 'Cannot find a recipient of this badge among the existing users.';