Merge branch 'MDL-67798-master' of git://github.com/cescobedo/moodle

This commit is contained in:
Andrew Nicols 2020-05-01 11:43:20 +08:00
commit 468817abcd
4 changed files with 564 additions and 47 deletions

View File

@ -26,9 +26,14 @@ namespace core_contentbank\privacy;
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\writer;
use core_privacy\local\request\userlist;
use core_privacy\local\request\approved_userlist;
use context_system;
use context_coursecat;
use context_course;
/**
* Privacy provider implementation for core_contentbank.
@ -42,88 +47,236 @@ class provider implements
\core_privacy\local\request\plugin\provider {
/**
* Returns metadata.
* TODO: MDL-67798.
* Returns meta data about this system.
*
* @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 {
// We are not implementing a proper privacy provider for now.
// A right privacy provider will be implemented in MDL-67798.
$collection->add_database_table('contentbank_content', [
'name' => 'privacy:metadata:content:name',
'contenttype' => 'privacy:metadata:content:contenttype',
'usercreated' => 'privacy:metadata:content:usercreated',
'usermodified' => 'privacy:metadata:content:usermodified',
], 'privacy:metadata:userid');
'timecreated' => 'privacy:metadata:content:timecreated',
'timemodified' => 'privacy:metadata:content:timemodified',
], 'privacy:metadata:contentbankcontent');
return $collection;
}
/**
* TODO: MDL-67798.
*
* @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) {
// We are not implementing a proper privacy provider for now.
// A right privacy provider will be implemented in MDL-67798.
}
/**
* TODO: MDL-67798.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
// We are not implementing a proper privacy provider for now.
// A right privacy provider will be implemented in MDL-67798.
}
/**
* TODO: MDL-67798.
* 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.
* @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) : contextlist {
// We are not implementing a proper privacy provider for now.
// A right privacy provider will be implemented in MDL-67798.
$sql = "SELECT DISTINCT ctx.id
FROM {context} ctx
JOIN {contentbank_content} cb
ON cb.contextid = ctx.id
WHERE cb.usercreated = :userid
AND (ctx.contextlevel = :contextlevel1
OR ctx.contextlevel = :contextlevel2
OR ctx.contextlevel = :contextlevel3)";
return (new contextlist());
$params = [
'userid' => $userid,
'contextlevel1' => CONTEXT_SYSTEM,
'contextlevel2' => CONTEXT_COURSECAT,
'contextlevel3' => CONTEXT_COURSE
];
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, $params);
return $contextlist;
}
/**
* Get the list of users within a specific 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();
$allowedcontextlevels = [
CONTEXT_SYSTEM,
CONTEXT_COURSECAT,
CONTEXT_COURSE
];
if (!in_array($context->contextlevel, $allowedcontextlevels)) {
return;
}
$sql = "SELECT cb.usercreated as userid
FROM {contentbank_content} cb
WHERE cb.contextid = :contextid";
$params = [
'contextid' => $context->id
];
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* TODO: MDL-67798.
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
// We are not implementing a proper privacy provider for now.
// A right privacy provider will be implemented in MDL-67798.
global $DB;
// Remove contexts different from SYSTEM, COURSECAT or COURSE.
$contextids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT
|| $context->contextlevel == CONTEXT_COURSE) {
$carry[] = $context->id;
}
return $carry;
}, []);
if (empty($contextids)) {
return;
}
$userid = $contextlist->get_user()->id;
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
// Retrieve the contentbank_content records created for the user.
$sql = "SELECT cb.id,
cb.name,
cb.contenttype,
cb.usercreated,
cb.usermodified,
cb.timecreated,
cb.timemodified,
cb.contextid
FROM {contentbank_content} cb
WHERE cb.usercreated = :userid
AND cb.contextid {$contextsql}
ORDER BY cb.contextid";
$params = ['userid' => $userid] + $contextparams;
$contents = $DB->get_recordset_sql($sql, $params);
$data = [];
$lastcontextid = null;
$subcontext = [
get_string('name', 'core_contentbank')
];
foreach ($contents as $content) {
// The core_contentbank data export is organised in:
// {Sytem|Course Category|Course Context Level}/Content/data.json.
if ($lastcontextid && $lastcontextid != $content->contextid) {
$context = \context::instance_by_id($lastcontextid);
writer::with_context($context)->export_data($subcontext, (object)$data);
$data = [];
}
$data[] = (object) [
'name' => $content->name,
'contenttype' => $content->contenttype,
'usercreated' => transform::user($content->usercreated),
'usermodified' => transform::user($content->usermodified),
'timecreated' => transform::datetime($content->timecreated),
'timemodified' => transform::datetime($content->timemodified)
];
$lastcontextid = $content->contextid;
// The core_contentbank files export is organised in:
// {Sytem|Course Category|Course Context Level}/Content/_files/public/_itemid/filename.
$context = \context::instance_by_id($lastcontextid);
writer::with_context($context)->export_area_files($subcontext, 'contentbank', 'public', $content->id);
}
if (!empty($data)) {
$context = \context::instance_by_id($lastcontextid);
writer::with_context($context)->export_data($subcontext, (object)$data);
}
$contents->close();
}
/**
* TODO: MDL-67798.
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
* @param context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
// We are not implementing a proper privacy provider for now.
// A right privacy provider will be implemented in MDL-67798.
global $DB;
if (!$context instanceof context_system && !$context instanceof context_coursecat
&& !$context instanceof context_course) {
return;
}
static::delete_data($context, []);
}
/**
* 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) {
$context = $userlist->get_context();
if (!$context instanceof context_system && !$context instanceof context_coursecat
&& !$context instanceof context_course) {
return;
}
static::delete_data($context, $userlist->get_userids());
}
/**
* TODO: MDL-67798.
* 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.
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
// We are not implementing a proper privacy provider for now.
// A right privacy provider will be implemented in MDL-67798.
if (empty($contextlist->count())) {
return;
}
$userid = $contextlist->get_user()->id;
foreach ($contextlist->get_contexts() as $context) {
if (!$context instanceof context_system && !$context instanceof context_coursecat
&& !$context instanceof context_course) {
continue;
}
static::delete_data($context, [$userid]);
}
}
/**
* Delete data related to a context and users (if defined).
*
* @param context $context A context.
* @param array $userids The user IDs.
*/
protected static function delete_data(\context $context, array $userids) {
global $DB;
$params = ['contextid' => $context->id];
$select = 'contextid = :contextid';
// Delete the Content Bank files.
if (!empty($userids)) {
list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$params += $inparams;
$select .= ' AND usercreated '.$insql;
}
$fs = get_file_storage();
$contents = $DB->get_records_select('contentbank_content',
$select, $params);
foreach ($contents as $content) {
$fs->delete_area_files($content->contextid, 'contentbank', 'public', $content->id);
}
// Delete all the contents.
$DB->delete_records_select('contentbank_content', $select, $params);
}
}

