Merge branch 'MDL-62167-master' of git://github.com/FMCorz/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 1 May 2018 03:10:31 +0000 (11:10 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 1 May 2018 03:10:31 +0000 (11:10 +0800)
badges/classes/privacy/provider.php [new file with mode: 0644]
badges/tests/privacy_test.php [new file with mode: 0644]
lang/en/badges.php

diff --git a/badges/classes/privacy/provider.php b/badges/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..29939af
--- /dev/null
@@ -0,0 +1,511 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Data provider.
+ *
+ * @package    core_badges
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @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 <fred@branchup.tech>
+ * @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 {
+
+        $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(int $userid) : \core_privacy\local\request\contextlist {
+        $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 (file)
index 0000000..bf3b084
--- /dev/null
@@ -0,0 +1,557 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Data provider tests.
+ *
+ * @package    core_badges
+ * @category   test
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @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 <fred@branchup.tech>
+ * @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;
+    }
+
+}
index cb7597e..0b92684 100644 (file)
@@ -388,6 +388,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.';