MDL-63531 mod_assign: Update mod assign to use new interface.

This introduces a new interface for assign sub-plugins and
updates the mod_assign provider to implement the new general
interface for deleting data for users in a context.
This commit is contained in:
Adrian Greeve 2018-10-10 13:27:23 +08:00 committed by Andrew Nicols
parent 980425022b
commit b96e7fa6b2
3 changed files with 483 additions and 24 deletions

View File

@ -48,6 +48,15 @@ class assign_plugin_request_data {
/** @var object If set then only export data related directly to this user. */
protected $user;
/** @var array The user IDs of the users that will be affected. */
protected $userids;
/** @var array The submissions related to the users added. */
protected $submissions = [];
/** @var array The grades related to the users added. */
protected $grades = [];
/** @var assign The assign object */
protected $assign;
@ -69,6 +78,16 @@ class assign_plugin_request_data {
$this->assign = $assign;
}
/**
* Method for adding an array of user IDs. This will do a query to populate the submissions and grades
* for these users.
*
* @param array $userids User IDs to do something with.
*/
public function set_userids(array $userids) {
$this->userids = $userids;
}
/**
* Getter for this attribute.
*
@ -113,4 +132,75 @@ class assign_plugin_request_data {
public function get_assign() {
return $this->assign;
}
/**
* A method to conveniently fetch the assign id.
*
* @return int The assign id.
*/
public function get_assignid() {
return $this->assign->get_instance()->id;
}
/**
* Get all of the user IDs
*
* @return array User IDs
*/
public function get_userids() {
return $this->userids;
}
/**
* Returns all of the submission IDs
*
* @return array submission IDs
*/
public function get_submissionids() {
return array_keys($this->submissions);
}
/**
* Returns the submissions related to the user IDs
*
* @return array User submissions.
*/
public function get_submissions() {
return $this->submissions;
}
/**
* Returns the grade IDs related to the user IDs
*
* @return array User grade IDs.
*/
public function get_gradeids() {
return array_keys($this->grades);
}
/**
* Returns the grades related to the user IDs
*
* @return array User grades.
*/
public function get_grades() {
return $this->grades;
}
/**
* Fetches all of the submissions and grades related to the User IDs provided. Use get_grades, get_submissions etc to
* retrieve this information.
*/
public function populate_submissions_and_grades() {
global $DB;
if (empty($this->get_userids())) {
throw new \coding_exception('Please use set_userids() before calling this method.');
}
list($sql, $params) = $DB->get_in_or_equal($this->get_userids(), SQL_PARAMS_NAMED);
$params['assign'] = $this->get_assign()->get_instance()->id;
$this->submissions = $DB->get_records_select('assign_submission', "assignment = :assign AND userid $sql", $params);
$this->grades = $DB->get_records_select('assign_grades', "assignment = :assign AND userid $sql", $params);
}
}

View File

