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.