View File

@ -0,0 +1,358 @@
<?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/>.
/**
* Base class for unit tests for core_contentbank.
*
* @package core_contentbank
* @category test
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_contentbank;
defined('MOODLE_INTERNAL') || die();
use stdClass;
use context_system;
use context_coursecat;
use context_course;
use core_contentbank\privacy\provider;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\writer;
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\userlist;
use core_privacy\local\request\approved_userlist;
/**
* Unit tests for contentbank\classes\privacy\provider.php
*
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_contentbank_privacy_testcase extends provider_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
}
/**
* Test for provider::get_contexts_for_userid().
*/
public function test_get_contexts_for_userid() {
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
// Testing againts Manager who has content in the three contexts.
$contextlist = provider::get_contexts_for_userid($scenario->manager->id);
// There are three contexts in the list.
$contextlistids = $contextlist->get_contextids();
$this->assertCount(3, $contextlistids);
// Check the list against the expected list of contexts.
$this->assertContains($scenario->systemcontext->id, $contextlistids);
$this->assertContains($scenario->coursecategorycontext->id,
$contextlistids);
$this->assertContains($scenario->coursecontext->id, $contextlistids);
// Testing againts Teacher who has content in the one context.
$contextlist = provider::get_contexts_for_userid($scenario->teacher->id);
// There are only one context in the list.
$contextlistids = $contextlist->get_contextids();
$this->assertCount(1, $contextlistids);
// Check the againts Course Context.
$this->assertContains($scenario->coursecontext->id, $contextlistids);
// And there is not a System and Course Category Context.
$this->assertNotContains($scenario->systemcontext->id, $contextlistids);
$this->assertNotContains($scenario->coursecategorycontext->id, $contextlistids);
}
/**
* Test for provider::get_users_in_context().
*/
public function test_get_users_in_context() {
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
// Get the userlist to Context System, only Manager will be there.
$userlist = new userlist($scenario->systemcontext, 'core_contentbank');
provider::get_users_in_context($userlist);
$this->assertEquals([$scenario->manager->id], $userlist->get_userids());
// Teacher will not be there.
$this->assertNotEquals([$scenario->teacher->id], $userlist->get_userids());
// Get the userlist to Context Course, Manager and Teacher will be there.
$userlist = new userlist($scenario->coursecontext, 'core_contentbank');
provider::get_users_in_context($userlist);
$this->assertEquals([$scenario->manager->id, $scenario->teacher->id],
$userlist->get_userids());
}
/**
* Test for provider::test_export_user_data().
*/
public function test_export_user_data() {
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
$subcontexts = [
get_string('name', 'core_contentbank')
];
// Get the data for the System Context.
$writer = writer::with_context($scenario->systemcontext);
$this->assertFalse($writer->has_any_data());
// Export data for Manager.
$this->export_context_data_for_user($scenario->manager->id,
$scenario->systemcontext, 'core_contentbank');
$data = $writer->get_data($subcontexts);
$this->assertCount(3, (array) $data);
$this->assertCount(3, $writer->get_files($subcontexts));
// Get the data for the Course Categoy Context.
$writer = writer::with_context($scenario->coursecategorycontext);
// Export data for Manager.
$this->export_context_data_for_user($scenario->manager->id,
$scenario->coursecategorycontext, 'core_contentbank');
$data = $writer->get_data($subcontexts);
$this->assertCount(2, (array) $data);
$this->assertCount(2, $writer->get_files($subcontexts));
// Get the data for the Course Context.
$writer = writer::with_context($scenario->coursecontext);
// Export data for Manager.
$this->export_context_data_for_user($scenario->manager->id,
$scenario->coursecontext, 'core_contentbank');
$data = $writer->get_data($subcontexts);
$this->assertCount(2, (array) $data);
$this->assertCount(2, $writer->get_files($subcontexts));
// Export data for Teacher.
$writer = writer::reset();
$writer = writer::with_context($scenario->coursecontext);
$this->export_context_data_for_user($scenario->teacher->id,
$scenario->coursecontext, 'core_contentbank');
$data = $writer->get_data($subcontexts);
$this->assertCount(3, (array) $data);
$this->assertCount(3, $writer->get_files($subcontexts));
}
/**
* Test for provider::delete_data_for_all_users_in_context().
*/
public function test_delete_data_for_all_users_in_context() {
global $DB;
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
// Before delete data, we have 4 contents.
// - 3 in a system context.
// - 2 in a course category context.
// - 5 in a course context (2 by manager and 3 by teacher).
// Delete data based on system context.
provider::delete_data_for_all_users_in_context($scenario->systemcontext);
$count = $DB->count_records('contentbank_content');
// 3 content should be deleted.
// 7 contents should be remain.
$this->assertEquals(7, $count);
// Delete data based on course category context.
provider::delete_data_for_all_users_in_context($scenario->coursecategorycontext);
$count = $DB->count_records('contentbank_content');
// 2 contents should be deleted.
// 5 content should be remain.
$this->assertEquals(5, $count);
// Delete data based on course context.
provider::delete_data_for_all_users_in_context($scenario->coursecontext);
$count = $DB->count_records('contentbank_content');
// 5 content should be deleted.
// 0 content should be remain.
$this->assertEquals(0, $count);
}
/**
* Test for provider::test_delete_data_for_users().
*/
public function test_delete_data_for_users() {
global $DB;
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
// Before delete data, we have 4 contents.
// - 3 in a system context.
// - 2 in a course category context.
// - 5 in a course context (2 by manager and 3 by teacher).
// A list of users who has created content in Course Category Context.
$userlist1 = new userlist($scenario->coursecategorycontext,
'core_contentbank');
provider::get_users_in_context($userlist1);
$this->assertCount(1, $userlist1);
// Only Manager should be.
$this->assertEquals([$scenario->manager->id], $userlist1->get_userids());
// A list of users who has created content in Course Context.
$userlist2 = new userlist($scenario->coursecontext, 'core_contentbank');
provider::get_users_in_context($userlist2);
$this->assertCount(2, $userlist2);
// Manager and Teacher should be.
$this->assertEquals([$scenario->manager->id, $scenario->teacher->id],
$userlist2->get_userids());
// Convert $userlist1 into an approved_contextlist.
$approvedlist1 = new approved_userlist($scenario->coursecategorycontext, 'core_contentbank', $userlist1->get_userids());
// Delete data for users in course category context.
provider::delete_data_for_users($approvedlist1);
// Re-fetch users in course category context.
$userlist1 = new userlist($scenario->coursecategorycontext,
'core_contentbank');
provider::get_users_in_context($userlist1);
// The user data in course category context should be deleted.
$this->assertCount(0, $userlist1);
// Re-fetch users in course category context.
$userlist2 = new userlist($scenario->coursecontext, 'core_contentbank');
provider::get_users_in_context($userlist2);
// The user data in course context should be still present.
$this->assertCount(2, $userlist2);
// Convert $userlist2 into an approved_contextlist.
$approvedlist2 = new approved_userlist($scenario->coursecontext,
'core_contentbank', $userlist2->get_userids());
// Delete data for users in course context.
provider::delete_data_for_users($approvedlist2);
$userlist2 = new userlist($scenario->coursecontext, 'core_contentbank');
provider::get_users_in_context($userlist2);
// The user data in course context should be deleted.
$this->assertCount(0, $userlist2);
}
/**
* Test for provider::delete_data_for_user().
*/
public function test_delete_data_for_user() {
global $DB;
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
// Before delete data, we have 4 contents.
// - 3 in a system context.
// - 2 in a course category context.
// - 5 in a course context (2 by manager and 3 by teacher).
// Get all the context for Manager.
$contextlist = provider::get_contexts_for_userid($scenario->manager->id);
$approvedcontextlist = new approved_contextlist($scenario->manager,
'core_contentbank', $contextlist->get_contextids());
// Delete all the data created by the Manager in all the contexts.
provider::delete_data_for_user($approvedcontextlist);
// After deletion, only 3 content for teacher should be present.
$count = $DB->count_records('contentbank_content');
$this->assertEquals(3, $count);
// Confirm that the remaining content was created by the teacher.
$count = $DB->count_records('contentbank_content',
['usercreated' => $scenario->teacher->id]);
$this->assertEquals(3, $count);
// Get all the context for Teacher.
$contextlist = provider::get_contexts_for_userid($scenario->teacher->id);
$approvedcontextlist = new approved_contextlist($scenario->teacher,
'core_contentbank', $contextlist->get_contextids());
// Delete all the data created by the Teacher in all the contexts.
provider::delete_data_for_user($approvedcontextlist);
// After deletion, no content should be present.
$count = $DB->count_records('contentbank_content');
$this->assertEquals(0, $count);
}
/**
* Create a complex scenario to use into the tests.
*
* @return stdClass $scenario
*/
protected function setup_scenario() {
global $DB;
$systemcontext = context_system::instance();
$manager = $this->getDataGenerator()->create_user();
$managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']);
$this->getDataGenerator()->role_assign($managerroleid, $manager->id);
$coursecategory = $this->getDataGenerator()->create_category();
$coursecategorycontext = context_coursecat::instance($coursecategory->id);
$course = $this->getDataGenerator()->create_course();
$coursecontext = context_course::instance($course->id);
$teacher = $this->getDataGenerator()->create_and_enrol($course,
'editingteacher');
// Add some content to the content bank.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
// Add contents by Manager in Context System.
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $systemcontext, false, 'systemtestfile1.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $systemcontext, false, 'systemtestfile2.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $systemcontext, false, 'systemtestfile3.h5p');
// Add contents by Manager in Context Course Category.
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $coursecategorycontext, false, 'coursecattestfile1.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $coursecategorycontext, false, 'coursecattestfile2.h5p');
// Add contents by Manager in Context Course.
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $coursecontext, false, 'coursetestfile1.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $coursecontext, false, 'coursetestfile2.h5p');
// Add contents by Teacher.
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $teacher->id, $coursecontext, false, 'courseteacherfile1.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $teacher->id, $coursecontext, false, 'courseteacherfile2.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $teacher->id, $coursecontext, false, 'courseteacherfile3.h5p');
$scenario = new stdClass();
$scenario->systemcontext = $systemcontext;
$scenario->coursecategorycontext = $coursecategorycontext;
$scenario->coursecontext = $coursecontext;
$scenario->manager = $manager;
$scenario->teacher = $teacher;
return $scenario;
}
}

