diff --git a/enrol/externallib.php b/enrol/externallib.php index 9bf1e5dd159..3d1f03f7450 100644 --- a/enrol/externallib.php +++ b/enrol/externallib.php @@ -584,6 +584,94 @@ class core_enrol_external extends external_api { return new external_multiple_structure(core_user_external::user_description()); } + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function search_users_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'courseid' => new external_value(PARAM_INT, 'course id'), + 'search' => new external_value(PARAM_RAW, 'query'), + 'searchanywhere' => new external_value(PARAM_BOOL, 'find a match anywhere, or only at the beginning'), + 'page' => new external_value(PARAM_INT, 'Page number'), + 'perpage' => new external_value(PARAM_INT, 'Number per page'), + ] + ); + } + + /** + * Search course participants. + * + * @param int $courseid Course id + * @param string $search The query + * @param bool $searchanywhere Match anywhere in the string + * @param int $page Page number + * @param int $perpage Max per page + * @return array An array of users + * @throws moodle_exception + */ + public static function search_users(int $courseid, string $search, bool $searchanywhere, int $page, int $perpage): array { + global $PAGE, $DB, $CFG; + + require_once($CFG->dirroot.'/enrol/locallib.php'); + require_once($CFG->dirroot.'/user/lib.php'); + + $params = self::validate_parameters( + self::search_users_parameters(), + [ + 'courseid' => $courseid, + 'search' => $search, + 'searchanywhere' => $searchanywhere, + 'page' => $page, + 'perpage' => $perpage + ] + ); + $context = context_course::instance($params['courseid']); + try { + self::validate_context($context); + } catch (Exception $e) { + $exceptionparam = new stdClass(); + $exceptionparam->message = $e->getMessage(); + $exceptionparam->courseid = $params['courseid']; + throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); + } + course_require_view_participants($context); + + $course = get_course($params['courseid']); + $manager = new course_enrolment_manager($PAGE, $course); + + $users = $manager->search_users($params['search'], + $params['searchanywhere'], + $params['page'], + $params['perpage']); + + $results = []; + // Add also extra user fields. + $requiredfields = array_merge( + ['id', 'fullname', 'profileimageurl', 'profileimageurlsmall'], + get_extra_user_fields($context) + ); + foreach ($users['users'] as $user) { + if ($userdetails = user_get_user_details($user, $course, $requiredfields)) { + $results[] = $userdetails; + } + } + return $results; + } + + /** + * Returns description of method result value + * + * @return external_multiple_structure + */ + public static function search_users_returns(): external_multiple_structure { + global $CFG; + require_once($CFG->dirroot . '/user/externallib.php'); + return new external_multiple_structure(core_user_external::user_description()); + } + /** * Returns description of method parameters * diff --git a/enrol/locallib.php b/enrol/locallib.php index c77e1510c9f..bb4c8583fa5 100644 --- a/enrol/locallib.php +++ b/enrol/locallib.php @@ -531,6 +531,35 @@ class course_enrolment_manager { return $this->execute_search_queries($search, $fields, $countfields, $sql, $params, $page, $perpage, 0, $returnexactcount); } + /** + * Searches through the enrolled users in this course. + * + * @param string $search The search term. + * @param bool $searchanywhere Can the search term be anywhere, or must it be at the start. + * @param int $page Starting at 0. + * @param int $perpage Number of users returned per page. + * @param bool $returnexactcount Return the exact total users using count_record or not. + * @return array with two or three elements: + * int totalusers Number users matching the search. (This element only exist if $returnexactcount was set to true) + * array users List of user objects returned by the query. + * boolean moreusers True if there are still more users, otherwise is False. + */ + public function search_users(string $search = '', bool $searchanywhere = false, int $page = 0, int $perpage = 25, + bool $returnexactcount = false) { + list($ufields, $params, $wherecondition) = $this->get_basic_search_conditions($search, $searchanywhere); + + $fields = 'SELECT ' . $ufields; + $countfields = 'SELECT COUNT(u.id)'; + $sql = " FROM {user} u + JOIN {user_enrolments} ue ON ue.userid = u.id + JOIN {enrol} e ON ue.enrolid = e.id + WHERE $wherecondition + AND e.courseid = :courseid"; + $params['courseid'] = $this->course->id; + + return $this->execute_search_queries($search, $fields, $countfields, $sql, $params, $page, $perpage, 0, $returnexactcount); + } + /** * Gets an array containing some SQL to user for when selecting, params for * that SQL, and the filter that was used in constructing the sql. diff --git a/enrol/tests/course_enrolment_manager_test.php b/enrol/tests/course_enrolment_manager_test.php index 9e9f0ed5690..6954ff37c9f 100644 --- a/enrol/tests/course_enrolment_manager_test.php +++ b/enrol/tests/course_enrolment_manager_test.php @@ -325,7 +325,7 @@ class core_course_enrolment_manager_testcase extends advanced_testcase { } /** - * Test case for test_get_potential_users and test_search_other_users tests. + * Test case for test_get_potential_users, test_search_other_users and test_search_users tests. * * @return array Dataset */ @@ -337,4 +337,42 @@ class core_course_enrolment_manager_testcase extends advanced_testcase { [5, true, 3, 3, false] ]; } + + /** + * Test search_users function. + * + * @dataProvider search_users_provider + * + * @param int $perpage Number of users per page. + * @param bool $returnexactcount Return the exact count or not. + * @param int $expectedusers Expected number of users return. + * @param int $expectedtotalusers Expected total of users in database. + * @param bool $expectedmoreusers Expected for more users return or not. + */ + public function test_search_users($perpage, $returnexactcount, $expectedusers, $expectedtotalusers, $expectedmoreusers) { + global $PAGE; + $this->resetAfterTest(); + + $this->getDataGenerator()->create_and_enrol($this->course, 'student', ['firstname' => 'sutest 1']); + $this->getDataGenerator()->create_and_enrol($this->course, 'student', ['firstname' => 'sutest 2']); + $this->getDataGenerator()->create_and_enrol($this->course, 'student', ['firstname' => 'sutest 3']); + + $manager = new course_enrolment_manager($PAGE, $this->course); + $users = $manager->search_users( + 'sutest', + true, + 0, + $perpage, + $returnexactcount + ); + + $this->assertCount($expectedusers, $users['users']); + $this->assertEquals($expectedmoreusers, $users['moreusers']); + if ($returnexactcount) { + $this->assertArrayHasKey('totalusers', $users); + $this->assertEquals($expectedtotalusers, $users['totalusers']); + } else { + $this->assertArrayNotHasKey('totalusers', $users); + } + } } diff --git a/enrol/tests/externallib_test.php b/enrol/tests/externallib_test.php index 8ae3b1b7566..baf1d70a7f9 100644 --- a/enrol/tests/externallib_test.php +++ b/enrol/tests/externallib_test.php @@ -1171,4 +1171,92 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase { $ue = $DB->count_records('user_enrolments', ['id' => $ueid]); $this->assertEquals(0, $ue); } + + /** + * Test for core_enrol_external::test_search_users(). + */ + public function test_search_users() { + global $DB; + + $this->resetAfterTest(true); + $datagen = $this->getDataGenerator(); + + /** @var enrol_manual_plugin $manualplugin */ + $manualplugin = enrol_get_plugin('manual'); + $this->assertNotNull($manualplugin); + + $studentroleid = $DB->get_field('role', 'id', ['shortname' => 'student'], MUST_EXIST); + $teacherroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher'], MUST_EXIST); + + $course1 = $datagen->create_course(); + $course2 = $datagen->create_course(); + + $user1 = $datagen->create_user(['firstname' => 'user 1']); + $user2 = $datagen->create_user(['firstname' => 'user 2']); + $user3 = $datagen->create_user(['firstname' => 'user 3']); + $teacher = $datagen->create_user(['firstname' => 'user 4']); + + $instanceid = null; + $instances = enrol_get_instances($course1->id, true); + foreach ($instances as $inst) { + if ($inst->enrol == 'manual') { + $instanceid = (int)$inst->id; + break; + } + } + if (empty($instanceid)) { + $instanceid = $manualplugin->add_default_instance($course1); + if (empty($instanceid)) { + $instanceid = $manualplugin->add_instance($course1); + } + } + $this->assertNotNull($instanceid); + + $instance = $DB->get_record('enrol', ['id' => $instanceid], '*', MUST_EXIST); + $manualplugin->enrol_user($instance, $user1->id, $studentroleid, 0, 0, ENROL_USER_ACTIVE); + $manualplugin->enrol_user($instance, $user2->id, $studentroleid, 0, 0, ENROL_USER_ACTIVE); + $manualplugin->enrol_user($instance, $user3->id, $studentroleid, 0, 0, ENROL_USER_ACTIVE); + $manualplugin->enrol_user($instance, $teacher->id, $teacherroleid, 0, 0, ENROL_USER_ACTIVE); + + $this->setUser($teacher); + + // Search for users in a course with enrolled users. + $result = core_enrol_external::search_users($course1->id, 'user', true, 0, 30); + $this->assertCount(4, $result); + + $this->expectException('moodle_exception'); + // Search for users in a course without any enrolled users, shouldn't return anything. + $result = core_enrol_external::search_users($course2->id, 'user', true, 0, 30); + $this->assertCount(0, $result); + + // Search for invalid first name. + $result = core_enrol_external::search_users($course1->id, 'yada yada', true, 0, 30); + $this->assertCount(0, $result); + + // Test pagination, it should return only 3 users. + $result = core_enrol_external::search_users($course1->id, 'user', true, 0, 3); + $this->assertCount(3, $result); + + // Test pagination, it should return only 3 users. + $result = core_enrol_external::search_users($course1->id, 'user 1', true, 0, 1); + $result = $result[0]; + $this->assertEquals($user1->id, $result['id']); + $this->assertEquals($user1->email, $result['email']); + $this->assertEquals(fullname($user1), $result['fullname']); + + $this->setUser($user1); + + // Search for users in a course with enrolled users. + $result = core_enrol_external::search_users($course1->id, 'user', true, 0, 30); + $this->assertCount(4, $result); + + $this->expectException('moodle_exception'); + // Search for users in a course without any enrolled users, shouldn't return anything. + $result = core_enrol_external::search_users($course2->id, 'user', true, 0, 30); + $this->assertCount(0, $result); + + // Search for invalid first name. + $result = core_enrol_external::search_users($course1->id, 'yada yada', true, 0, 30); + $this->assertCount(0, $result); + } } diff --git a/lib/db/services.php b/lib/db/services.php index 62b74b1db54..fd519a10501 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -634,6 +634,15 @@ $functions = array( 'type' => 'read', 'capabilities' => 'moodle/course:enrolreview' ), + 'core_enrol_search_users' => [ + 'classname' => 'core_enrol_external', + 'methodname' => 'search_users', + 'classpath' => 'enrol/externallib.php', + 'description' => 'Search within the list of course participants', + 'ajax' => true, + 'type' => 'read', + 'capabilities' => 'moodle/course:viewparticipants', + ], 'core_enrol_get_users_courses' => array( 'classname' => 'core_enrol_external', 'methodname' => 'get_users_courses', diff --git a/mod/forum/amd/build/form-user-selector.min.js b/mod/forum/amd/build/form-user-selector.min.js new file mode 100644 index 00000000000..4de61b175ca --- /dev/null +++ b/mod/forum/amd/build/form-user-selector.min.js @@ -0,0 +1,2 @@ +define ("mod_forum/form-user-selector",["jquery","core/ajax","core/templates"],function(a,b,c){return{processResults:function processResults(b,c){var d=[];a.each(c,function(a,b){d.push({value:b.id,label:b._label})});return d},transport:function transport(d,e,f,g){var h,i=a(d).attr("courseid");h=b.call([{methodname:"core_enrol_search_users",args:{courseid:i,search:e,searchanywhere:!0,page:0,perpage:30}}]);h[0].then(function(b){var d=[],e=0;a.each(b,function(a,b){d.push(c.render("mod_forum/form-user-selector-suggestion",b))});return a.when.apply(a.when,d).then(function(){var c=arguments;a.each(b,function(a,b){b._label=c[e];e++});f(b)})}).fail(g)}}}); +//# sourceMappingURL=form-user-selector.min.js.map diff --git a/mod/forum/amd/build/form-user-selector.min.js.map b/mod/forum/amd/build/form-user-selector.min.js.map new file mode 100644 index 00000000000..3d1a5ffe5cc --- /dev/null +++ b/mod/forum/amd/build/form-user-selector.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/form-user-selector.js"],"names":["define","$","Ajax","Templates","processResults","selector","results","users","each","index","user","push","value","id","label","_label","transport","query","success","failure","promise","courseid","attr","call","methodname","args","search","searchanywhere","page","perpage","then","promises","i","render","when","apply","arguments","fail"],"mappings":"AAyBAA,OAAM,gCAAC,CAAC,QAAD,CAAW,WAAX,CAAwB,gBAAxB,CAAD,CAA4C,SAASC,CAAT,CAAYC,CAAZ,CAAkBC,CAAlB,CAA6B,CAC3E,MAAyD,CACrDC,cAAc,CAAE,wBAASC,CAAT,CAAmBC,CAAnB,CAA4B,CACxC,GAAIC,CAAAA,CAAK,CAAG,EAAZ,CACAN,CAAC,CAACO,IAAF,CAAOF,CAAP,CAAgB,SAASG,CAAT,CAAgBC,CAAhB,CAAsB,CAClCH,CAAK,CAACI,IAAN,CAAW,CACPC,KAAK,CAAEF,CAAI,CAACG,EADL,CAEPC,KAAK,CAAEJ,CAAI,CAACK,MAFL,CAAX,CAIH,CALD,EAMA,MAAOR,CAAAA,CACV,CAVoD,CAYrDS,SAAS,CAAE,mBAASX,CAAT,CAAmBY,CAAnB,CAA0BC,CAA1B,CAAmCC,CAAnC,CAA4C,IAC/CC,CAAAA,CAD+C,CAE/CC,CAAQ,CAAGpB,CAAC,CAACI,CAAD,CAAD,CAAYiB,IAAZ,CAAiB,UAAjB,CAFoC,CAInDF,CAAO,CAAGlB,CAAI,CAACqB,IAAL,CAAU,CAAC,CACjBC,UAAU,CAAE,yBADK,CAEjBC,IAAI,CAAE,CACFJ,QAAQ,CAAEA,CADR,CAEFK,MAAM,CAAET,CAFN,CAGFU,cAAc,GAHZ,CAIFC,IAAI,CAAE,CAJJ,CAKFC,OAAO,CAAE,EALP,CAFW,CAAD,CAAV,CAAV,CAWAT,CAAO,CAAC,CAAD,CAAP,CAAWU,IAAX,CAAgB,SAASxB,CAAT,CAAkB,CAC9B,GAAIyB,CAAAA,CAAQ,CAAG,EAAf,CACIC,CAAC,CAAG,CADR,CAIA/B,CAAC,CAACO,IAAF,CAAOF,CAAP,CAAgB,SAASG,CAAT,CAAgBC,CAAhB,CAAsB,CAClCqB,CAAQ,CAACpB,IAAT,CAAcR,CAAS,CAAC8B,MAAV,CAAiB,yCAAjB,CAA4DvB,CAA5D,CAAd,CACH,CAFD,EAKA,MAAOT,CAAAA,CAAC,CAACiC,IAAF,CAAOC,KAAP,CAAalC,CAAC,CAACiC,IAAf,CAAqBH,CAArB,EAA+BD,IAA/B,CAAoC,UAAW,CAClD,GAAIL,CAAAA,CAAI,CAAGW,SAAX,CACAnC,CAAC,CAACO,IAAF,CAAOF,CAAP,CAAgB,SAASG,CAAT,CAAgBC,CAAhB,CAAsB,CAClCA,CAAI,CAACK,MAAL,CAAcU,CAAI,CAACO,CAAD,CAAlB,CACAA,CAAC,EACJ,CAHD,EAIAd,CAAO,CAACZ,CAAD,CAEV,CARM,CAUV,CApBD,EAoBG+B,IApBH,CAoBQlB,CApBR,CAqBH,CAhDoD,CAoD5D,CArDK,CAAN","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Enrolled user selector module.\n *\n * @module mod_forum/form-user-selector\n * @class form-user-selector\n * @package mod_forum\n * @copyright 2019 Shamim Rezaie\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {\n return /** @alias module:mod_forum/form-user-selector */ {\n processResults: function(selector, results) {\n var users = [];\n $.each(results, function(index, user) {\n users.push({\n value: user.id,\n label: user._label\n });\n });\n return users;\n },\n\n transport: function(selector, query, success, failure) {\n var promise;\n var courseid = $(selector).attr('courseid');\n\n promise = Ajax.call([{\n methodname: 'core_enrol_search_users',\n args: {\n courseid: courseid,\n search: query,\n searchanywhere: true,\n page: 0,\n perpage: 30\n }\n }]);\n\n promise[0].then(function(results) {\n var promises = [],\n i = 0;\n\n // Render the label.\n $.each(results, function(index, user) {\n promises.push(Templates.render('mod_forum/form-user-selector-suggestion', user));\n });\n\n // Apply the label to the results.\n return $.when.apply($.when, promises).then(function() {\n var args = arguments;\n $.each(results, function(index, user) {\n user._label = args[i];\n i++;\n });\n success(results);\n return;\n });\n\n }).fail(failure);\n }\n\n };\n\n});\n"],"file":"form-user-selector.min.js"} \ No newline at end of file diff --git a/mod/forum/amd/src/form-user-selector.js b/mod/forum/amd/src/form-user-selector.js new file mode 100644 index 00000000000..58191219e42 --- /dev/null +++ b/mod/forum/amd/src/form-user-selector.js @@ -0,0 +1,79 @@ +// 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 . + +/** + * Enrolled user selector module. + * + * @module mod_forum/form-user-selector + * @class form-user-selector + * @package mod_forum + * @copyright 2019 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) { + return /** @alias module:mod_forum/form-user-selector */ { + processResults: function(selector, results) { + var users = []; + $.each(results, function(index, user) { + users.push({ + value: user.id, + label: user._label + }); + }); + return users; + }, + + transport: function(selector, query, success, failure) { + var promise; + var courseid = $(selector).attr('courseid'); + + promise = Ajax.call([{ + methodname: 'core_enrol_search_users', + args: { + courseid: courseid, + search: query, + searchanywhere: true, + page: 0, + perpage: 30 + } + }]); + + promise[0].then(function(results) { + var promises = [], + i = 0; + + // Render the label. + $.each(results, function(index, user) { + promises.push(Templates.render('mod_forum/form-user-selector-suggestion', user)); + }); + + // Apply the label to the results. + return $.when.apply($.when, promises).then(function() { + var args = arguments; + $.each(results, function(index, user) { + user._label = args[i]; + i++; + }); + success(results); + return; + }); + + }).fail(failure); + } + + }; + +}); diff --git a/mod/forum/classes/form/export_form.php b/mod/forum/classes/form/export_form.php new file mode 100644 index 00000000000..905e5e63e39 --- /dev/null +++ b/mod/forum/classes/form/export_form.php @@ -0,0 +1,81 @@ +. + +/** + * This file contains the form definition for discussion export. + * + * @package mod_forum + * @copyright 2019 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_forum\form; + +defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); + +require_once($CFG->dirroot.'/mod/forum/lib.php'); +require_once($CFG->libdir.'/formslib.php'); + +/** + * Export discussion form. + * + * @package mod_forum + * @copyright 2019 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL Juv3 or later + */ +class export_form extends \moodleform { + + /** + * Define the form - called by parent constructor + */ + public function definition() { + $mform = $this->_form; + $forum = $this->_customdata['forum']; + + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + $mform->setDefault('id', $forum->get_id()); + + $options = [ + 'ajax' => 'mod_forum/form-user-selector', + 'multiple' => true, + 'noselectionstring' => get_string('allusers', 'mod_forum'), + 'courseid' => $forum->get_course_id(), + ]; + $mform->addElement('autocomplete', 'userids', get_string('users'), [], $options); + + // Get the discussions on this forum. + $vaultfactory = \mod_forum\local\container::get_vault_factory(); + $discussionvault = $vaultfactory->get_discussion_vault(); + $discussions = array_map(function($discussion) { + return $discussion->get_name(); + }, $discussionvault->get_all_discussions_in_forum($forum)); + $options = [ + 'multiple' => true, + 'noselectionstring' => get_string('alldiscussions', 'mod_forum'), + ]; + $mform->addElement('autocomplete', 'discussionids', get_string('discussions', 'mod_forum'), $discussions, $options); + + // Export formats. + $formats = \core_plugin_manager::instance()->get_plugins_of_type('dataformat'); + $options = []; + foreach ($formats as $format) { + $options[$format->name] = $format->displayname; + } + $mform->addElement('select', 'format', 'Format', $options); + $this->add_action_buttons(true, get_string('export', 'mod_forum')); + } +} diff --git a/mod/forum/classes/local/managers/capability.php b/mod/forum/classes/local/managers/capability.php index ece049e94f0..b1fd17f886f 100644 --- a/mod/forum/classes/local/managers/capability.php +++ b/mod/forum/classes/local/managers/capability.php @@ -633,4 +633,14 @@ class capability { return $canstart; } + + /** + * Checks whether the user can export the whole forum (discussions and posts). + * + * @param stdClass $user The user object. + * @return bool True if the user can export the forum or false otherwise. + */ + public function can_export_forum(stdClass $user) : bool { + return has_capability('mod/forum:exportforum', $this->get_context(), $user); + } } diff --git a/mod/forum/classes/local/vaults/discussion.php b/mod/forum/classes/local/vaults/discussion.php index f5a60bc4b6d..084c6127354 100644 --- a/mod/forum/classes/local/vaults/discussion.php +++ b/mod/forum/classes/local/vaults/discussion.php @@ -86,6 +86,20 @@ class discussion extends db_table_vault { }, $results); } + /** + * Get all discussions in the specified forum. + * + * @param forum_entity $forum + * @return array + */ + public function get_all_discussions_in_forum(forum_entity $forum): ?array { + $records = $this->get_db()->get_records(self::TABLE, [ + 'forum' => $forum->get_id(), + ]); + + return $this->transform_db_records_to_entities($records); + } + /** * Get the first discussion in the specified forum. * diff --git a/mod/forum/classes/local/vaults/post.php b/mod/forum/classes/local/vaults/post.php index ecbbf6fac39..daf5e7fb3d4 100644 --- a/mod/forum/classes/local/vaults/post.php +++ b/mod/forum/classes/local/vaults/post.php @@ -112,33 +112,24 @@ class post extends db_table_vault { bool $canseeprivatereplies, string $orderby = 'created ASC' ) : array { - $alias = $this->get_table_alias(); - - [ - 'where' => $privatewhere, - 'params' => $privateparams, - ] = $this->get_private_reply_sql($user, $canseeprivatereplies); - - $wheresql = "{$alias}.discussion = :discussionid {$privatewhere}"; - $orderbysql = $alias . '.' . $orderby; - - $sql = $this->generate_get_records_sql($wheresql, $orderbysql); - $records = $this->get_db()->get_records_sql($sql, array_merge([ - 'discussionid' => $discussionid, - ], $privateparams)); - - return $this->transform_db_records_to_entities($records); + return $this->get_from_discussion_ids($user, [$discussionid], $canseeprivatereplies, $orderby); } /** * Get the list of posts for the given discussions. * - * @param stdClass $user The user to check the unread count for + * @param stdClass $user The user to load posts for. * @param int[] $discussionids The list of discussion ids to load posts for * @param bool $canseeprivatereplies Whether this user can see all private replies or not + * @param string $orderby Order the results * @return post_entity[] */ - public function get_from_discussion_ids(stdClass $user, array $discussionids, bool $canseeprivatereplies) : array { + public function get_from_discussion_ids( + stdClass $user, + array $discussionids, + bool $canseeprivatereplies, + string $orderby = '' + ) : array { if (empty($discussionids)) { return []; } @@ -153,12 +144,65 @@ class post extends db_table_vault { $wheresql = "{$alias}.discussion {$insql} {$privatewhere}"; - $sql = $this->generate_get_records_sql($wheresql, ''); + if ($orderby) { + $orderbysql = $alias . '.' . $orderby; + } else { + $orderbysql = ''; + } + + $sql = $this->generate_get_records_sql($wheresql, $orderbysql); $records = $this->get_db()->get_records_sql($sql, array_merge($params, $privateparams)); return $this->transform_db_records_to_entities($records); } + /** + * The method returns posts made by the supplied users in the supplied discussions. + * + * @param stdClass $user Only used when restricting private replies + * @param int[] $discussionids The list of discussion ids to load posts for + * @param int[] $userids Only return posts made by these users + * @param bool $canseeprivatereplies Whether this user can see all private replies or not + * @param string $orderby Order the results + * @return post_entity[] + */ + public function get_from_discussion_ids_and_user_ids( + stdClass $user, + array $discussionids, + array $userids, + bool $canseeprivatereplies, + string $orderby = '' + ): array { + if (empty($discussionids) || empty($userids)) { + return []; + } + + $alias = $this->get_table_alias(); + + list($indiscussionssql, $indiscussionsparams) = $this->get_db()->get_in_or_equal($discussionids, SQL_PARAMS_NAMED); + list($inuserssql, $inusersparams) = $this->get_db()->get_in_or_equal($userids, SQL_PARAMS_NAMED); + + [ + 'where' => $privatewhere, + 'params' => $privateparams, + ] = $this->get_private_reply_sql($user, $canseeprivatereplies); + + $wheresql = "{$alias}.discussion {$indiscussionssql} + AND {$alias}.userid {$inuserssql} + {$privatewhere}"; + + if ($orderby) { + $orderbysql = $alias . '.' . $orderby; + } else { + $orderbysql = ''; + } + + $sql = $this->generate_get_records_sql($wheresql, $orderbysql); + $records = $this->get_db()->get_records_sql($sql, array_merge($indiscussionsparams, $inusersparams, $privateparams)); + + return $this->transform_db_records_to_entities($records); + } + /** * Load a list of replies to the given post. This will load all descendants of the post. * That is, all direct replies and replies to those replies etc. diff --git a/mod/forum/db/access.php b/mod/forum/db/access.php index 3a5733c33df..0a9e7221fbf 100644 --- a/mod/forum/db/access.php +++ b/mod/forum/db/access.php @@ -328,6 +328,17 @@ $capabilities = array( 'manager' => CAP_ALLOW ) ), + 'mod/forum:exportforum' => array( + 'riskbitmask' => RISK_PERSONAL, + + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ) + ), 'mod/forum:exportpost' => array( 'riskbitmask' => RISK_PERSONAL, diff --git a/mod/forum/export.php b/mod/forum/export.php new file mode 100644 index 00000000000..419997754a6 --- /dev/null +++ b/mod/forum/export.php @@ -0,0 +1,125 @@ +. + +/** + * Page to export forum discussions. + * + * @package mod_forum + * @copyright 2019 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define('NO_OUTPUT_BUFFERING', true); + +require_once(__DIR__ . '/../../config.php'); +require_once($CFG->libdir . '/adminlib.php'); +require_once($CFG->libdir . '/dataformatlib.php'); + +$forumid = required_param('id', PARAM_INT); + +$vaultfactory = mod_forum\local\container::get_vault_factory(); +$managerfactory = mod_forum\local\container::get_manager_factory(); +$legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); + +$forumvault = $vaultfactory->get_forum_vault(); + +$forum = $forumvault->get_from_id($forumid); +if (empty($forum)) { + throw new moodle_exception('Unable to find forum with id ' . $forumid); +} + +$capabilitymanager = $managerfactory->get_capability_manager($forum); +if (!$capabilitymanager->can_export_forum($USER)) { + throw new moodle_exception('cannotexportforum', 'forum'); +} + +$course = $forum->get_course_record(); +$coursemodule = $forum->get_course_module_record(); +$cm = cm_info::create($coursemodule); + +require_course_login($course, true, $cm); + +$url = new moodle_url('/mod/forum/export.php'); +$pagetitle = get_string('export', 'mod_forum'); +$context = $forum->get_context(); + +$form = new mod_forum\form\export_form($url->out(false), [ + 'forum' => $forum +]); + +if ($form->is_cancelled()) { + redirect(new moodle_url('/mod/forum/view.php', ['id' => $cm->id])); +} else if ($data = $form->get_data()) { + require_sesskey(); + + $dataformat = $data->format; + + $discussionvault = $vaultfactory->get_discussion_vault(); + $postvault = $vaultfactory->get_post_vault(); + $discussionids = []; + if ($data->discussionids) { + $discussionids = $data->discussionids; + } else { + $discussions = $discussionvault->get_all_discussions_in_forum($forum); + $discussionids = array_map(function ($discussion) { + return $discussion->get_id(); + }, $discussions); + } + + if ($data->userids) { + $posts = $postvault->get_from_discussion_ids_and_user_ids($USER, + $discussionids, + $data->userids, + $capabilitymanager->can_view_any_private_reply($USER)); + } else { + $posts = $postvault->get_from_discussion_ids($USER, + $discussionids, + $capabilitymanager->can_view_any_private_reply($USER)); + } + + $fields = ['id', 'discussion', 'parent', 'userid', 'created', 'modified', 'mailed', 'subject', 'message', + 'messageformat', 'messagetrust', 'attachment', 'totalscore', 'mailnow', 'deleted', 'privatereplyto']; + + $datamapper = $legacydatamapperfactory->get_post_data_mapper(); + $exportdata = new ArrayObject($datamapper->to_legacy_objects($posts)); + $iterator = $exportdata->getIterator(); + + require_once($CFG->libdir . '/dataformatlib.php'); + $filename = clean_filename('discussion'); + download_as_dataformat($filename, $dataformat, $fields, $iterator, function($exportdata) use ($fields) { + $data = $exportdata; + foreach ($fields as $field) { + // Convert any boolean fields to their integer equivalent for output. + if (is_bool($data->$field)) { + $data->$field = (int) $data->$field; + } + } + return $data; + }); + die; +} + +$PAGE->set_context($context); +$PAGE->set_url($url); +$PAGE->set_title($pagetitle); +$PAGE->set_pagelayout('admin'); +$PAGE->set_heading($pagetitle); + +echo $OUTPUT->header(); +echo $OUTPUT->heading($pagetitle); + +$form->display(); + +echo $OUTPUT->footer(); diff --git a/mod/forum/lang/en/forum.php b/mod/forum/lang/en/forum.php index ddab9f8f03b..090e2717e17 100644 --- a/mod/forum/lang/en/forum.php +++ b/mod/forum/lang/en/forum.php @@ -29,12 +29,14 @@ $string['addanewquestion'] = 'Add a new question'; $string['addanewtopic'] = 'Add a new topic'; $string['addtofavourites'] = 'Star this discussion'; $string['advancedsearch'] = 'Advanced search'; +$string['alldiscussions'] = 'All discussions'; $string['allforums'] = 'All forums'; $string['allowdiscussions'] = 'Can a {$a} post to this forum?'; $string['allowsallsubscribe'] = 'This forum allows everyone to choose whether to subscribe or not'; $string['allowsdiscussions'] = 'This forum allows each person to start one discussion topic.'; $string['allsubscribe'] = 'Subscribe to all forums'; $string['allunsubscribe'] = 'Unsubscribe from all forums'; +$string['allusers'] = 'All users'; $string['alreadyfirstpost'] = 'This is already the first post in the discussion'; $string['anyfile'] = 'Any file'; $string['areaattachment'] = 'Attachments'; @@ -66,6 +68,7 @@ $string['cannotcreateinstanceforteacher'] = 'Could not create new course module $string['cannotdeletepost'] = 'You can\'t delete this post!'; $string['cannotdeletediscussioninsinglediscussion'] = 'You cannot delete the first post in a single discussion'; $string['cannoteditposts'] = 'You can\'t edit other people\'s posts!'; +$string['cannotexportforum'] = 'You cannot export this forum'; $string['cannotfinddiscussion'] = 'Could not find the discussion in this forum'; $string['cannotfindfirstpost'] = 'Could not find the first post in this forum'; $string['cannotfindorcreateforum'] = 'Could not find or create a main announcements forum for the site'; @@ -242,6 +245,7 @@ $string['everyonecannowchoose'] = 'Everyone can now choose to be subscribed'; $string['everyoneisnowsubscribed'] = 'Everyone is now subscribed to this forum'; $string['everyoneissubscribed'] = 'Everyone is subscribed to this forum'; $string['existingsubscribers'] = 'Existing subscribers'; +$string['export'] = 'Export'; $string['exportdiscussion'] = 'Export whole discussion to portfolio'; $string['exportattachmentname'] = 'Export attachment {$a} to portfolio'; $string['firstpost'] = 'First post'; @@ -268,6 +272,7 @@ $string['forum:deleteanypost'] = 'Delete any posts (anytime)'; $string['forum:deleteownpost'] = 'Delete own posts (within deadline)'; $string['forum:editanypost'] = 'Edit any post'; $string['forum:exportdiscussion'] = 'Export whole discussion'; +$string['forum:exportforum'] = 'Export forum'; $string['forum:exportownpost'] = 'Export own post'; $string['forum:exportpost'] = 'Export post'; $string['forumintro'] = 'Description'; diff --git a/mod/forum/lib.php b/mod/forum/lib.php index 62f242353a3..8f086d08149 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -5245,6 +5245,11 @@ function forum_extend_settings_navigation(settings_navigation $settingsnav, navi $PAGE->cm->context = context_module::instance($PAGE->cm->instance); } + $vaultfactory = mod_forum\local\container::get_vault_factory(); + $managerfactory = mod_forum\local\container::get_manager_factory(); + $forumvault = $vaultfactory->get_forum_vault(); + $forumentity = $forumvault->get_from_id($forumobject->id); + $params = $PAGE->url->params(); if (!empty($params['d'])) { $discussionid = $params['d']; @@ -5379,6 +5384,12 @@ function forum_extend_settings_navigation(settings_navigation $settingsnav, navi $url = new moodle_url(rss_get_url($PAGE->cm->context->id, $userid, "mod_forum", $forumobject->id)); $forumnode->add($string, $url, settings_navigation::TYPE_SETTING, null, null, new pix_icon('i/rss', '')); } + + $capabilitymanager = $managerfactory->get_capability_manager($forumentity); + if ($capabilitymanager->can_export_forum($USER)) { + $url = new moodle_url('/mod/forum/export.php', ['id' => $forumobject->id]); + $forumnode->add(get_string('export', 'mod_forum'), $url, navigation_node::TYPE_SETTING); + } } /** diff --git a/mod/forum/templates/form-user-selector-suggestion.mustache b/mod/forum/templates/form-user-selector-suggestion.mustache new file mode 100644 index 00000000000..377cf093fe4 --- /dev/null +++ b/mod/forum/templates/form-user-selector-suggestion.mustache @@ -0,0 +1,52 @@ +{{! + 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 . +}} +{{! + @template mod_forum/form-user-selector-suggestion + + Moodle template for the list of valid options in an autocomplate form element. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * fullname string Users full name + * email string user email field + + Example context (json): + { + "fullname": "Admin User", + "extrafields": [ + { + "name": "email", + "value": "admin@example.com" + }, + { + "name": "phone1", + "value": "0123456789" + } + ] + } +}} + + {{fullname}} + {{#extrafields}} + {{value}} + {{/extrafields}} + diff --git a/mod/forum/tests/vaults_post_test.php b/mod/forum/tests/vaults_post_test.php index 3d3efadf192..92d399ac877 100644 --- a/mod/forum/tests/vaults_post_test.php +++ b/mod/forum/tests/vaults_post_test.php @@ -194,6 +194,20 @@ class mod_forum_vaults_post_testcase extends advanced_testcase { $this->assertArrayHasKey($post2->id, $entities); $this->assertArrayHasKey($post3->id, $entities); $this->assertArrayHasKey($post4->id, $entities); + + // Test ordering by id descending. + $entities = $this->vault->get_from_discussion_ids($user, [$discussion1->id, $discussion2->id], false, 'id DESC'); + $this->assertEquals($post4->id, array_values($entities)[0]->get_id()); + $this->assertEquals($post3->id, array_values($entities)[1]->get_id()); + $this->assertEquals($post2->id, array_values($entities)[2]->get_id()); + $this->assertEquals($post1->id, array_values($entities)[3]->get_id()); + + // Test ordering by id ascending. + $entities = $this->vault->get_from_discussion_ids($user, [$discussion1->id, $discussion2->id], false, 'id ASC'); + $this->assertEquals($post1->id, array_values($entities)[0]->get_id()); + $this->assertEquals($post2->id, array_values($entities)[1]->get_id()); + $this->assertEquals($post3->id, array_values($entities)[2]->get_id()); + $this->assertEquals($post4->id, array_values($entities)[3]->get_id()); } /** @@ -213,38 +227,85 @@ class mod_forum_vaults_post_testcase extends advanced_testcase { [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); - [$discussion, $post] = $this->helper_post_to_forum($forum, $teacher); - $reply = $this->helper_post_to_discussion($forum, $discussion, $teacher, [ - 'privatereplyto' => $student->id, - ]); + + // Create the posts structure below. + // Forum: + // -> Post (student 1) + // ---> Post private reply (teacher 1) + // -> Otherpost (teacher 1) + // ---> Otherpost private reply (teacher 2) + // ---> Otherpost reply (student 1) + // ----> Otherpost reply private reply (teacher 1). + [$discussion, $post] = $this->helper_post_to_forum($forum, $student); + $postprivatereply = $this->helper_reply_to_post($post, $teacher, [ + 'privatereplyto' => $student->id + ]); [$otherdiscussion, $otherpost] = $this->helper_post_to_forum($forum, $teacher); + $otherpostprivatereply = $this->helper_reply_to_post($otherpost, $otherteacher, [ + 'privatereplyto' => $teacher->id, + ]); + $otherpostreply = $this->helper_reply_to_post($otherpost, $student); + $otherpostreplyprivatereply = $this->helper_reply_to_post($otherpostreply, $teacher, [ + 'privatereplyto' => $student->id + ]); - // The user is the author. + // Teacher 1. Request all posts from the vault, telling the vault that the teacher CAN see private replies made by anyone. $entities = $this->vault->get_from_discussion_ids($teacher, [$discussion->id, $otherdiscussion->id], true); - $this->assertCount(3, $entities); + $this->assertCount(6, $entities); $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. - $this->assertArrayHasKey($reply->id, $entities); + $this->assertArrayHasKey($postprivatereply->id, $entities); $this->assertArrayHasKey($otherpost->id, $entities); + $this->assertArrayHasKey($otherpostprivatereply->id, $entities); + $this->assertArrayHasKey($otherpostreply->id, $entities); + $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); - // The user is the intended recipient. + // Student 1. Request all posts from the vault, telling the vault that the student CAN'T see private replies made by anyone. + // Teacher2's private reply to otherpost is omitted. $entities = $this->vault->get_from_discussion_ids($student, [$discussion->id, $otherdiscussion->id], false); - $this->assertCount(3, $entities); + $this->assertCount(5, $entities); $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. - $this->assertArrayHasKey($reply->id, $entities); + $this->assertArrayHasKey($postprivatereply->id, $entities); $this->assertArrayHasKey($otherpost->id, $entities); + $this->assertArrayHasKey($otherpostreply->id, $entities); + $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); - // The user is another teacher.. + // Student 1. Request all posts from the vault, telling the vault that student CAN see all private replies made. + // The private reply made by teacher 2 to otherpost is now included. + $entities = $this->vault->get_from_discussion_ids($student, [$discussion->id, $otherdiscussion->id], true); + $this->assertCount(6, $entities); + $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. + $this->assertArrayHasKey($postprivatereply->id, $entities); + $this->assertArrayHasKey($otherpost->id, $entities); + $this->assertArrayHasKey($otherpostprivatereply->id, $entities); + $this->assertArrayHasKey($otherpostreply->id, $entities); + $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); + + // Teacher 2. Request all posts from the vault, telling the vault that teacher2 CAN see all private replies made. $entities = $this->vault->get_from_discussion_ids($otherteacher, [$discussion->id, $otherdiscussion->id], true); + $this->assertCount(6, $entities); + $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. + $this->assertArrayHasKey($postprivatereply->id, $entities); + $this->assertArrayHasKey($otherpost->id, $entities); + $this->assertArrayHasKey($otherpostprivatereply->id, $entities); + $this->assertArrayHasKey($otherpostreply->id, $entities); + $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); + + // Teacher 2. Request all posts from the vault, telling the vault that teacher2 CANNOT see all private replies made. + // The private replies not relating to teacher 2 directly are omitted. + $entities = $this->vault->get_from_discussion_ids($otherteacher, [$discussion->id, $otherdiscussion->id], false); + $this->assertCount(4, $entities); + $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. + $this->assertArrayHasKey($otherpost->id, $entities); + $this->assertArrayHasKey($otherpostprivatereply->id, $entities); + $this->assertArrayHasKey($otherpostreply->id, $entities); + + // Student 2. Request all posts from the vault, telling the vault that student2 CAN'T see all private replies made. + // All private replies are omitted, as none relate to student2. + $entities = $this->vault->get_from_discussion_ids($otherstudent, [$discussion->id, $otherdiscussion->id], false); $this->assertCount(3, $entities); $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. - $this->assertArrayHasKey($reply->id, $entities); - $this->assertArrayHasKey($otherpost->id, $entities); - - // The user is a different student. - $entities = $this->vault->get_from_discussion_ids($otherstudent, [$discussion->id, $otherdiscussion->id], false); - $this->assertCount(2, $entities); - $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. $this->assertArrayHasKey($otherpost->id, $entities); + $this->assertArrayHasKey($otherpostreply->id, $entities); } /** @@ -877,4 +938,175 @@ class mod_forum_vaults_post_testcase extends advanced_testcase { $this->assertEquals([], $this->vault->get_first_post_for_discussion_ids([])); } + + /** + * Test get_from_discussion_ids_and_user_ids. + * + * @covers ::get_from_discussion_ids_and_user_ids + * @covers :: + */ + public function test_get_from_discussion_ids_and_user_ids() { + $this->resetAfterTest(); + + $datagenerator = $this->getDataGenerator(); + $course = $datagenerator->create_course(); + [$user, $user2] = $this->helper_create_users($course, 2, 'student'); + $forum = $datagenerator->create_module('forum', ['course' => $course->id]); + + [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); + $post2 = $this->helper_reply_to_post($post1, $user); + $post3 = $this->helper_reply_to_post($post1, $user); + + [$discussion2, $post4] = $this->helper_post_to_forum($forum, $user); + $discussionids = [$discussion1->id, $discussion2->id]; + + $userids = [$user->id]; + $entities = array_values($this->vault->get_from_discussion_ids_and_user_ids($user, + $discussionids, + $userids, + true, + 'id ASC')); + + $this->assertCount(4, $entities); + $this->assertEquals($post1->id, $entities[0]->get_id()); + $this->assertEquals($post2->id, $entities[1]->get_id()); + $this->assertEquals($post3->id, $entities[2]->get_id()); + $this->assertEquals($post4->id, $entities[3]->get_id()); + + $entities = $this->vault->get_from_discussion_ids_and_user_ids($user, [$discussion1->id], $userids, false); + $this->assertCount(3, $entities); + $this->assertArrayHasKey($post1->id, $entities); + $this->assertArrayHasKey($post2->id, $entities); + $this->assertArrayHasKey($post3->id, $entities); + + $entities = $this->vault->get_from_discussion_ids_and_user_ids($user, [$discussion1->id, $discussion2->id], + [$user->id, $user2->id], false); + $this->assertCount(4, $entities); + $this->assertArrayHasKey($post1->id, $entities); + $this->assertArrayHasKey($post2->id, $entities); + $this->assertArrayHasKey($post3->id, $entities); + $this->assertArrayHasKey($post4->id, $entities); + + // Test ordering by id descending. + $entities = $this->vault->get_from_discussion_ids_and_user_ids($user, [$discussion1->id, $discussion2->id], + [$user->id], false, 'id DESC'); + $this->assertEquals($post4->id, array_values($entities)[0]->get_id()); + $this->assertEquals($post3->id, array_values($entities)[1]->get_id()); + $this->assertEquals($post2->id, array_values($entities)[2]->get_id()); + $this->assertEquals($post1->id, array_values($entities)[3]->get_id()); + + // Test ordering by id ascending. + $entities = $this->vault->get_from_discussion_ids_and_user_ids($user, [$discussion1->id, $discussion2->id], + [$user->id], false, 'id ASC'); + $this->assertEquals($post1->id, array_values($entities)[0]->get_id()); + $this->assertEquals($post2->id, array_values($entities)[1]->get_id()); + $this->assertEquals($post3->id, array_values($entities)[2]->get_id()); + $this->assertEquals($post4->id, array_values($entities)[3]->get_id()); + } + + /** + * Test get_from_discussion_ids_and_user_ids when no discussion ids were provided. + * + * @covers ::get_from_discussion_ids_and_user_ids + */ + public function test_get_from_discussion_ids_and_user_ids_empty() { + $this->resetAfterTest(); + + $datagenerator = $this->getDataGenerator(); + $course = $datagenerator->create_course(); + [$student1, $student2] = $this->helper_create_users($course, 2, 'student'); + $forum = $datagenerator->create_module('forum', ['course' => $course->id]); + [$discussion, $post] = $this->helper_post_to_forum($forum, $student1); + $this->assertEquals([], $this->vault->get_from_discussion_ids_and_user_ids($student1, [], [], false)); + $this->assertEquals([], $this->vault->get_from_discussion_ids_and_user_ids($student1, [$discussion->id], [], false)); + $this->assertEquals([], $this->vault->get_from_discussion_ids_and_user_ids($student1, [], [$student2->id], false)); + } + + /** + * Ensure that selecting posts in a discussion only returns posts that the user can see, when considering private + * replies. + * + * @covers ::get_from_discussion_ids_and_user_ids + * @covers :: + */ + public function test_get_from_discussion_ids_and_user_ids_private_replies() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $forum = $this->getDataGenerator()->create_module('forum', [ + 'course' => $course->id, + ]); + + [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); + [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); + + // Create the posts structure below. + // Forum: + // -> Post (student 1) + // ---> Post private reply (teacher 1) + // -> Otherpost (teacher 1) + // ---> Otherpost private reply (teacher 2) + // ---> Otherpost reply (student 1) + // ----> Otherpost reply private reply (teacher 1). + [$discussion, $post] = $this->helper_post_to_forum($forum, $student); + $postprivatereply = $this->helper_reply_to_post($post, $teacher, [ + 'privatereplyto' => $student->id + ]); + [$otherdiscussion, $otherpost] = $this->helper_post_to_forum($forum, $teacher); + $otherpostprivatereply = $this->helper_reply_to_post($otherpost, $otherteacher, [ + 'privatereplyto' => $teacher->id, + ]); + $otherpostreply = $this->helper_reply_to_post($otherpost, $student); + $otherpostreplyprivatereply = $this->helper_reply_to_post($otherpostreply, $teacher, [ + 'privatereplyto' => $student->id + ]); + + $userids = [$otherstudent->id, $teacher->id, $otherteacher->id]; + $discussionids = [$discussion->id, $otherdiscussion->id]; + + // Teacher 1. Request all posts from the vault, telling the vault that the teacher CAN see private replies made by anyone. + $entities = $this->vault->get_from_discussion_ids_and_user_ids($teacher, $discussionids, $userids, true); + $this->assertCount(4, $entities); + $this->assertArrayHasKey($postprivatereply->id, $entities); + $this->assertArrayHasKey($otherpost->id, $entities); + $this->assertArrayHasKey($otherpostprivatereply->id, $entities); + $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); + + // Student 1. Request all posts from the vault, telling the vault that the student CAN'T see private replies made by anyone. + // Teacher2's private reply to otherpost is omitted. + $entities = $this->vault->get_from_discussion_ids_and_user_ids($student, $discussionids, $userids, false); + $this->assertCount(3, $entities); + $this->assertArrayHasKey($postprivatereply->id, $entities); + $this->assertArrayHasKey($otherpost->id, $entities); + $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); + + // Student 1. Request all posts from the vault, telling the vault that student CAN see all private replies made. + // The private reply made by teacher 2 to otherpost is now included. + $entities = $this->vault->get_from_discussion_ids_and_user_ids($student, $discussionids, $userids, true); + $this->assertCount(4, $entities); + $this->assertArrayHasKey($postprivatereply->id, $entities); + $this->assertArrayHasKey($otherpost->id, $entities); + $this->assertArrayHasKey($otherpostprivatereply->id, $entities); + $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); + + // Teacher 2. Request all posts from the vault, telling the vault that teacher2 CAN see all private replies made. + $entities = $this->vault->get_from_discussion_ids_and_user_ids($otherteacher, $discussionids, $userids, true); + $this->assertCount(4, $entities); + $this->assertArrayHasKey($otherpost->id, $entities); + $this->assertArrayHasKey($otherpostprivatereply->id, $entities); + $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); + + // Teacher 2. Request all posts from the vault, telling the vault that teacher2 CANNOT see all private replies made. + // The private replies not relating to teacher 2 directly are omitted. + $entities = $this->vault->get_from_discussion_ids_and_user_ids($otherteacher, $discussionids, $userids, false); + $this->assertCount(2, $entities); + $this->assertArrayHasKey($otherpost->id, $entities); + $this->assertArrayHasKey($otherpostprivatereply->id, $entities); + + // Student 2. Request all posts from the vault, telling the vault that student2 CAN'T see all private replies made. + // All private replies are omitted, as none relate to student2. + $entities = $this->vault->get_from_discussion_ids_and_user_ids($otherstudent, $discussionids, $userids, false); + $this->assertCount(1, $entities); + $this->assertArrayHasKey($otherpost->id, $entities); + } } diff --git a/mod/forum/version.php b/mod/forum/version.php index 0e875b0e790..2777c1ffea8 100644 --- a/mod/forum/version.php +++ b/mod/forum/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2019052000; // The current module version (Date: YYYYMMDDXX) +$plugin->version = 2019052001; // The current module version (Date: YYYYMMDDXX) $plugin->requires = 2019051100; // Requires this Moodle version $plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics) diff --git a/version.php b/version.php index 838f4da6f8b..fc6e40a3ec3 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2019092000.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2019092000.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.