Merge branch 'MDL-63564-master' of git://github.com/mihailges/moodle

This commit is contained in:
Andrew Nicols 2018-11-05 12:57:48 +08:00
commit 5b2856864d
7 changed files with 488 additions and 16 deletions

View File

@ -23,6 +23,8 @@
*/
namespace mod_quiz\privacy;
use core_privacy\local\request\approved_userlist;
defined('MOODLE_INTERNAL') || die();
/**
@ -63,4 +65,13 @@ trait legacy_quizaccess_polyfill {
public static function delete_quizaccess_data_for_user(\quiz $quiz, \stdClass $user) {
static::_delete_quizaccess_data_for_user($quiz, $user);
}
/**
* Delete all user data for the specified users, in the specified context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_quizaccess_data_for_users(approved_userlist $userlist) {
static::_delete_quizaccess_data_for_users($userlist);
}
}

View File

@ -18,19 +18,22 @@
* Privacy Subsystem implementation for mod_quiz.
*
* @package mod_quiz
* @category privacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\privacy;
use \core_privacy\local\request\writer;
use \core_privacy\local\request\transform;
use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\deletion_criteria;
use \core_privacy\local\metadata\collection;
use \core_privacy\manager;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\deletion_criteria;
use core_privacy\local\request\transform;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
use core_privacy\manager;
defined('MOODLE_INTERNAL') || die();
@ -48,7 +51,10 @@ class provider implements
\core_privacy\local\metadata\provider,
// This plugin currently implements the original plugin_provider interface.
\core_privacy\local\request\plugin\provider {
\core_privacy\local\request\plugin\provider,
// This plugin is capable of determining which users have data within it.
\core_privacy\local\request\core_userlist_provider {
/**
* Get the list of contexts that contain user information for the specified user.
@ -176,6 +182,52 @@ class provider implements
return $resultset;
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
if (!$context instanceof \context_module) {
return;
}
$params = [
'cmid' => $context->instanceid,
'modname' => 'quiz',
];
// Users who attempted the quiz.
$sql = "SELECT qa.userid
FROM {course_modules} cm
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {quiz} q ON q.id = cm.instance
JOIN {quiz_attempts} qa ON qa.quiz = q.id
WHERE cm.id = :cmid AND qa.preview = 0";
$userlist->add_from_sql('userid', $sql, $params);
// Users with quiz overrides.
$sql = "SELECT qo.userid
FROM {course_modules} cm
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {quiz} q ON q.id = cm.instance
JOIN {quiz_overrides} qo ON qo.quiz = q.id
WHERE cm.id = :cmid";
$userlist->add_from_sql('userid', $sql, $params);
// Question usages in context.
// This includes where a user is the manual marker on a question attempt.
$sql = "SELECT qa.uniqueid
FROM {course_modules} cm
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {quiz} q ON q.id = cm.instance
JOIN {quiz_attempts} qa ON qa.quiz = q.id
WHERE cm.id = :cmid AND qa.preview = 0";
\core_question\privacy\provider::get_users_in_context_from_sql($userlist, 'qn', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
@ -366,6 +418,56 @@ class provider implements
}
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
global $DB;
$context = $userlist->get_context();
if ($context->contextlevel != CONTEXT_MODULE) {
// Only quiz module will be handled.
return;
}
$cm = get_coursemodule_from_id('quiz', $context->instanceid);
if (!$cm) {
// Only quiz module will be handled.
return;
}
$quizobj = \quiz::create($cm->instance);
$quiz = $quizobj->get_quiz();
$userids = $userlist->get_userids();
// Handle the 'quizaccess' quizaccess.
manager::plugintype_class_callback(
'quizaccess',
quizaccess_user_provider::class,
'delete_quizaccess_data_for_users',
[$userlist]
);
foreach ($userids as $userid) {
// Remove overrides for this user.
$overrides = $DB->get_records('quiz_overrides' , [
'quiz' => $quizobj->get_quizid(),
'userid' => $userid,
]);
foreach ($overrides as $override) {
quiz_delete_override($quiz, $override->id, false);
}
// This will delete all question attempts, quiz attempts, and quiz grades for this user in the given quiz.
quiz_delete_user_attempts($quizobj, (object)['id' => $userid]);
}
}
/**
* Store all quiz attempts for the contextlist.
*

View File

@ -0,0 +1,41 @@
<?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/>.
/**
* The quizaccess_user_provider interface provides the expected interface for all 'quizaccess' quizaccesss.
*
* Quiz sub plugins should implement this if they store personal information and can retrieve a userid.
*
* @package mod_quiz
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\privacy;
defined('MOODLE_INTERNAL') || die();
use core_privacy\local\request\approved_userlist;
interface quizaccess_user_provider extends \core_privacy\local\request\plugin\subplugin_provider {
/**
* Delete multiple users data within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_quizaccess_data_for_users(approved_userlist $userlist);
}

View File

@ -73,7 +73,7 @@ class core_privacy_legacy_quizaccess_polyfill_test extends advanced_testcase {
}
/**
* Test the _delete_quizaccess_for_context shim.
* Test the _delete_quizaccess_for_user shim.
*/
public function test_delete_quizaccess_for_user() {
$context = context_system::instance();
@ -89,6 +89,23 @@ class core_privacy_legacy_quizaccess_polyfill_test extends advanced_testcase {
test_privacy_legacy_quizaccess_polyfill_provider::$mock = $mock;
test_privacy_legacy_quizaccess_polyfill_provider::delete_quizaccess_data_for_user($quiz, $user);
}
/**
* Test the _delete_quizaccess_for_users shim.
*/
public function test_delete_quizaccess_for_users() {
$context = $this->createMock(context_module::class);
$user = (object) [];
$approveduserlist = new \core_privacy\local\request\approved_userlist($context, 'mod_quiz', [$user]);
$mock = $this->createMock(test_privacy_legacy_quizaccess_polyfill_mock_wrapper::class);
$mock->expects($this->once())
->method('get_return_value')
->with('_delete_quizaccess_data_for_users', [$approveduserlist]);
test_privacy_legacy_quizaccess_polyfill_provider::$mock = $mock;
test_privacy_legacy_quizaccess_polyfill_provider::delete_quizaccess_data_for_users($approveduserlist);
}
}
/**
@ -99,7 +116,8 @@ class core_privacy_legacy_quizaccess_polyfill_test extends advanced_testcase {
*/
class test_privacy_legacy_quizaccess_polyfill_provider implements
\core_privacy\local\metadata\provider,
\mod_quiz\privacy\quizaccess_provider {
\mod_quiz\privacy\quizaccess_provider,
\mod_quiz\privacy\quizaccess_user_provider {
use \mod_quiz\privacy\legacy_quizaccess_polyfill;
use \core_privacy\local\legacy_polyfill;
@ -138,6 +156,15 @@ class test_privacy_legacy_quizaccess_polyfill_provider implements
static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
/**
* Delete all user data for the specified users, in the specified context.
*
* @param \core_privacy\local\request\approved_userlist $userlist
*/
protected static function _delete_quizaccess_data_for_users($userlist) {
static::$mock->get_return_value(__FUNCTION__, func_get_args());
}
/**
* Returns metadata about this plugin.
*

View File

@ -457,4 +457,99 @@ class mod_quiz_privacy_provider_testcase extends \core_privacy\tests\provider_te
return [$quizobj, $quba, $attemptobj];
}
/**
* Test for provider::get_users_in_context().
*/
public function test_get_users_in_context() {
global $DB;
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$anotheruser = $this->getDataGenerator()->create_user();
$extrauser = $this->getDataGenerator()->create_user();
// Make a quiz.
$this->setUser();
$quiz = $this->create_test_quiz($course);
// Create an override for user1.
$DB->insert_record('quiz_overrides', [
'quiz' => $quiz->id,
'userid' => $user->id,
'timeclose' => 1300,
'timelimit' => null,
]);
// Make an attempt on the quiz as user2.
list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $anotheruser);
$context = $quizobj->get_context();
// Fetch users - user1 and user2 should be returned.
$userlist = new \core_privacy\local\request\userlist($context, 'mod_quiz');
\mod_quiz\privacy\provider::get_users_in_context($userlist);
$this->assertEquals(
[$user->id, $anotheruser->id],
$userlist->get_userids(),
'', 0.0, 10, true);
}
/**
* Test for provider::delete_data_for_users().
*/
public function test_delete_data_for_users() {
global $DB;
$this->resetAfterTest(true);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
// Make a quiz in each course.
$quiz1 = $this->create_test_quiz($course1);
$quiz2 = $this->create_test_quiz($course2);
// Attempt quiz1 as user1 and user2.
list($quiz1obj) = $this->attempt_quiz($quiz1, $user1);
$this->attempt_quiz($quiz1, $user2);
// Create an override in quiz1 for user3.
$DB->insert_record('quiz_overrides', [
'quiz' => $quiz1->id,
'userid' => $user3->id,
'timeclose' => 1300,
'timelimit' => null,
]);
// Attempt quiz2 as user1.
$this->attempt_quiz($quiz2, $user1);
// Delete the data for user1 and user3 in course1 and check it is removed.
$quiz1context = $quiz1obj->get_context();
$approveduserlist = new \core_privacy\local\request\approved_userlist($quiz1context, 'mod_quiz',
[$user1->id, $user3->id]);
provider::delete_data_for_users($approveduserlist);
// Only the attempt of user2 should be remained in quiz1.
$this->assertEquals(
[$user2->id],
$DB->get_fieldset_select('quiz_attempts', 'userid', 'quiz = ?', [$quiz1->id])
);
// The attempt that user1 made in quiz2 should be remained.
$this->assertEquals(
[$user1->id],
$DB->get_fieldset_select('quiz_attempts', 'userid', 'quiz = ?', [$quiz2->id])
);
// The quiz override in quiz1 that we had for user3 should be deleted.
$this->assertEquals(
[],
$DB->get_fieldset_select('quiz_overrides', 'userid', 'quiz = ?', [$quiz1->id])
);
}
}