View File

@ -31,9 +31,16 @@ $string['file'] = 'Upload content';
$string['file_help'] = 'Files may be stored in the content bank for use in courses. Only files used by content types enabled on the site may be uploaded.';
$string['name'] = 'Content';
$string['nopermissiontodelete'] = 'You do not have permission to delete content.';
$string['privacy:metadata:userid'] = 'The ID of the user creating or modifying content bank content.';
$string['privacy:metadata:content:contenttype'] = 'The contenttype plugin of the content in the content bank.';
$string['privacy:metadata:content:name'] = 'Name of the content in the content bank.';
$string['privacy:metadata:content:timecreated'] = 'The time when the content was created.';
$string['privacy:metadata:content:timemodified'] = 'The time when the content was modified.';
$string['privacy:metadata:content:usercreated'] = 'The user has created the content.';
$string['privacy:metadata:content:usercreated'] = 'The user who created the content.';
$string['privacy:metadata:content:usermodified'] = 'Last user has modified the content.';
$string['privacy:metadata:content:usermodified'] = 'The last user who modified the content.';
$string['privacy:metadata:contentbankcontent'] = 'Stores the content of the content bank.';
$string['privacy:metadata:userid'] = 'The ID of the user creating or modifying content bank content.';
$string['timecreated'] = 'Time created';
$string['unsupported'] = 'This content type is not supported.';
$string['upload'] = 'Upload';

View File

@ -418,7 +418,6 @@ class content_writer implements \core_privacy\local\request\content_writer {
$filepath[] = $file->get_filename();
$filepath = array_filter($filepath);
$filepath = implode('/', $filepath);
$current = $this->fetch_root($this->files, $subcontext);
$current->data[$filepath] = $file;
}