@ -29,14 +29,13 @@ defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/assign/locallib.php');
use \core_privacy\local\metadata\collection;
use \core_privacy\local\metadata\provider as metadataprovider;
use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\plugin\provider as pluginprovider;
use \core_privacy\local\request\user_preference_provider as preference_provider;
use \core_privacy\local\request\writer;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\transform;
use \core_privacy\local\request\helper;
use \core_privacy\local\request\userlist;
use \core_privacy\local\request\approved_userlist;
use \core_privacy\manager;
/**
@ -46,11 +45,21 @@ use \core_privacy\manager;
* @copyright 2018 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements metadataprovider, pluginprovider, preference_provider {
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\plugin\provider,
\core_privacy\local\request\user_preference_provider,
\core_privacy\local\request\core_userlist_provider {
/** Interface for all assign submission sub-plugins. */
const ASSIGNSUBMISSION_INTERFACE = 'mod_assign\privacy\assignsubmission_provider';
/** Interface for all assign submission sub-plugins. This allows for deletion of users with a context. */
const ASSIGNSUBMISSION_USER_INTERFACE = 'mod_assign\privacy\assignsubmission_user_provider';
/** Interface for all assign feedback sub-plugins. This allows for deletion of users with a context. */
const ASSIGNFEEDBACK_USER_INTERFACE = 'mod_assign\privacy\assignfeedback_user_provider';
/** Interface for all assign feedback sub-plugins. */
const ASSIGNFEEDBACK_INTERFACE = 'mod_assign\privacy\assignfeedback_provider';
@ -192,6 +201,76 @@ class provider implements metadataprovider, pluginprovider, preference_provider
return $contextlist;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @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->contextlevel != CONTEXT_MODULE) {
return;
}
$params = [
'modulename' => 'assign',
'contextid' => $context->id,
'contextlevel' => CONTEXT_MODULE
];
$sql = "SELECT g.userid, g.grader
FROM {context} ctx
JOIN {course_modules} cm ON cm.id = ctx.instanceid
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
JOIN {assign} a ON a.id = cm.instance
JOIN {assign_grades} g ON a.id = g.assignment
WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
$userlist->add_from_sql('userid', $sql, $params);
$userlist->add_from_sql('grader', $sql, $params);
$sql = "SELECT o.userid
FROM {context} ctx
JOIN {course_modules} cm ON cm.id = ctx.instanceid
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
JOIN {assign} a ON a.id = cm.instance
JOIN {assign_overrides} o ON a.id = o.assignid
WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
$userlist->add_from_sql('userid', $sql, $params);
$sql = "SELECT s.userid
FROM {context} ctx
JOIN {course_modules} cm ON cm.id = ctx.instanceid
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
JOIN {assign} a ON a.id = cm.instance
JOIN {assign_submission} s ON a.id = s.assignment
WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
$userlist->add_from_sql('userid', $sql, $params);
$sql = "SELECT uf.userid
FROM {context} ctx
JOIN {course_modules} cm ON cm.id = ctx.instanceid
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
JOIN {assign} a ON a.id = cm.instance
JOIN {assign_user_flags} uf ON a.id = uf.assignment
WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
$userlist->add_from_sql('userid', $sql, $params);
$sql = "SELECT um.userid
FROM {context} ctx
JOIN {course_modules} cm ON cm.id = ctx.instanceid
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
JOIN {assign} a ON a.id = cm.instance
JOIN {assign_user_mapping} um ON a.id = um.assignment
WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
$userlist->add_from_sql('userid', $sql, $params);
manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_USER_INTERFACE,
'get_userids_from_context', [$userlist]);
manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_USER_INTERFACE,
'get_userids_from_context', [$userlist]);
}
/**
* Write out the user data filtered by contexts.
*
@ -265,7 +344,7 @@ class provider implements metadataprovider, pluginprovider, preference_provider
}
// Time to roll my own method for deleting overrides.
static::delete_user_overrides($assign);
static::delete_overrides_for_users($assign);
$DB->delete_records('assign_submission', ['assignment' => $assign->get_instance()->id]);
$DB->delete_records('assign_user_flags', ['assignment' => $assign->get_instance()->id]);
$DB->delete_records('assign_user_mapping', ['assignment' => $assign->get_instance()->id]);
@ -311,7 +390,7 @@ class provider implements metadataprovider, pluginprovider, preference_provider
}
}
static::delete_user_overrides($assign, $user);
static::delete_overrides_for_users($assign, [$user->id]);
$DB->delete_records('assign_user_flags', ['assignment' => $assignid, 'userid' => $user->id]);
$DB->delete_records('assign_user_mapping', ['assignment' => $assignid, 'userid' => $user->id]);
$DB->delete_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]);
@ -320,32 +399,87 @@ class provider implements metadataprovider, pluginprovider, preference_provider
}
/**
* Deletes assignment overrides.
* Delete multiple users within a single context.
*
* @param \assign $assign The assignment object
* @param \stdClass $user The user object if we are deleting only the overrides for one user.
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
protected static function delete_user_overrides(\assign $assign, \stdClass $user = null) {
public static function delete_data_for_users(approved_userlist $userlist) {
global $DB;
$context = $userlist->get_context();
if ($context->contextlevel != CONTEXT_MODULE) {
return;
}
$userids = $userlist->get_userids();
$assign = new \assign($context, null, null);
$assignid = $assign->get_instance()->id;
$params = (isset($user)) ? ['assignid' => $assignid, 'userid' => $user->id] : ['assignid' => $assignid];
$requestdata = new assign_plugin_request_data($context, $assign);
$requestdata->set_userids($userids);
$requestdata->populate_submissions_and_grades();
manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_USER_INTERFACE, 'delete_submissions',
[$requestdata]);
manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_USER_INTERFACE, 'delete_feedback_for_grades',
[$requestdata]);
$overrides = $DB->get_records('assign_overrides', $params);
if (!empty($overrides)) {
foreach ($overrides as $override) {
// First delete calendar events associated with this override.
$conditions = ['modulename' => 'assign', 'instance' => $assignid];
if (isset($user)) {
$conditions['userid'] = $user->id;
}
$DB->delete_records('event', $conditions);
// Next delete the overrides.
$DB->delete_records('assign_overrides', ['id' => $override->id]);
// Update this function to delete advanced grading information.
$gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions');
$controller = $gradingmanager->get_active_controller();
if (isset($controller)) {
$gradeids = $requestdata->get_gradeids();
// Careful here, if no gradeids are provided then all data is deleted for the context.
if (!empty($gradeids)) {
\core_grading\privacy\provider::delete_data_for_instances($context, $gradeids);
}
}
static::delete_overrides_for_users($assign, $userids);
list($sql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$params['assignment'] = $assignid;
$DB->delete_records_select('assign_user_flags', "assignment = :assignment AND userid $sql", $params);
$DB->delete_records_select('assign_user_mapping', "assignment = :assignment AND userid $sql", $params);
$DB->delete_records_select('assign_grades', "assignment = :assignment AND userid $sql", $params);
$DB->delete_records_select('assign_submission', "assignment = :assignment AND userid $sql", $params);
}
/**
* Deletes assignment overrides in bulk
*
* @param \assign $assign The assignment object
* @param array $userids An array of user IDs
*/
protected static function delete_overrides_for_users(\assign $assign, array $userids = []) {
global $DB;
$assignid = $assign->get_instance()->id;
$usersql = '';
$params = ['assignid' => $assignid];
if (!empty($userids)) {
list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$params = array_merge($params, $userparams);
$overrides = $DB->get_records_select('assign_overrides', "assignid = :assignid AND userid $usersql", $params);
} else {
$overrides = $DB->get_records('assign_overrides', $params);
}
if (!empty($overrides)) {
$params = ['modulename' => 'assign', 'instance' => $assignid];
if (!empty($userids)) {
$params = array_merge($params, $userparams);
$DB->delete_records_select('event', "modulename = :modulename AND instance = :instance AND userid $usersql",
$params);
// Setting up for the next query.
$params = $userparams;
$usersql = "AND userid $usersql";
} else {
$DB->delete_records('event', $params);
// Setting up for the next query.
$params = [];
}
list($overridesql, $overrideparams) = $DB->get_in_or_equal(array_keys($overrides), SQL_PARAMS_NAMED);
$params = array_merge($params, $overrideparams);
$DB->delete_records_select('assign_overrides', "id $overridesql $usersql", $params);
}
}
/**

View File

@ -148,6 +148,95 @@ class mod_assign_privacy_testcase extends provider_testcase {
$this->assertEmpty(array_diff($usercontextids, $contextlist->get_contextids()));
}
/**
* Test returning a list of user IDs related to a context (assign).
*/
public function test_get_users_in_context() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
// Only made a comment on a submission.
$user1 = $this->getDataGenerator()->create_user();
// User 2 only has information about an activity override.
$user2 = $this->getDataGenerator()->create_user();
// User 3 made a submission.
$user3 = $this->getDataGenerator()->create_user();
// User 4 makes a submission and it is marked by the teacher.
$user4 = $this->getDataGenerator()->create_user();
// Grading and providing feedback as a teacher.
$user5 = $this->getDataGenerator()->create_user();
// This user has no entries and should not show up.
$user6 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
$this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
$this->getDataGenerator()->enrol_user($user3->id, $course->id, 'student');
$this->getDataGenerator()->enrol_user($user4->id, $course->id, 'student');
$this->getDataGenerator()->enrol_user($user5->id, $course->id, 'editingteacher');
$this->getDataGenerator()->enrol_user($user6->id, $course->id, 'student');
$assign1 = $this->create_instance(['course' => $course,
'assignsubmission_onlinetext_enabled' => true,
'assignfeedback_comments_enabled' => true]);
$assign2 = $this->create_instance(['course' => $course]);
$context = $assign1->get_context();
// Jam an entry in the comments table for user 1.
$comment = (object) [
'contextid' => $context->id,
'component' => 'assignsubmission_comments',
'commentarea' => 'submission_comments',
'itemid' => 5,
'content' => 'A comment by user 1',
'format' => 0,
'userid' => $user1->id,
'timecreated' => time()
];
$DB->insert_record('comments', $comment);
$this->setUser($user5); // Set the user to the teacher.
$overridedata = new \stdClass();
$overridedata->assignid = $assign1->get_instance()->id;
$overridedata->userid = $user2->id;
$overridedata->duedate = time();
$overridedata->allowsubmissionsfromdate = time();
$overridedata->cutoffdate = time();
$DB->insert_record('assign_overrides', $overridedata);
$submissiontext = 'My first submission';
$submission = $this->create_submission($assign1, $user3, $submissiontext);
$submissiontext = 'My first submission';
$submission = $this->create_submission($assign1, $user4, $submissiontext);
$this->setUser($user5);
$grade = '72.00';
$teachercommenttext = 'This is better. Thanks.';
$data = new \stdClass();
$data->attemptnumber = 1;
$data->grade = $grade;
$data->assignfeedbackcomments_editor = ['text' => $teachercommenttext, 'format' => FORMAT_MOODLE];
// Give the submission a grade.
$assign1->save_grade($user4->id, $data);
$userlist = new \core_privacy\local\request\userlist($context, 'assign');
provider::get_users_in_context($userlist);
$userids = $userlist->get_userids();
$this->assertTrue(in_array($user1->id, $userids));
$this->assertTrue(in_array($user2->id, $userids));
$this->assertTrue(in_array($user3->id, $userids));
$this->assertTrue(in_array($user4->id, $userids));
$this->assertTrue(in_array($user5->id, $userids));
$this->assertFalse(in_array($user6->id, $userids));
}
/**
* Test that a student with multiple submissions and grades is returned with the correct data.
*/
@ -577,4 +666,150 @@ class mod_assign_privacy_testcase extends provider_testcase {
// The remaining event should be for user 1.
$this->assertEquals($user1->id, $record->userid);
}
/**
* A test for deleting all user data for a bunch of users.
*/
public function test_delete_data_for_users() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
// Only made a comment on a submission.
$user1 = $this->getDataGenerator()->create_user();
// User 2 only has information about an activity override.
$user2 = $this->getDataGenerator()->create_user();
// User 3 made a submission.
$user3 = $this->getDataGenerator()->create_user();
// User 4 makes a submission and it is marked by the teacher.
$user4 = $this->getDataGenerator()->create_user();
// Grading and providing feedback as a teacher.
$user5 = $this->getDataGenerator()->create_user();
// This user has entries in assignment 2 and should not have their data deleted.
$user6 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
$this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
$this->getDataGenerator()->enrol_user($user3->id, $course->id, 'student');
$this->getDataGenerator()->enrol_user($user4->id, $course->id, 'student');
$this->getDataGenerator()->enrol_user($user5->id, $course->id, 'editingteacher');
$this->getDataGenerator()->enrol_user($user6->id, $course->id, 'student');
$assign1 = $this->create_instance(['course' => $course,
'assignsubmission_onlinetext_enabled' => true,
'assignfeedback_comments_enabled' => true]);
$assign2 = $this->create_instance(['course' => $course,
'assignsubmission_onlinetext_enabled' => true,
'assignfeedback_comments_enabled' => true]);
$context = $assign1->get_context();
// Jam an entry in the comments table for user 1.
$comment = (object) [
'contextid' => $context->id,
'component' => 'assignsubmission_comments',
'commentarea' => 'submission_comments',
'itemid' => 5,
'content' => 'A comment by user 1',
'format' => 0,
'userid' => $user1->id,
'timecreated' => time()
];
$DB->insert_record('comments', $comment);
$this->setUser($user5); // Set the user to the teacher.
$overridedata = new \stdClass();
$overridedata->assignid = $assign1->get_instance()->id;
$overridedata->userid = $user2->id;
$overridedata->duedate = time();
$overridedata->allowsubmissionsfromdate = time();
$overridedata->cutoffdate = time();
$DB->insert_record('assign_overrides', $overridedata);
$submissiontext = 'My first submission';
$submission = $this->create_submission($assign1, $user3, $submissiontext);
$submissiontext = 'My first submission';
$submission = $this->create_submission($assign1, $user4, $submissiontext);
$submissiontext = 'My first submission';
$submission = $this->create_submission($assign2, $user6, $submissiontext);
$this->setUser($user5);
$grade = '72.00';
$teachercommenttext = 'This is better. Thanks.';
$data = new \stdClass();
$data->attemptnumber = 1;
$data->grade = $grade;
$data->assignfeedbackcomments_editor = ['text' => $teachercommenttext, 'format' => FORMAT_MOODLE];
// Give the submission a grade.
$assign1->save_grade($user4->id, $data);
$this->setUser($user5);
$grade = '81.00';
$teachercommenttext = 'This is nice.';
$data = new \stdClass();
$data->attemptnumber = 1;
$data->grade = $grade;
$data->assignfeedbackcomments_editor = ['text' => $teachercommenttext, 'format' => FORMAT_MOODLE];
// Give the submission a grade.
$assign2->save_grade($user6->id, $data);
// Check data is in place.
$data = $DB->get_records('assign_submission');
// We should have one entry for user 3 and two entries each for user 4 and 6.
$this->assertCount(5, $data);
$usercounts = [
$user3->id => 0,
$user4->id => 0,
$user6->id => 0
];
foreach ($data as $datum) {
$usercounts[$datum->userid]++;
}
$this->assertEquals(1, $usercounts[$user3->id]);
$this->assertEquals(2, $usercounts[$user4->id]);
$this->assertEquals(2, $usercounts[$user6->id]);
$data = $DB->get_records('assign_grades');
// Two entries in assign_grades, one for each grade given.
$this->assertCount(2, $data);
$data = $DB->get_records('assign_overrides');
$this->assertCount(1, $data);
$data = $DB->get_records('comments');
$this->assertCount(1, $data);
$userlist = new \core_privacy\local\request\approved_userlist($context, 'assign', [$user1->id, $user2->id]);
provider::delete_data_for_users($userlist);
$data = $DB->get_records('assign_overrides');
$this->assertEmpty($data);
$data = $DB->get_records('comments');
$this->assertEmpty($data);
$data = $DB->get_records('assign_submission');
// No change here.
$this->assertCount(5, $data);
$userlist = new \core_privacy\local\request\approved_userlist($context, 'assign', [$user3->id, $user5->id]);
provider::delete_data_for_users($userlist);
$data = $DB->get_records('assign_submission');
// Only the record for user 3 has been deleted.
$this->assertCount(4, $data);
$data = $DB->get_records('assign_grades');
// Grades should be unchanged.
$this->assertCount(2, $data);
}
}