View File

@ -18,17 +18,20 @@
* Privacy Subsystem implementation for core_question.
*
* @package core_question
* @category privacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\privacy;
use \core_privacy\local\metadata\collection;
use \core_privacy\local\request\writer;
use \core_privacy\local\request\transform;
use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\approved_contextlist;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
defined('MOODLE_INTERNAL') || die();
@ -55,7 +58,10 @@ class provider implements
\core_privacy\local\request\subsystem\provider,
// This is a subsysytem which provides information to plugins.
\core_privacy\local\request\subsystem\plugin_provider
\core_privacy\local\request\subsystem\plugin_provider,
// This plugin is capable of determining which users have data within it.
\core_privacy\local\request\core_userlist_provider
{
/**
@ -344,6 +350,30 @@ class provider implements
return $contextlist;
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
// A user may have created or updated a question.
// Questions are linked against a question category, which has a contextid field.
$sql = "SELECT q.createdby, q.modifiedby
FROM {question} q
JOIN {question_categories} cat
ON cat.id = q.category
WHERE cat.contextid = :contextid";
$params = [
'contextid' => $context->id
];
$userlist->add_from_sql('createdby', $sql, $params);
$userlist->add_from_sql('modifiedby', $sql, $params);
}
/**
* Determine related question usages for a user.
*
@ -367,6 +397,32 @@ class provider implements
]);
}
/**
* Add the list of users who have rated in the specified constraints.
*
* @param userlist $userlist The userlist to add the users to.
* @param string $prefix A unique prefix to add to the table alias to avoid interference with your own sql.
* @param string $insql The SQL to use in a sub-select for the question_usages.id query.
* @param array $params The params required for the insql.
* @param int|null $contextid An optional context id, in case the $sql query is not already filtered by that.
*/
public static function get_users_in_context_from_sql(userlist $userlist, string $prefix, string $insql, $params,
int $contextid = null) {
$sql = "SELECT {$prefix}_qas.userid
FROM {question_attempt_steps} {$prefix}_qas
JOIN {question_attempts} {$prefix}_qa ON {$prefix}_qas.questionattemptid = {$prefix}_qa.id
JOIN {question_usages} {$prefix}_qu ON {$prefix}_qa.questionusageid = {$prefix}_qu.id
WHERE {$prefix}_qu.id IN ({$insql})";
if ($contextid) {
$sql .= " AND {$prefix}_qu.contextid = :{$prefix}_contextid";
$params["{$prefix}_contextid"] = $contextid;
}
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
@ -476,4 +532,33 @@ class provider implements
category IN (SELECT id FROM {question_categories} WHERE contextid {$contextsql})
AND modifiedby = :modifiedby", $contextparams);
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
global $DB;
// Questions are considered to be 'owned' by the institution, even if they were originally written by a specific
// user. They are still exported in the list of a users data, but they are not removed.
// The userid is instead anonymised.
$context = $userlist->get_context();
$userids = $userlist->get_userids();
list($createdbysql, $createdbyparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
list($modifiedbysql, $modifiedbyparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$params = ['contextid' => $context->id];
$DB->set_field_select('question', 'createdby', 0, "
category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)
AND createdby {$createdbysql}", $params + $createdbyparams);
$DB->set_field_select('question', 'modifiedby', 0, "
category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)
AND modifiedby {$modifiedbysql}", $params + $modifiedbyparams);
}
}

View File

@ -425,4 +425,115 @@ class core_question_privacy_provider_testcase extends \core_privacy\tests\provid
$this->assertEquals($user->id, $qrecord->createdby);
$this->assertEquals($user->id, $qrecord->modifiedby);
}
/**
* Test for provider::get_users_in_context().
*/
public function test_get_users_in_context() {
$this->resetAfterTest();
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
// Create three test users.
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
// Create one question as each user in different contexts.
$this->setUser($user1);
$user1data = $questiongenerator->setup_course_and_questions();
$this->setUser($user2);
$user2data = $questiongenerator->setup_course_and_questions();
$course1context = \context_course::instance($user1data[1]->id);
$course1questions = $user1data[3];
// Log in as user3 and update the questions in course1.
$this->setUser($user3);
foreach ($course1questions as $question) {
$questiongenerator->update_question($question);
}
$userlist = new \core_privacy\local\request\userlist($course1context, 'core_question');
provider::get_users_in_context($userlist);
// User1 has created questions and user3 has edited them.
$this->assertCount(2, $userlist);
$this->assertEquals(
[$user1->id, $user3->id],
$userlist->get_userids(),
'', 0.0, 10, true);
}
/**
* Test for provider::delete_data_for_users().
*/
public function test_delete_data_for_users() {
global $DB;
$this->resetAfterTest();
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
// Create three test users.
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
// Create one question as each user in different contexts.
$this->setUser($user1);
$course1data = $questiongenerator->setup_course_and_questions();
$course1 = $course1data[1];
$course1qcat = $course1data[2];
$course1questions = $course1data[3];
$course1context = \context_course::instance($course1->id);
// Log in as user2 and update the questions in course1.
$this->setUser($user2);
foreach ($course1questions as $question) {
$questiongenerator->update_question($question);
}
// Add 2 more questions to course1 by user3.
$this->setUser($user3);
$questiongenerator->create_question('shortanswer', null, ['category' => $course1qcat->id]);
$questiongenerator->create_question('shortanswer', null, ['category' => $course1qcat->id]);
// Now, log in as user1 again, and then create a new course and add questions to that.
$this->setUser($user1);
$questiongenerator->setup_course_and_questions();
$approveduserlist = new \core_privacy\local\request\approved_userlist($course1context, 'core_question',
[$user1->id, $user2->id]);
provider::delete_data_for_users($approveduserlist);
// Now, there should be no question related to user1 or user2 in course1.
$this->assertEquals(
0,
$DB->count_records_sql("SELECT COUNT(q.id)
FROM {question} q
JOIN {question_categories} qc ON q.category = qc.id
WHERE qc.contextid = ?
AND (q.createdby = ? OR q.modifiedby = ? OR q.createdby = ? OR q.modifiedby = ?)",
[$course1context->id, $user1->id, $user1->id, $user2->id, $user2->id])
);
// User3 data in course1 should not change.
$this->assertEquals(
2,
$DB->count_records_sql("SELECT COUNT(q.id)
FROM {question} q
JOIN {question_categories} qc ON q.category = qc.id
WHERE qc.contextid = ? AND (q.createdby = ? OR q.modifiedby = ?)",
[$course1context->id, $user3->id, $user3->id])
);
// User1 has authored 2 questions in another course.
$this->assertEquals(
2,
$DB->count_records_select('question', "createdby = ? OR modifiedby = ?", [$user1->id, $user1->id])
);
}
}