diff --git a/grade/classes/privacy/provider.php b/grade/classes/privacy/provider.php index 1c3b53e0e9672..f51b1c0d329da 100644 --- a/grade/classes/privacy/provider.php +++ b/grade/classes/privacy/provider.php @@ -60,6 +60,7 @@ class provider implements */ public static function get_metadata(collection $collection) { + // Tables without 'real' user information. $collection->add_database_table('grade_outcomes', [ 'timemodified' => 'privacy:metadata:outcomes:timemodified', 'usermodified' => 'privacy:metadata:outcomes:usermodified', @@ -80,6 +81,18 @@ public static function get_metadata(collection $collection) { 'loggeduser' => 'privacy:metadata:history:loggeduser', ], 'privacy:metadata:itemshistory'); + $collection->add_database_table('scale', [ + 'userid' => 'privacy:metadata:scale:userid', + 'timemodified' => 'privacy:metadata:scale:timemodified', + ], 'privacy:metadata:scale'); + + $collection->add_database_table('scale_history', [ + 'userid' => 'privacy:metadata:scale:userid', + 'timemodified' => 'privacy:metadata:history:timemodified', + 'loggeduser' => 'privacy:metadata:history:loggeduser', + ], 'privacy:metadata:scalehistory'); + + // Table with user information. $gradescommonfields = [ 'userid' => 'privacy:metadata:grades:userid', 'usermodified' => 'privacy:metadata:grades:usermodified', @@ -97,16 +110,6 @@ public static function get_metadata(collection $collection) { 'loggeduser' => 'privacy:metadata:history:loggeduser', ]), 'privacy:metadata:gradeshistory'); - $collection->add_database_table('scale_history', [ - 'action' => 'privacy:metadata:scale_history:action', - 'timemodified' => 'privacy:metadata:scale_history:timemodified', - 'loggeduser' => 'privacy:metadata:scale_history:loggeduser', - 'userid' => 'privacy:metadata:scale_history:userid', - 'name' => 'privacy:metadata:scale_history:name', - 'scale' => 'privacy:metadata:scale_history:scale', - 'description' => 'privacy:metadata:scale_history:description' - ], 'privacy:metadata:scale_history'); - // The following tables are reported but not exported/deleted because their data is temporary and only // used during an import. It's content is deleted after a successful, or failed, import. @@ -125,15 +128,6 @@ public static function get_metadata(collection $collection) { 'importonlyfeedback' => 'privacy:metadata:grade_import_values:importonlyfeedback' ], 'privacy:metadata:grade_import_values'); - // Table 'scale' stores userid of the user who created a scale. This is not considered to be user data. - $collection->add_database_table('scale', [ - 'userid' => 'privacy:metadata:scale:userid', - 'name' => 'privacy:metadata:scale:name', - 'scale' => 'privacy:metadata:scale:scale', - 'timemodified' => 'privacy:metadata:scale:timemodified', - 'description' => 'privacy:metadata:scale:description' - ], 'privacy:metadata:scale'); - return $collection; } @@ -152,18 +146,29 @@ public static function get_contexts_for_userid($userid) { FROM {grade_outcomes} go JOIN {context} ctx ON (go.courseid > 0 AND ctx.instanceid = go.courseid AND ctx.contextlevel = :courselevel) - OR (ctx.id = :syscontextid) + OR ((go.courseid IS NULL OR go.courseid < 1) AND ctx.id = :syscontextid) WHERE go.usermodified = :userid"; $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID]; $contextlist->add_from_sql($sql, $params); - // Add where appear in the history of outcomes, categories or items. + // Add where we modified scales. + $sql = " + SELECT DISTINCT ctx.id + FROM {scale} s + JOIN {context} ctx + ON (s.courseid > 0 AND ctx.instanceid = s.courseid AND ctx.contextlevel = :courselevel) + OR (s.courseid = 0 AND ctx.id = :syscontextid) + WHERE s.userid = :userid"; + $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID]; + $contextlist->add_from_sql($sql, $params); + + // Add where appear in the history of outcomes, categories, scales or items. $sql = " SELECT DISTINCT ctx.id FROM {context} ctx LEFT JOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1 AND ( (goh.courseid > 0 AND goh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel1) - OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid) + OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid1) ) LEFT JOIN {grade_categories_history} gch ON gch.loggeduser = :userid2 AND ( gch.courseid = ctx.instanceid @@ -173,17 +178,28 @@ public static function get_contexts_for_userid($userid) { gih.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel3 ) + LEFT JOIN {scale_history} sh + ON (sh.userid = :userid4 OR sh.loggeduser = :userid5) + AND ( + (sh.courseid > 0 AND sh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel4) + OR (sh.courseid = 0 AND ctx.id = :syscontextid2) + ) WHERE goh.id IS NOT NULL OR gch.id IS NOT NULL - OR gih.id IS NOT NULL"; + OR gih.id IS NOT NULL + OR sh.id IS NOT NULL"; $params = [ - 'syscontextid' => SYSCONTEXTID, + 'syscontextid1' => SYSCONTEXTID, + 'syscontextid2' => SYSCONTEXTID, 'courselevel1' => CONTEXT_COURSE, 'courselevel2' => CONTEXT_COURSE, 'courselevel3' => CONTEXT_COURSE, + 'courselevel4' => CONTEXT_COURSE, 'userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid, + 'userid4' => $userid, + 'userid5' => $userid, ]; $contextlist->add_from_sql($sql, $params); @@ -274,6 +290,9 @@ public static function export_user_data(approved_contextlist $contextlist) { // Export the outcomes. static::export_user_data_outcomes_in_contexts($contextlist); + // Export the scales. + static::export_user_data_scales_in_contexts($contextlist); + // Export the historical grades which have become orphans (their grade items were deleted). // We place those in ther user context of the graded user. $userids = array_values(array_map(function($context) { @@ -693,6 +712,100 @@ protected static function export_user_data_outcomes_in_contexts(approved_context }); } + /** + * Export the user data related to scales. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + * @return void + */ + protected static function export_user_data_scales_in_contexts(approved_contextlist $contextlist) { + global $DB; + + $rootpath = [get_string('grades', 'core_grades')]; + $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]); + $userid = $contextlist->get_user()->id; + + // Reorganise the contexts. + $reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_SYSTEM) { + $carry['in_system'] = true; + } else if ($context->contextlevel == CONTEXT_COURSE) { + $carry['courseids'][] = $context->instanceid; + } + return $carry; + }, [ + 'in_system' => false, + 'courseids' => [] + ]); + + // Construct SQL. + $sqltemplateparts = []; + $templateparams = []; + if ($reduced['in_system']) { + $sqltemplateparts[] = '{prefix}.courseid = 0'; + } + if (!empty($reduced['courseids'])) { + list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED); + $sqltemplateparts[] = "{prefix}.courseid $insql"; + $templateparams = array_merge($templateparams, $inparams); + } + if (empty($sqltemplateparts)) { + return; + } + $sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')'; + + // Export edited scales. + $sqlwhere = str_replace('{prefix}', 's', $sqltemplate); + $sql = " + SELECT s.id, s.courseid, s.name, s.timemodified + FROM {scale} s + WHERE $sqlwhere + AND s.userid = :userid + ORDER BY s.courseid, s.timemodified, s.id"; + $params = array_merge($templateparams, ['userid' => $userid]); + $recordset = $DB->get_recordset_sql($sql, $params); + static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) { + $carry[] = [ + 'name' => $record->name, + 'timemodified' => transform::datetime($record->timemodified), + 'created_or_modified_by_you' => transform::yesno(true) + ]; + return $carry; + + }, function($courseid, $data) use ($relatedtomepath) { + $context = $courseid ? context_course::instance($courseid) : context_system::instance(); + writer::with_context($context)->export_related_data($relatedtomepath, 'scales', + (object) ['scales' => $data]); + }); + + // Export edits of scales history. + $sqlwhere = str_replace('{prefix}', 'sh', $sqltemplate); + $sql = " + SELECT sh.id, sh.courseid, sh.name, sh.userid, sh.timemodified, sh.action, sh.loggeduser + FROM {scale_history} sh + WHERE $sqlwhere + AND sh.loggeduser = :userid1 + OR sh.userid = :userid2 + ORDER BY sh.courseid, sh.timemodified, sh.id"; + $params = array_merge($templateparams, ['userid1' => $userid, 'userid2' => $userid]); + $recordset = $DB->get_recordset_sql($sql, $params); + static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) use ($userid) { + $carry[] = [ + 'name' => $record->name, + 'timemodified' => transform::datetime($record->timemodified), + 'author_of_change_was_you' => transform::yesno($record->userid == $userid), + 'author_of_action_was_you' => transform::yesno($record->loggeduser == $userid), + 'action' => static::transform_history_action($record->action) + ]; + return $carry; + + }, function($courseid, $data) use ($relatedtomepath) { + $context = $courseid ? context_course::instance($courseid) : context_system::instance(); + writer::with_context($context)->export_related_data($relatedtomepath, 'scales_history', + (object) ['modified_records' => $data]); + }); + } + /** * Extract grade_grade from a record. * diff --git a/grade/tests/privacy_test.php b/grade/tests/privacy_test.php index 83605a11c0030..f7dfc15c569fd 100644 --- a/grade/tests/privacy_test.php +++ b/grade/tests/privacy_test.php @@ -64,6 +64,11 @@ public function test_get_contexts_for_userid_gradebook_edits() { $u4 = $dg->create_user(); $u5 = $dg->create_user(); $u6 = $dg->create_user(); + $u7 = $dg->create_user(); + $u8 = $dg->create_user(); + $u9 = $dg->create_user(); + $u10 = $dg->create_user(); + $u11 = $dg->create_user(); $sysctx = context_system::instance(); $c1ctx = context_course::instance($c1->id); @@ -80,16 +85,22 @@ public function test_get_contexts_for_userid_gradebook_edits() { 'fullname' => 'go2']), false); // Nothing as of now. - foreach ([$u1, $u2, $u3, $u4] as $u) { + foreach ([$u1, $u2, $u3, $u4, $u5, $u6, $u7, $u8, $u9, $u10, $u11] as $u) { $contexts = array_flip(provider::get_contexts_for_userid($u->id)->get_contextids()); $this->assertEmpty($contexts); } $go0 = new grade_outcome(['shortname' => 'go0', 'fullname' => 'go0', 'usermodified' => $u1->id]); $go0->insert(); - $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u1->id]); + $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u11->id]); $go1->insert(); + // Create scales. + $s1 = new grade_scale(['name' => 's1', 'scale' => 'a,b', 'userid' => $u7->id, 'courseid' => 0, 'description' => '']); + $s1->insert(); + $s2 = new grade_scale(['name' => 's2', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c1->id, 'description' => '']); + $s2->insert(); + // User 2 creates history. $this->setUser($u2); $go0->shortname .= ' edited'; @@ -118,11 +129,18 @@ public function test_get_contexts_for_userid_gradebook_edits() { $this->setUser($u6); $gi2a->delete(); + // User 9 creates history. + $this->setUser($u9); + $s1->name .= ' edited'; + $s1->update(); + // Assert contexts. $contexts = array_flip(provider::get_contexts_for_userid($u1->id)->get_contextids()); - $this->assertCount(2, $contexts); - $this->assertArrayHasKey($c1ctx->id, $contexts); + $this->assertCount(1, $contexts); $this->assertArrayHasKey($sysctx->id, $contexts); + $contexts = array_flip(provider::get_contexts_for_userid($u11->id)->get_contextids()); + $this->assertCount(1, $contexts); + $this->assertArrayHasKey($c1ctx->id, $contexts); $contexts = array_flip(provider::get_contexts_for_userid($u2->id)->get_contextids()); $this->assertCount(2, $contexts); $this->assertArrayHasKey($sysctx->id, $contexts); @@ -140,6 +158,23 @@ public function test_get_contexts_for_userid_gradebook_edits() { $contexts = array_flip(provider::get_contexts_for_userid($u6->id)->get_contextids()); $this->assertCount(1, $contexts); $this->assertArrayHasKey($c2ctx->id, $contexts); + $contexts = array_flip(provider::get_contexts_for_userid($u7->id)->get_contextids()); + $this->assertCount(1, $contexts); + $this->assertArrayHasKey($sysctx->id, $contexts); + $contexts = array_flip(provider::get_contexts_for_userid($u8->id)->get_contextids()); + $this->assertCount(1, $contexts); + $this->assertArrayHasKey($c1ctx->id, $contexts); + $contexts = array_flip(provider::get_contexts_for_userid($u9->id)->get_contextids()); + $this->assertCount(1, $contexts); + $this->assertArrayHasKey($sysctx->id, $contexts); + + // User 10 creates history. + $this->setUser($u10); + $s2->delete(); + + $contexts = array_flip(provider::get_contexts_for_userid($u10->id)->get_contextids()); + $this->assertCount(1, $contexts); + $this->assertArrayHasKey($c1ctx->id, $contexts); } public function test_get_contexts_for_userid_grades_and_history() { @@ -609,6 +644,10 @@ public function test_export_data_for_user_about_gradebook_edits() { $u4 = $dg->create_user(); $u5 = $dg->create_user(); $u6 = $dg->create_user(); + $u7 = $dg->create_user(); + $u8 = $dg->create_user(); + $u9 = $dg->create_user(); + $u10 = $dg->create_user(); $sysctx = context_system::instance(); $u1ctx = context_user::instance($u1->id); @@ -641,6 +680,14 @@ public function test_export_data_for_user_about_gradebook_edits() { $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u1->id]); $go1->insert(); + // Create scales. + $s1 = new grade_scale(['name' => 's1', 'scale' => 'a,b', 'userid' => $u7->id, 'courseid' => 0, 'description' => '']); + $s1->insert(); + $s2 = new grade_scale(['name' => 's2', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c1->id, 'description' => '']); + $s2->insert(); + $s3 = new grade_scale(['name' => 's3', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c2->id, 'description' => '']); + $s3->insert(); + // User 2 creates history. $this->setUser($u2); $go0->shortname .= ' edited'; @@ -669,6 +716,15 @@ public function test_export_data_for_user_about_gradebook_edits() { $this->setUser($u6); $gi2a->delete(); + // User 9 creates history. + $this->setUser($u9); + $s1->name .= ' edited'; + $s1->update(); + + // User 10 creates history. + $this->setUser($u10); + $s3->delete(); + $this->setAdminUser(); // Export data for u1. @@ -755,6 +811,74 @@ public function test_export_data_for_user_about_gradebook_edits() { $this->assertEquals(transform::yesno(true), $data->modified_records[0]['logged_in_user_was_you']); $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'), $data->modified_records[0]['action']); + + // Export data for u7. + writer::reset(); + provider::export_user_data(new approved_contextlist($u7, 'core_grades', $allcontexts)); + $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertCount(1, $data->scales); + $this->assertEquals($s1->name, $data->scales[0]['name']); + $this->assertEquals(transform::yesno(true), $data->scales[0]['created_or_modified_by_you']); + + // Export data for u8. + writer::reset(); + provider::export_user_data(new approved_contextlist($u8, 'core_grades', $allcontexts)); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertCount(1, $data->scales); + $this->assertEquals($s2->name, $data->scales[0]['name']); + $this->assertEquals(transform::yesno(true), $data->scales[0]['created_or_modified_by_you']); + $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales_history'); + $this->assertCount(2, $data->modified_records); + $this->assertEquals($s3->name, $data->modified_records[0]['name']); + $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_change_was_you']); + $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_action_was_you']); + $this->assertEquals(get_string('privacy:request:historyactioninsert', 'core_grades'), + $data->modified_records[0]['action']); + $this->assertEquals($s3->name, $data->modified_records[1]['name']); + $this->assertEquals(transform::yesno(true), $data->modified_records[1]['author_of_change_was_you']); + $this->assertEquals(transform::yesno(false), $data->modified_records[1]['author_of_action_was_you']); + $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'), + $data->modified_records[1]['action']); + + // Export data for u9. + writer::reset(); + provider::export_user_data(new approved_contextlist($u9, 'core_grades', $allcontexts)); + $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales_history'); + $this->assertCount(1, $data->modified_records); + $this->assertEquals($s1->name, $data->modified_records[0]['name']); + $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_change_was_you']); + $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_action_was_you']); + $this->assertEquals(get_string('privacy:request:historyactionupdate', 'core_grades'), + $data->modified_records[0]['action']); + + // Export data for u10. + writer::reset(); + provider::export_user_data(new approved_contextlist($u10, 'core_grades', $allcontexts)); + $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales_history'); + $this->assertEmpty($data); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales_history'); + $this->assertEmpty($data); + $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales_history'); + $this->assertCount(1, $data->modified_records); + $this->assertEquals($s3->name, $data->modified_records[0]['name']); + $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_change_was_you']); + $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_action_was_you']); + $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'), + $data->modified_records[0]['action']); } /** diff --git a/lang/en/grades.php b/lang/en/grades.php index cce740bab4e32..2153ef4855bc4 100644 --- a/lang/en/grades.php +++ b/lang/en/grades.php @@ -324,31 +324,6 @@ $string['graderreport'] = 'Grader report'; $string['grades'] = 'Grades'; $string['gradesforuser'] = 'Grades for {$a->user}'; -$string['privacy:metadata:grade_import_newitem'] = 'Temporary table for storing new grade_item names from grade import'; -$string['privacy:metadata:grade_import_newitem:importcode'] = 'A unique batch code for identifying one batch of imports'; -$string['privacy:metadata:grade_import_newitem:importer'] = 'User importing the data'; -$string['privacy:metadata:grade_import_newitem:itemname'] = 'New grade item name'; -$string['privacy:metadata:grade_import_values'] = 'Temporary table for importing grades'; -$string['privacy:metadata:grade_import_values:feedback'] = 'Grade feedback'; -$string['privacy:metadata:grade_import_values:finalgrade'] = 'Raw grade value'; -$string['privacy:metadata:grade_import_values:importcode'] = 'A unique batch code for identifying one batch of imports'; -$string['privacy:metadata:grade_import_values:importer'] = 'User importing the data'; -$string['privacy:metadata:grade_import_values:importonlyfeedback'] = 'Flag if only feedback was imported'; -$string['privacy:metadata:grade_import_values:userid'] = 'User whose grade was imported'; -$string['privacy:metadata:scale'] = 'Grading scales, store ID of the user who created the scale. Not considered personal information'; -$string['privacy:metadata:scale:description'] = 'Description of the scale'; -$string['privacy:metadata:scale:name'] = 'Name of the scale'; -$string['privacy:metadata:scale:scale'] = 'Values in the scale'; -$string['privacy:metadata:scale:timemodified'] = 'Time when the scale was last modified'; -$string['privacy:metadata:scale:userid'] = 'ID of the user who created the scale'; -$string['privacy:metadata:scale_history'] = 'History table'; -$string['privacy:metadata:scale_history:action'] = 'created/modified/deleted constants'; -$string['privacy:metadata:scale_history:description'] = 'description'; -$string['privacy:metadata:scale_history:loggeduser'] = 'the userid of the person who last modified this outcome'; -$string['privacy:metadata:scale_history:name'] = 'name'; -$string['privacy:metadata:scale_history:scale'] = 'scale'; -$string['privacy:metadata:scale_history:timemodified'] = 'The last time this grade_item was modified'; -$string['privacy:metadata:scale_history:userid'] = 'userid'; $string['singleview'] = 'Single view for {$a}'; $string['gradesonly'] = 'Change to grades only'; $string['gradesmoduledeletionpendingwarning'] = 'Warning: Activity deletion in progress! Some grades are about to be removed.'; @@ -633,6 +608,17 @@ $string['prefshow'] = 'Show/hide toggles'; $string['previewrows'] = 'Preview rows'; $string['privacy:metadata:categorieshistory'] = 'A record of previous versions of grade categories'; +$string['privacy:metadata:grade_import_newitem'] = 'Temporary table for storing new grade_item names from grade import'; +$string['privacy:metadata:grade_import_newitem:importcode'] = 'A unique batch code for identifying one batch of imports'; +$string['privacy:metadata:grade_import_newitem:importer'] = 'User importing the data'; +$string['privacy:metadata:grade_import_newitem:itemname'] = 'New grade item name'; +$string['privacy:metadata:grade_import_values'] = 'Temporary table for importing grades'; +$string['privacy:metadata:grade_import_values:feedback'] = 'Grade feedback'; +$string['privacy:metadata:grade_import_values:finalgrade'] = 'Raw grade value'; +$string['privacy:metadata:grade_import_values:importcode'] = 'A unique batch code for identifying one batch of imports'; +$string['privacy:metadata:grade_import_values:importer'] = 'User importing the data'; +$string['privacy:metadata:grade_import_values:importonlyfeedback'] = 'Flag if only feedback was imported'; +$string['privacy:metadata:grade_import_values:userid'] = 'User whose grade was imported'; $string['privacy:metadata:grades'] = 'A record of grades'; $string['privacy:metadata:grades:aggregationstatus'] = 'The aggregation status'; $string['privacy:metadata:grades:aggregationweight'] = 'The weight in aggregation'; @@ -650,6 +636,10 @@ $string['privacy:metadata:outcomes:timemodified'] = 'Time at which the record was modified'; $string['privacy:metadata:outcomes:usermodified'] = 'The user who last modified the record'; $string['privacy:metadata:outcomeshistory'] = 'A record of previous versions of outcomes'; +$string['privacy:metadata:scale'] = 'A record of scales'; +$string['privacy:metadata:scale:timemodified'] = 'Time at which the record was last modified'; +$string['privacy:metadata:scale:userid'] = 'The user who last modified the record'; +$string['privacy:metadata:scalehistory'] = 'A record of previous versions of scales'; $string['privacy:path:relatedtome'] = 'Related to me'; $string['privacy:request:historyactiondelete'] = 'Delete'; $string['privacy:request:historyactioninsert'] = 'Insert';