From 7e0b0bc14b141ae0dccdabb4392edb09c17175f5 Mon Sep 17 00:00:00 2001 From: Mihail Geshoski Date: Fri, 30 Sep 2022 01:44:45 +0800 Subject: [PATCH 1/2] MDL-68437 course: ws support to optionally fetch only active users Modifies the 'core_course_get_enrolled_users_by_cmid' webservice and the generic js methods that call this webservice and enables defining whether the ws should only return the active users or all enrolled users in the course. --- course/amd/build/repository.min.js | 2 +- course/amd/build/repository.min.js.map | 2 +- course/amd/src/repository.js | 4 ++- course/externallib.php | 13 +++++---- course/tests/externallib_test.php | 38 ++++++++++++++++++++++++++ lib/db/services.php | 2 +- 6 files changed, 51 insertions(+), 10 deletions(-) diff --git a/course/amd/build/repository.min.js b/course/amd/build/repository.min.js index 113b947fd09..090572e95e2 100644 --- a/course/amd/build/repository.min.js +++ b/course/amd/build/repository.min.js @@ -5,6 +5,6 @@ define("core_course/repository",["exports","core/ajax"],(function(_exports,_ajax * @module core_course/repository * @copyright 2018 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};var _default={getEnrolledCoursesByTimelineClassification:(classification,limit,offset,sort)=>{const args={classification:classification};void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_enrolled_courses_by_timeline_classification",args:args};return _ajax.default.call([request])[0]},getLastAccessedCourses:(userid,limit,offset,sort)=>{const args={};void 0!==userid&&(args.userid=userid),void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_recent_courses",args:args};return _ajax.default.call([request])[0]},getUsersFromCourseModuleID:(cmid,groupID)=>{var request={methodname:"core_course_get_enrolled_users_by_cmid",args:{cmid:cmid,groupid:groupID}};return _ajax.default.call([request])[0]},toggleManualCompletion:(cmid,completed)=>{const request={methodname:"core_completion_update_activity_completion_status_manually",args:{cmid:cmid,completed:completed}};return _ajax.default.call([request])[0]},getEnrolledCoursesWithEventsByTimelineClassification:function(classification){let limit=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,offset=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,sort=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,searchValue=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,eventsFrom=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null,eventsTo=arguments.length>6&&void 0!==arguments[6]?arguments[6]:null;const args={classification:classification,limit:limit,offset:offset,sort:sort,eventsfrom:eventsFrom,eventsto:eventsTo,searchvalue:searchValue},request={methodname:"core_course_get_enrolled_courses_with_action_events_by_timeline_classification",args:args};return _ajax.default.call([request])[0]}};return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};var _default={getEnrolledCoursesByTimelineClassification:(classification,limit,offset,sort)=>{const args={classification:classification};void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_enrolled_courses_by_timeline_classification",args:args};return _ajax.default.call([request])[0]},getLastAccessedCourses:(userid,limit,offset,sort)=>{const args={};void 0!==userid&&(args.userid=userid),void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_recent_courses",args:args};return _ajax.default.call([request])[0]},getUsersFromCourseModuleID:function(cmid,groupID){let onlyActive=arguments.length>2&&void 0!==arguments[2]&&arguments[2];var request={methodname:"core_course_get_enrolled_users_by_cmid",args:{cmid:cmid,groupid:groupID,onlyactive:onlyActive}};return _ajax.default.call([request])[0]},toggleManualCompletion:(cmid,completed)=>{const request={methodname:"core_completion_update_activity_completion_status_manually",args:{cmid:cmid,completed:completed}};return _ajax.default.call([request])[0]},getEnrolledCoursesWithEventsByTimelineClassification:function(classification){let limit=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,offset=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,sort=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,searchValue=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,eventsFrom=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null,eventsTo=arguments.length>6&&void 0!==arguments[6]?arguments[6]:null;const args={classification:classification,limit:limit,offset:offset,sort:sort,eventsfrom:eventsFrom,eventsto:eventsTo,searchvalue:searchValue},request={methodname:"core_course_get_enrolled_courses_with_action_events_by_timeline_classification",args:args};return _ajax.default.call([request])[0]}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/course/amd/build/repository.min.js.map b/course/amd/build/repository.min.js.map index f4c17fd8a08..b2b7eeddad9 100644 --- a/course/amd/build/repository.min.js.map +++ b/course/amd/build/repository.min.js.map @@ -1 +1 @@ -{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"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 * A javascript module to handle course ajax actions.\n *\n * @module core_course/repository\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Get the list of courses that the logged in user is enrolled in for a given\n * timeline classification.\n *\n * @param {string} classification past, inprogress, or future\n * @param {int} limit Only return this many results\n * @param {int} offset Skip this many results from the start of the result set\n * @param {string} sort Column to sort by and direction, e.g. 'shortname asc'\n * @return {object} jQuery promise resolved with courses.\n */\nconst getEnrolledCoursesByTimelineClassification = (classification, limit, offset, sort) => {\n const args = {\n classification: classification\n };\n\n if (typeof limit !== 'undefined') {\n args.limit = limit;\n }\n\n if (typeof offset !== 'undefined') {\n args.offset = offset;\n }\n\n if (typeof sort !== 'undefined') {\n args.sort = sort;\n }\n\n const request = {\n methodname: 'core_course_get_enrolled_courses_by_timeline_classification',\n args: args\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get a list of courses that the logged in user is enrolled in, where they have at least one action event,\n * for a given timeline classification.\n *\n * @param {string} classification past, inprogress, or future\n * @param {int} limit The maximum number of courses to return\n * @param {int} offset Skip this many results from the start of the result set\n * @param {string} sort Column to sort by and direction, e.g. 'shortname asc'\n * @param {string} searchValue Optional text search value\n * @param {int} eventsFrom Optional start timestamp (inclusive) that the course should have event(s) in\n * @param {int} eventsTo Optional end timestamp (inclusive) that the course should have event(s) in\n * @return {object} jQuery promise resolved with courses.\n */\n const getEnrolledCoursesWithEventsByTimelineClassification = (classification, limit = 0, offset = 0, sort = null,\n searchValue = null, eventsFrom = null, eventsTo = null) => {\n\n const args = {\n classification: classification,\n limit: limit,\n offset: offset,\n sort: sort,\n eventsfrom: eventsFrom,\n eventsto: eventsTo,\n searchvalue: searchValue,\n };\n\n const request = {\n methodname: 'core_course_get_enrolled_courses_with_action_events_by_timeline_classification',\n args: args\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get the list of courses that the user has most recently accessed.\n *\n * @method getLastAccessedCourses\n * @param {int} userid User from which the courses will be obtained\n * @param {int} limit Only return this many results\n * @param {int} offset Skip this many results from the start of the result set\n * @param {string} sort Column to sort by and direction, e.g. 'shortname asc'\n * @return {promise} Resolved with an array of courses\n */\nconst getLastAccessedCourses = (userid, limit, offset, sort) => {\n const args = {};\n\n if (typeof userid !== 'undefined') {\n args.userid = userid;\n }\n\n if (typeof limit !== 'undefined') {\n args.limit = limit;\n }\n\n if (typeof offset !== 'undefined') {\n args.offset = offset;\n }\n\n if (typeof sort !== 'undefined') {\n args.sort = sort;\n }\n\n const request = {\n methodname: 'core_course_get_recent_courses',\n args: args\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get the list of users enrolled in this cmid.\n *\n * @param {Number} cmid Course Module from which the users will be obtained\n * @param {Number} groupID Group ID from which the users will be obtained\n * @returns {Promise} Promise containing a list of users\n */\nconst getEnrolledUsersFromCourseModuleID = (cmid, groupID) => {\n var request = {\n methodname: 'core_course_get_enrolled_users_by_cmid',\n args: {\n cmid: cmid,\n groupid: groupID,\n },\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Toggle the completion state of an activity with manual completion.\n *\n * @param {Number} cmid The course module ID.\n * @param {Boolean} completed Whether to set as complete or not.\n * @returns {object} jQuery promise\n */\nconst toggleManualCompletion = (cmid, completed) => {\n const request = {\n methodname: 'core_completion_update_activity_completion_status_manually',\n args: {\n cmid,\n completed,\n }\n };\n return Ajax.call([request])[0];\n};\n\nexport default {\n getEnrolledCoursesByTimelineClassification,\n getLastAccessedCourses,\n getUsersFromCourseModuleID: getEnrolledUsersFromCourseModuleID,\n toggleManualCompletion,\n getEnrolledCoursesWithEventsByTimelineClassification,\n};\n"],"names":["getEnrolledCoursesByTimelineClassification","classification","limit","offset","sort","args","request","methodname","Ajax","call","getLastAccessedCourses","userid","getUsersFromCourseModuleID","cmid","groupID","groupid","toggleManualCompletion","completed","getEnrolledCoursesWithEventsByTimelineClassification","searchValue","eventsFrom","eventsTo","eventsfrom","eventsto","searchvalue"],"mappings":";;;;;;;sJAwKe,CACXA,2CAtI+C,CAACC,eAAgBC,MAAOC,OAAQC,cACzEC,KAAO,CACTJ,eAAgBA,qBAGC,IAAVC,QACPG,KAAKH,MAAQA,YAGK,IAAXC,SACPE,KAAKF,OAASA,aAGE,IAATC,OACPC,KAAKD,KAAOA,YAGVE,QAAU,CACZC,WAAY,8DACZF,KAAMA,aAGHG,cAAKC,KAAK,CAACH,UAAU,IAiH5BI,uBAlE2B,CAACC,OAAQT,MAAOC,OAAQC,cAC7CC,KAAO,QAES,IAAXM,SACPN,KAAKM,OAASA,aAGG,IAAVT,QACPG,KAAKH,MAAQA,YAGK,IAAXC,SACPE,KAAKF,OAASA,aAGE,IAATC,OACPC,KAAKD,KAAOA,YAGVE,QAAU,CACZC,WAAY,iCACZF,KAAMA,aAGHG,cAAKC,KAAK,CAACH,UAAU,IA2C5BM,2BAjCuC,CAACC,KAAMC,eAC1CR,QAAU,CACVC,WAAY,yCACZF,KAAM,CACFQ,KAAMA,KACNE,QAASD,iBAIVN,cAAKC,KAAK,CAACH,UAAU,IAyB5BU,uBAf2B,CAACH,KAAMI,mBAC5BX,QAAU,CACZC,WAAY,6DACZF,KAAM,CACFQ,KAAAA,KACAI,UAAAA,mBAGDT,cAAKC,KAAK,CAACH,UAAU,IAQ5BY,qDApG0D,SAACjB,oBAAgBC,6DAAQ,EAAGC,8DAAS,EAAGC,4DAAO,KACrGe,mEAAc,KAAMC,kEAAa,KAAMC,gEAAW,WAEhDhB,KAAO,CACTJ,eAAgBA,eAChBC,MAAOA,MACPC,OAAQA,OACRC,KAAMA,KACNkB,WAAYF,WACZG,SAAUF,SACVG,YAAaL,aAGXb,QAAU,CACZC,WAAY,iFACZF,KAAMA,aAGHG,cAAKC,KAAK,CAACH,UAAU"} \ No newline at end of file +{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"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 * A javascript module to handle course ajax actions.\n *\n * @module core_course/repository\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Get the list of courses that the logged in user is enrolled in for a given\n * timeline classification.\n *\n * @param {string} classification past, inprogress, or future\n * @param {int} limit Only return this many results\n * @param {int} offset Skip this many results from the start of the result set\n * @param {string} sort Column to sort by and direction, e.g. 'shortname asc'\n * @return {object} jQuery promise resolved with courses.\n */\nconst getEnrolledCoursesByTimelineClassification = (classification, limit, offset, sort) => {\n const args = {\n classification: classification\n };\n\n if (typeof limit !== 'undefined') {\n args.limit = limit;\n }\n\n if (typeof offset !== 'undefined') {\n args.offset = offset;\n }\n\n if (typeof sort !== 'undefined') {\n args.sort = sort;\n }\n\n const request = {\n methodname: 'core_course_get_enrolled_courses_by_timeline_classification',\n args: args\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get a list of courses that the logged in user is enrolled in, where they have at least one action event,\n * for a given timeline classification.\n *\n * @param {string} classification past, inprogress, or future\n * @param {int} limit The maximum number of courses to return\n * @param {int} offset Skip this many results from the start of the result set\n * @param {string} sort Column to sort by and direction, e.g. 'shortname asc'\n * @param {string} searchValue Optional text search value\n * @param {int} eventsFrom Optional start timestamp (inclusive) that the course should have event(s) in\n * @param {int} eventsTo Optional end timestamp (inclusive) that the course should have event(s) in\n * @return {object} jQuery promise resolved with courses.\n */\n const getEnrolledCoursesWithEventsByTimelineClassification = (classification, limit = 0, offset = 0, sort = null,\n searchValue = null, eventsFrom = null, eventsTo = null) => {\n\n const args = {\n classification: classification,\n limit: limit,\n offset: offset,\n sort: sort,\n eventsfrom: eventsFrom,\n eventsto: eventsTo,\n searchvalue: searchValue,\n };\n\n const request = {\n methodname: 'core_course_get_enrolled_courses_with_action_events_by_timeline_classification',\n args: args\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get the list of courses that the user has most recently accessed.\n *\n * @method getLastAccessedCourses\n * @param {int} userid User from which the courses will be obtained\n * @param {int} limit Only return this many results\n * @param {int} offset Skip this many results from the start of the result set\n * @param {string} sort Column to sort by and direction, e.g. 'shortname asc'\n * @return {promise} Resolved with an array of courses\n */\nconst getLastAccessedCourses = (userid, limit, offset, sort) => {\n const args = {};\n\n if (typeof userid !== 'undefined') {\n args.userid = userid;\n }\n\n if (typeof limit !== 'undefined') {\n args.limit = limit;\n }\n\n if (typeof offset !== 'undefined') {\n args.offset = offset;\n }\n\n if (typeof sort !== 'undefined') {\n args.sort = sort;\n }\n\n const request = {\n methodname: 'core_course_get_recent_courses',\n args: args\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get the list of users enrolled in this cmid.\n *\n * @param {Number} cmid Course Module from which the users will be obtained\n * @param {Number} groupID Group ID from which the users will be obtained\n * @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course.\n * @returns {Promise} Promise containing a list of users\n */\nconst getEnrolledUsersFromCourseModuleID = (cmid, groupID, onlyActive = false) => {\n var request = {\n methodname: 'core_course_get_enrolled_users_by_cmid',\n args: {\n cmid: cmid,\n groupid: groupID,\n onlyactive: onlyActive,\n },\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Toggle the completion state of an activity with manual completion.\n *\n * @param {Number} cmid The course module ID.\n * @param {Boolean} completed Whether to set as complete or not.\n * @returns {object} jQuery promise\n */\nconst toggleManualCompletion = (cmid, completed) => {\n const request = {\n methodname: 'core_completion_update_activity_completion_status_manually',\n args: {\n cmid,\n completed,\n }\n };\n return Ajax.call([request])[0];\n};\n\nexport default {\n getEnrolledCoursesByTimelineClassification,\n getLastAccessedCourses,\n getUsersFromCourseModuleID: getEnrolledUsersFromCourseModuleID,\n toggleManualCompletion,\n getEnrolledCoursesWithEventsByTimelineClassification,\n};\n"],"names":["getEnrolledCoursesByTimelineClassification","classification","limit","offset","sort","args","request","methodname","Ajax","call","getLastAccessedCourses","userid","getUsersFromCourseModuleID","cmid","groupID","onlyActive","groupid","onlyactive","toggleManualCompletion","completed","getEnrolledCoursesWithEventsByTimelineClassification","searchValue","eventsFrom","eventsTo","eventsfrom","eventsto","searchvalue"],"mappings":";;;;;;;sJA0Ke,CACXA,2CAxI+C,CAACC,eAAgBC,MAAOC,OAAQC,cACzEC,KAAO,CACTJ,eAAgBA,qBAGC,IAAVC,QACPG,KAAKH,MAAQA,YAGK,IAAXC,SACPE,KAAKF,OAASA,aAGE,IAATC,OACPC,KAAKD,KAAOA,YAGVE,QAAU,CACZC,WAAY,8DACZF,KAAMA,aAGHG,cAAKC,KAAK,CAACH,UAAU,IAmH5BI,uBApE2B,CAACC,OAAQT,MAAOC,OAAQC,cAC7CC,KAAO,QAES,IAAXM,SACPN,KAAKM,OAASA,aAGG,IAAVT,QACPG,KAAKH,MAAQA,YAGK,IAAXC,SACPE,KAAKF,OAASA,aAGE,IAATC,OACPC,KAAKD,KAAOA,YAGVE,QAAU,CACZC,WAAY,iCACZF,KAAMA,aAGHG,cAAKC,KAAK,CAACH,UAAU,IA6C5BM,2BAlCuC,SAACC,KAAMC,aAASC,uEACnDT,QAAU,CACVC,WAAY,yCACZF,KAAM,CACFQ,KAAMA,KACNG,QAASF,QACTG,WAAYF,oBAIbP,cAAKC,KAAK,CAACH,UAAU,IAyB5BY,uBAf2B,CAACL,KAAMM,mBAC5Bb,QAAU,CACZC,WAAY,6DACZF,KAAM,CACFQ,KAAAA,KACAM,UAAAA,mBAGDX,cAAKC,KAAK,CAACH,UAAU,IAQ5Bc,qDAtG0D,SAACnB,oBAAgBC,6DAAQ,EAAGC,8DAAS,EAAGC,4DAAO,KACrGiB,mEAAc,KAAMC,kEAAa,KAAMC,gEAAW,WAEhDlB,KAAO,CACTJ,eAAgBA,eAChBC,MAAOA,MACPC,OAAQA,OACRC,KAAMA,KACNoB,WAAYF,WACZG,SAAUF,SACVG,YAAaL,aAGXf,QAAU,CACZC,WAAY,iFACZF,KAAMA,aAGHG,cAAKC,KAAK,CAACH,UAAU"} \ No newline at end of file diff --git a/course/amd/src/repository.js b/course/amd/src/repository.js index 7e08a725a60..96dfa488f91 100644 --- a/course/amd/src/repository.js +++ b/course/amd/src/repository.js @@ -134,14 +134,16 @@ const getLastAccessedCourses = (userid, limit, offset, sort) => { * * @param {Number} cmid Course Module from which the users will be obtained * @param {Number} groupID Group ID from which the users will be obtained + * @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course. * @returns {Promise} Promise containing a list of users */ -const getEnrolledUsersFromCourseModuleID = (cmid, groupID) => { +const getEnrolledUsersFromCourseModuleID = (cmid, groupID, onlyActive = false) => { var request = { methodname: 'core_course_get_enrolled_users_by_cmid', args: { cmid: cmid, groupid: groupID, + onlyactive: onlyActive, }, }; diff --git a/course/externallib.php b/course/externallib.php index 95a43cf5094..0397a8d9ed1 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -4151,6 +4151,8 @@ class core_course_external extends external_api { return new external_function_parameters([ 'cmid' => new external_value(PARAM_INT, 'id of the course module', VALUE_REQUIRED), 'groupid' => new external_value(PARAM_INT, 'id of the group', VALUE_DEFAULT, 0), + 'onlyactive' => new external_value(PARAM_BOOL, 'whether to return only active users or all.', + VALUE_DEFAULT, false), ]); } @@ -4159,26 +4161,25 @@ class core_course_external extends external_api { * * @param int $cmid Course Module id from which the users will be obtained * @param int $groupid Group id from which the users will be obtained + * @param bool $onlyactive Whether to return only the active enrolled users or all enrolled users in the course. * @return array List of users * @throws invalid_parameter_exception */ - public static function get_enrolled_users_by_cmid(int $cmid, int $groupid = 0) { + public static function get_enrolled_users_by_cmid(int $cmid, int $groupid = 0, bool $onlyactive = false) { global $PAGE; $warnings = []; - [ - 'cmid' => $cmid, - 'groupid' => $groupid, - ] = self::validate_parameters(self::get_enrolled_users_by_cmid_parameters(), [ + self::validate_parameters(self::get_enrolled_users_by_cmid_parameters(), [ 'cmid' => $cmid, 'groupid' => $groupid, + 'onlyactive' => $onlyactive, ]); list($course, $cm) = get_course_and_cm_from_cmid($cmid); $coursecontext = context_course::instance($course->id); self::validate_context($coursecontext); - $enrolledusers = get_enrolled_users($coursecontext, '', $groupid); + $enrolledusers = get_enrolled_users($coursecontext, '', $groupid, 'u.*', null, 0, 0, $onlyactive); $users = array_map(function ($user) use ($PAGE) { $user->fullname = fullname($user); diff --git a/course/tests/externallib_test.php b/course/tests/externallib_test.php index 4f97c8568bc..e547b781f43 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -3630,6 +3630,7 @@ class externallib_test extends externallib_advanced_testcase { $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); $user1picture = new user_picture($user1); $user1picture->size = 1; @@ -3639,6 +3640,10 @@ class externallib_test extends externallib_advanced_testcase { $user2picture->size = 1; $user2->profileimage = $user2picture->get_url($PAGE)->out(false); + $user3picture = new user_picture($user3); + $user3picture->size = 1; + $user3->profileimage = $user3picture->get_url($PAGE)->out(false); + // Set the first created user to the test user. self::setUser($user1); @@ -3653,6 +3658,8 @@ class externallib_test extends externallib_advanced_testcase { // Following lines enrol and assign default role id to the users. $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); + // Enrol a suspended user in the course. + $this->getDataGenerator()->enrol_user($user3->id, $course1->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED); // Create what we expect to be returned when querying the course module. $expectedusers = array( @@ -3674,11 +3681,42 @@ class externallib_test extends externallib_advanced_testcase { 'lastname' => $user2->lastname, 'profileimage' => $user2->profileimage, ]; + $expectedusers['users'][2] = [ + 'id' => $user3->id, + 'fullname' => fullname($user3), + 'firstname' => $user3->firstname, + 'lastname' => $user3->lastname, + 'profileimage' => $user3->profileimage, + ]; // Test getting the users in a given context. $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid); $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users); + $this->assertEquals(3, count($users['users'])); + $this->assertEquals($expectedusers, $users); + + // Test getting only the active users in a given context. + $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid, 0, true); + $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users); + + $expectedusers['users'] = [ + [ + 'id' => $user1->id, + 'fullname' => fullname($user1), + 'firstname' => $user1->firstname, + 'lastname' => $user1->lastname, + 'profileimage' => $user1->profileimage, + ], + [ + 'id' => $user2->id, + 'fullname' => fullname($user2), + 'firstname' => $user2->firstname, + 'lastname' => $user2->lastname, + 'profileimage' => $user2->profileimage, + ] + ]; + $this->assertEquals(2, count($users['users'])); $this->assertEquals($expectedusers, $users); } diff --git a/lib/db/services.php b/lib/db/services.php index b1a14ef8c0e..e2ff0df1a2e 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -679,7 +679,7 @@ $functions = array( 'classname' => 'core_course_external', 'methodname' => 'get_enrolled_users_by_cmid', 'classpath' => 'course/externallib.php', - 'description' => 'List users by course module id & filter by group id.', + 'description' => 'List users by course module id, filter by group and active enrolment status.', 'type' => 'read', 'ajax' => true, ), From ca77344eadc0e68b49255b65bfbd7bfe84445190 Mon Sep 17 00:00:00 2001 From: Mihail Geshoski Date: Fri, 30 Sep 2022 02:09:35 +0800 Subject: [PATCH 2/2] MDL-68437 forum: Support to show only active course users in grader The following change adds support to show only active course users in the forum grader and generally replicates the behaviour from the assignment grader which relies on certain config settings, user permissions and capabilities. --- mod/forum/amd/build/grades/grader.min.js | 2 +- mod/forum/amd/build/grades/grader.min.js.map | 2 +- mod/forum/amd/src/grades/grader.js | 8 +- mod/forum/classes/grades/forum_gradeitem.php | 17 ++++ .../local/renderers/discussion_list.php | 1 + .../templates/grades/grade_button.mustache | 1 + .../tests/grades_forum_gradeitem_test.php | 85 +++++++++++++++++++ mod/forum/view.php | 1 + 8 files changed, 112 insertions(+), 5 deletions(-) diff --git a/mod/forum/amd/build/grades/grader.min.js b/mod/forum/amd/build/grades/grader.min.js index 6de71f2efa8..2e0c3cf6510 100644 --- a/mod/forum/amd/build/grades/grader.min.js +++ b/mod/forum/amd/build/grades/grader.min.js @@ -5,6 +5,6 @@ define("mod_forum/grades/grader",["exports","./grader/selectors","mod_forum/repo * @module mod_forum/grades/grader * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.registerLaunchListeners=void 0,Selectors=_interopRequireWildcard(Selectors),_repository=_interopRequireDefault(_repository),_templates=_interopRequireDefault(_templates),Grader=_interopRequireWildcard(Grader),_notification=_interopRequireDefault(_notification),_repository2=_interopRequireDefault(_repository2);const templateNames_contentRegion="mod_forum/grades/grader/discussion/posts",getContentForUserIdFunction=(cmid,experimentalDisplayMode)=>userid=>_repository.default.getDiscussionByUserID(userid,cmid).then((context=>(context.discussions=context.discussions.map(discussionPostMapper),context.experimentaldisplaymode=!!experimentalDisplayMode,_templates.default.render(templateNames_contentRegion,context)))).catch(_notification.default.exception),getUsersForCmidFunction=(cmid,groupID)=>async()=>(await _repository2.default.getUsersFromCourseModuleID(cmid,groupID)).users,findGradableNode=node=>node.closest(Selectors.gradableItem),discussionPostMapper=discussion=>{const parentMap=new Map;discussion.posts.parentposts.forEach((post=>parentMap.set(post.id,post)));const userPosts=discussion.posts.userposts.map((post=>{post.readonly=!0,post.hasreplies=!1,post.replies=[];const parent=post.parentid?parentMap.get(post.parentid):null;return parent&&(parent.hasreplies=!1,parent.replies=[],parent.readonly=!0,post.parentauthorname=parent.author.fullname),{parent:parent,post:post}}));return{...discussion,posts:userPosts}};_exports.registerLaunchListeners=()=>{document.addEventListener("click",(async e=>{if(e.target.matches(Selectors.launch)){const rootNode=findGradableNode(e.target);if(!rootNode)throw Error("Unable to find a gradable item");if(!rootNode.matches(Selectors.gradableItems.wholeForum))throw Error("Unable to find a valid gradable item");e.preventDefault();try{await async function(rootNode){let{focusOnClose:focusOnClose=null}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const data=rootNode.dataset,gradingPanelFunctions=await Grader.getGradingPanelFunctions("mod_forum",data.contextid,data.gradingComponent,data.gradingComponentSubtype,data.gradableItemtype),groupID=data.group?data.group:0;await Grader.launch(getUsersForCmidFunction(data.cmid,groupID),getContentForUserIdFunction(data.cmid,"1"==data.experimentalDisplayMode),gradingPanelFunctions.getter,gradingPanelFunctions.setter,{groupid:data.groupid,initialUserId:data.initialuserid,moduleName:data.name,courseName:data.courseName,courseUrl:(0,_url.relativeUrl)("/course/view.php",{id:data.courseId}),sendStudentNotifications:data.sendStudentNotifications,focusOnClose:focusOnClose})}(rootNode,{focusOnClose:e.target})}catch(error){_notification.default.exception(error)}}if(e.target.matches(Selectors.viewGrade)){e.preventDefault();const rootNode=findGradableNode(e.target);if(!rootNode)throw Error("Unable to find a gradable item");if(!rootNode.matches(Selectors.gradableItems.wholeForum))throw Error("Unable to find a valid gradable item");e.preventDefault();try{await async function(rootNode){let{focusOnClose:focusOnClose=null}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const data=rootNode.dataset,gradingPanelFunctions=await Grader.getGradingPanelFunctions("mod_forum",data.contextid,data.gradingComponent,data.gradingComponentSubtype,data.gradableItemtype);await Grader.view(gradingPanelFunctions.getter,data.userid,data.name,{focusOnClose:focusOnClose})}(rootNode,{focusOnClose:e.target})}catch(error){_notification.default.exception(error)}}}))}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.registerLaunchListeners=void 0,Selectors=_interopRequireWildcard(Selectors),_repository=_interopRequireDefault(_repository),_templates=_interopRequireDefault(_templates),Grader=_interopRequireWildcard(Grader),_notification=_interopRequireDefault(_notification),_repository2=_interopRequireDefault(_repository2);const templateNames_contentRegion="mod_forum/grades/grader/discussion/posts",getContentForUserIdFunction=(cmid,experimentalDisplayMode)=>userid=>_repository.default.getDiscussionByUserID(userid,cmid).then((context=>(context.discussions=context.discussions.map(discussionPostMapper),context.experimentaldisplaymode=!!experimentalDisplayMode,_templates.default.render(templateNames_contentRegion,context)))).catch(_notification.default.exception),getUsersForCmidFunction=(cmid,groupID,onlyActive)=>async()=>(await _repository2.default.getUsersFromCourseModuleID(cmid,groupID,onlyActive)).users,findGradableNode=node=>node.closest(Selectors.gradableItem),discussionPostMapper=discussion=>{const parentMap=new Map;discussion.posts.parentposts.forEach((post=>parentMap.set(post.id,post)));const userPosts=discussion.posts.userposts.map((post=>{post.readonly=!0,post.hasreplies=!1,post.replies=[];const parent=post.parentid?parentMap.get(post.parentid):null;return parent&&(parent.hasreplies=!1,parent.replies=[],parent.readonly=!0,post.parentauthorname=parent.author.fullname),{parent:parent,post:post}}));return{...discussion,posts:userPosts}};_exports.registerLaunchListeners=()=>{document.addEventListener("click",(async e=>{if(e.target.matches(Selectors.launch)){const rootNode=findGradableNode(e.target);if(!rootNode)throw Error("Unable to find a gradable item");if(!rootNode.matches(Selectors.gradableItems.wholeForum))throw Error("Unable to find a valid gradable item");e.preventDefault();try{await async function(rootNode){let{focusOnClose:focusOnClose=null}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const data=rootNode.dataset,gradingPanelFunctions=await Grader.getGradingPanelFunctions("mod_forum",data.contextid,data.gradingComponent,data.gradingComponentSubtype,data.gradableItemtype),groupID=data.group?data.group:0,onlyActive=data.gradeOnlyActiveUsers;await Grader.launch(getUsersForCmidFunction(data.cmid,groupID,onlyActive),getContentForUserIdFunction(data.cmid,"1"==data.experimentalDisplayMode),gradingPanelFunctions.getter,gradingPanelFunctions.setter,{groupid:data.groupid,initialUserId:data.initialuserid,moduleName:data.name,courseName:data.courseName,courseUrl:(0,_url.relativeUrl)("/course/view.php",{id:data.courseId}),sendStudentNotifications:data.sendStudentNotifications,focusOnClose:focusOnClose})}(rootNode,{focusOnClose:e.target})}catch(error){_notification.default.exception(error)}}if(e.target.matches(Selectors.viewGrade)){e.preventDefault();const rootNode=findGradableNode(e.target);if(!rootNode)throw Error("Unable to find a gradable item");if(!rootNode.matches(Selectors.gradableItems.wholeForum))throw Error("Unable to find a valid gradable item");e.preventDefault();try{await async function(rootNode){let{focusOnClose:focusOnClose=null}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const data=rootNode.dataset,gradingPanelFunctions=await Grader.getGradingPanelFunctions("mod_forum",data.contextid,data.gradingComponent,data.gradingComponentSubtype,data.gradableItemtype);await Grader.view(gradingPanelFunctions.getter,data.userid,data.name,{focusOnClose:focusOnClose})}(rootNode,{focusOnClose:e.target})}catch(error){_notification.default.exception(error)}}}))}})); //# sourceMappingURL=grader.min.js.map \ No newline at end of file diff --git a/mod/forum/amd/build/grades/grader.min.js.map b/mod/forum/amd/build/grades/grader.min.js.map index 61aab8f47c0..b2e5cda5e3a 100644 --- a/mod/forum/amd/build/grades/grader.min.js.map +++ b/mod/forum/amd/build/grades/grader.min.js.map @@ -1 +1 @@ -{"version":3,"file":"grader.min.js","sources":["../../src/grades/grader.js"],"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 * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/grades/grader\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as Selectors from './grader/selectors';\nimport Repository from 'mod_forum/repository';\nimport Templates from 'core/templates';\nimport * as Grader from '../local/grades/grader';\nimport Notification from 'core/notification';\nimport CourseRepository from 'core_course/repository';\nimport {relativeUrl} from 'core/url';\n\nconst templateNames = {\n contentRegion: 'mod_forum/grades/grader/discussion/posts',\n};\n\n/**\n * Curried function with CMID set, this is then used in unified grader as a fetch a users content.\n *\n * @param {Number} cmid\n * @param {Bool} experimentalDisplayMode\n * @return {Function}\n */\nconst getContentForUserIdFunction = (cmid, experimentalDisplayMode) => (userid) => {\n /**\n * Given the parent function is called with the second param set execute the partially executed function.\n *\n * @param {Number} userid\n */\n return Repository.getDiscussionByUserID(userid, cmid)\n .then(context => {\n // Rebuild the returned data for the template.\n context.discussions = context.discussions.map(discussionPostMapper);\n context.experimentaldisplaymode = experimentalDisplayMode ? true : false;\n\n return Templates.render(templateNames.contentRegion, context);\n })\n .catch(Notification.exception);\n};\n\n/**\n * Curried function with CMID set, this is then used in unified grader as a fetch users call.\n * The function curried fetches all users in a course for a given CMID.\n *\n * @param {Number} cmid\n * @param {Number} groupID\n * @return {Array} Array of users for a given context.\n */\nconst getUsersForCmidFunction = (cmid, groupID) => async() => {\n const context = await CourseRepository.getUsersFromCourseModuleID(cmid, groupID);\n\n return context.users;\n};\n\n\nconst findGradableNode = node => node.closest(Selectors.gradableItem);\n\n/**\n * For a discussion we need to manipulate it's posts to hide certain UI elements.\n *\n * @param {Object} discussion\n * @return {Array} name, id, posts\n */\nconst discussionPostMapper = (discussion) => {\n // Map postid => post.\n const parentMap = new Map();\n discussion.posts.parentposts.forEach(post => parentMap.set(post.id, post));\n const userPosts = discussion.posts.userposts.map(post => {\n post.readonly = true;\n post.hasreplies = false;\n post.replies = [];\n\n const parent = post.parentid ? parentMap.get(post.parentid) : null;\n if (parent) {\n parent.hasreplies = false;\n parent.replies = [];\n parent.readonly = true;\n post.parentauthorname = parent.author.fullname;\n }\n\n return {\n parent,\n post\n };\n });\n\n return {\n ...discussion,\n posts: userPosts,\n };\n};\n\n/**\n * Launch the Grader.\n *\n * @param {HTMLElement} rootNode the root HTML element describing what is to be graded\n * @param {object} param\n * @param {bool} [param.focusOnClose=null]\n */\nconst launchWholeForumGrading = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const data = rootNode.dataset;\n const gradingPanelFunctions = await Grader.getGradingPanelFunctions(\n 'mod_forum',\n data.contextid,\n data.gradingComponent,\n data.gradingComponentSubtype,\n data.gradableItemtype\n );\n\n const groupID = data.group ? data.group : 0;\n\n await Grader.launch(\n getUsersForCmidFunction(data.cmid, groupID),\n getContentForUserIdFunction(data.cmid, data.experimentalDisplayMode == \"1\"),\n gradingPanelFunctions.getter,\n gradingPanelFunctions.setter,\n {\n groupid: data.groupid,\n initialUserId: data.initialuserid,\n moduleName: data.name,\n courseName: data.courseName,\n courseUrl: relativeUrl('/course/view.php', {id: data.courseId}),\n sendStudentNotifications: data.sendStudentNotifications,\n focusOnClose,\n }\n );\n};\n\n/**\n * Launch the Grader.\n *\n * @param {HTMLElement} rootNode the root HTML element describing what is to be graded\n * @param {object} param\n * @param {bool} [param.focusOnClose=null]\n */\nconst launchViewGrading = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const data = rootNode.dataset;\n const gradingPanelFunctions = await Grader.getGradingPanelFunctions(\n 'mod_forum',\n data.contextid,\n data.gradingComponent,\n data.gradingComponentSubtype,\n data.gradableItemtype\n );\n\n await Grader.view(\n gradingPanelFunctions.getter,\n data.userid,\n data.name,\n {\n focusOnClose,\n }\n );\n};\n\n/**\n * Register listeners to launch the grading panel.\n */\nexport const registerLaunchListeners = () => {\n document.addEventListener('click', async(e) => {\n if (e.target.matches(Selectors.launch)) {\n const rootNode = findGradableNode(e.target);\n\n if (!rootNode) {\n throw Error('Unable to find a gradable item');\n }\n\n if (rootNode.matches(Selectors.gradableItems.wholeForum)) {\n // Note: The preventDefault must be before any async function calls because the function becomes async\n // at that point and the default action is implemented.\n e.preventDefault();\n try {\n await launchWholeForumGrading(rootNode, {\n focusOnClose: e.target,\n });\n } catch (error) {\n Notification.exception(error);\n }\n } else {\n throw Error('Unable to find a valid gradable item');\n }\n }\n if (e.target.matches(Selectors.viewGrade)) {\n e.preventDefault();\n const rootNode = findGradableNode(e.target);\n\n if (!rootNode) {\n throw Error('Unable to find a gradable item');\n }\n\n if (rootNode.matches(Selectors.gradableItems.wholeForum)) {\n // Note: The preventDefault must be before any async function calls because the function becomes async\n // at that point and the default action is implemented.\n e.preventDefault();\n try {\n await launchViewGrading(rootNode, {\n focusOnClose: e.target,\n });\n } catch (error) {\n Notification.exception(error);\n }\n } else {\n throw Error('Unable to find a valid gradable item');\n }\n }\n });\n};\n"],"names":["templateNames","getContentForUserIdFunction","cmid","experimentalDisplayMode","userid","Repository","getDiscussionByUserID","then","context","discussions","map","discussionPostMapper","experimentaldisplaymode","Templates","render","catch","Notification","exception","getUsersForCmidFunction","groupID","async","CourseRepository","getUsersFromCourseModuleID","users","findGradableNode","node","closest","Selectors","gradableItem","discussion","parentMap","Map","posts","parentposts","forEach","post","set","id","userPosts","userposts","readonly","hasreplies","replies","parent","parentid","get","parentauthorname","author","fullname","document","addEventListener","e","target","matches","launch","rootNode","Error","gradableItems","wholeForum","preventDefault","focusOnClose","data","dataset","gradingPanelFunctions","Grader","getGradingPanelFunctions","contextid","gradingComponent","gradingComponentSubtype","gradableItemtype","group","getter","setter","groupid","initialUserId","initialuserid","moduleName","name","courseName","courseUrl","courseId","sendStudentNotifications","launchWholeForumGrading","error","viewGrade","view","launchViewGrading"],"mappings":";;;;;;;mYA8BMA,4BACa,2CAUbC,4BAA8B,CAACC,KAAMC,0BAA6BC,QAM7DC,oBAAWC,sBAAsBF,OAAQF,MAC3CK,MAAKC,UAEFA,QAAQC,YAAcD,QAAQC,YAAYC,IAAIC,sBAC9CH,QAAQI,0BAA0BT,wBAE3BU,mBAAUC,OAAOd,4BAA6BQ,YAExDO,MAAMC,sBAAaC,WAWtBC,wBAA0B,CAAChB,KAAMiB,UAAYC,gBACzBC,qBAAiBC,2BAA2BpB,KAAMiB,UAEzDI,MAIbC,iBAAmBC,MAAQA,KAAKC,QAAQC,UAAUC,cAQlDjB,qBAAwBkB,mBAEpBC,UAAY,IAAIC,IACtBF,WAAWG,MAAMC,YAAYC,SAAQC,MAAQL,UAAUM,IAAID,KAAKE,GAAIF,cAC9DG,UAAYT,WAAWG,MAAMO,UAAU7B,KAAIyB,OAC7CA,KAAKK,UAAW,EAChBL,KAAKM,YAAa,EAClBN,KAAKO,QAAU,SAETC,OAASR,KAAKS,SAAWd,UAAUe,IAAIV,KAAKS,UAAY,YAC1DD,SACAA,OAAOF,YAAa,EACpBE,OAAOD,QAAU,GACjBC,OAAOH,UAAW,EAClBL,KAAKW,iBAAmBH,OAAOI,OAAOC,UAGnC,CACHL,OAAAA,OACAR,KAAAA,eAID,IACAN,WACHG,MAAOM,6CA0EwB,KACnCW,SAASC,iBAAiB,SAAS9B,MAAAA,OAC3B+B,EAAEC,OAAOC,QAAQ1B,UAAU2B,QAAS,OAC9BC,SAAW/B,iBAAiB2B,EAAEC,YAE/BG,eACKC,MAAM,sCAGZD,SAASF,QAAQ1B,UAAU8B,cAAcC,kBAYnCF,MAAM,wCATZL,EAAEQ,2BA3EcvC,eAAMmC,cAAUK,aAC5CA,aAAe,6DACf,SACMC,KAAON,SAASO,QAChBC,4BAA8BC,OAAOC,yBACvC,YACAJ,KAAKK,UACLL,KAAKM,iBACLN,KAAKO,wBACLP,KAAKQ,kBAGHlD,QAAU0C,KAAKS,MAAQT,KAAKS,MAAQ,QAEpCN,OAAOV,OACTpC,wBAAwB2C,KAAK3D,KAAMiB,SACnClB,4BAA4B4D,KAAK3D,KAAsC,KAAhC2D,KAAK1D,yBAC5C4D,sBAAsBQ,OACtBR,sBAAsBS,OACtB,CACIC,QAASZ,KAAKY,QACdC,cAAeb,KAAKc,cACpBC,WAAYf,KAAKgB,KACjBC,WAAYjB,KAAKiB,WACjBC,WAAW,oBAAY,mBAAoB,CAAC1C,GAAIwB,KAAKmB,WACrDC,yBAA0BpB,KAAKoB,yBAC/BrB,aAAAA,eAmDcsB,CAAwB3B,SAAU,CACpCK,aAAcT,EAAEC,SAEtB,MAAO+B,6BACQlE,UAAUkE,WAM/BhC,EAAEC,OAAOC,QAAQ1B,UAAUyD,WAAY,CACvCjC,EAAEQ,uBACIJ,SAAW/B,iBAAiB2B,EAAEC,YAE/BG,eACKC,MAAM,sCAGZD,SAASF,QAAQ1B,UAAU8B,cAAcC,kBAYnCF,MAAM,wCATZL,EAAEQ,2BA5DQvC,eAAMmC,cAAUK,aACtCA,aAAe,6DACf,SACMC,KAAON,SAASO,QAChBC,4BAA8BC,OAAOC,yBACvC,YACAJ,KAAKK,UACLL,KAAKM,iBACLN,KAAKO,wBACLP,KAAKQ,wBAGHL,OAAOqB,KACTtB,sBAAsBQ,OACtBV,KAAKzD,OACLyD,KAAKgB,KACL,CACIjB,aAAAA,eA6Cc0B,CAAkB/B,SAAU,CAC9BK,aAAcT,EAAEC,SAEtB,MAAO+B,6BACQlE,UAAUkE"} \ No newline at end of file +{"version":3,"file":"grader.min.js","sources":["../../src/grades/grader.js"],"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 * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/grades/grader\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as Selectors from './grader/selectors';\nimport Repository from 'mod_forum/repository';\nimport Templates from 'core/templates';\nimport * as Grader from '../local/grades/grader';\nimport Notification from 'core/notification';\nimport CourseRepository from 'core_course/repository';\nimport {relativeUrl} from 'core/url';\n\nconst templateNames = {\n contentRegion: 'mod_forum/grades/grader/discussion/posts',\n};\n\n/**\n * Curried function with CMID set, this is then used in unified grader as a fetch a users content.\n *\n * @param {Number} cmid\n * @param {Bool} experimentalDisplayMode\n * @return {Function}\n */\nconst getContentForUserIdFunction = (cmid, experimentalDisplayMode) => (userid) => {\n /**\n * Given the parent function is called with the second param set execute the partially executed function.\n *\n * @param {Number} userid\n */\n return Repository.getDiscussionByUserID(userid, cmid)\n .then(context => {\n // Rebuild the returned data for the template.\n context.discussions = context.discussions.map(discussionPostMapper);\n context.experimentaldisplaymode = experimentalDisplayMode ? true : false;\n\n return Templates.render(templateNames.contentRegion, context);\n })\n .catch(Notification.exception);\n};\n\n/**\n * Curried function with CMID set, this is then used in unified grader as a fetch users call.\n * The function curried fetches all users in a course for a given CMID.\n *\n * @param {Number} cmid\n * @param {Number} groupID\n * @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course.\n * @return {Array} Array of users for a given context.\n */\nconst getUsersForCmidFunction = (cmid, groupID, onlyActive) => async() => {\n const context = await CourseRepository.getUsersFromCourseModuleID(cmid, groupID, onlyActive);\n\n return context.users;\n};\n\n\nconst findGradableNode = node => node.closest(Selectors.gradableItem);\n\n/**\n * For a discussion we need to manipulate it's posts to hide certain UI elements.\n *\n * @param {Object} discussion\n * @return {Array} name, id, posts\n */\nconst discussionPostMapper = (discussion) => {\n // Map postid => post.\n const parentMap = new Map();\n discussion.posts.parentposts.forEach(post => parentMap.set(post.id, post));\n const userPosts = discussion.posts.userposts.map(post => {\n post.readonly = true;\n post.hasreplies = false;\n post.replies = [];\n\n const parent = post.parentid ? parentMap.get(post.parentid) : null;\n if (parent) {\n parent.hasreplies = false;\n parent.replies = [];\n parent.readonly = true;\n post.parentauthorname = parent.author.fullname;\n }\n\n return {\n parent,\n post\n };\n });\n\n return {\n ...discussion,\n posts: userPosts,\n };\n};\n\n/**\n * Launch the Grader.\n *\n * @param {HTMLElement} rootNode the root HTML element describing what is to be graded\n * @param {object} param\n * @param {bool} [param.focusOnClose=null]\n */\nconst launchWholeForumGrading = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const data = rootNode.dataset;\n const gradingPanelFunctions = await Grader.getGradingPanelFunctions(\n 'mod_forum',\n data.contextid,\n data.gradingComponent,\n data.gradingComponentSubtype,\n data.gradableItemtype\n );\n\n const groupID = data.group ? data.group : 0;\n const onlyActive = data.gradeOnlyActiveUsers;\n\n await Grader.launch(\n getUsersForCmidFunction(data.cmid, groupID, onlyActive),\n getContentForUserIdFunction(data.cmid, data.experimentalDisplayMode == \"1\"),\n gradingPanelFunctions.getter,\n gradingPanelFunctions.setter,\n {\n groupid: data.groupid,\n initialUserId: data.initialuserid,\n moduleName: data.name,\n courseName: data.courseName,\n courseUrl: relativeUrl('/course/view.php', {id: data.courseId}),\n sendStudentNotifications: data.sendStudentNotifications,\n focusOnClose,\n }\n );\n};\n\n/**\n * Launch the Grader.\n *\n * @param {HTMLElement} rootNode the root HTML element describing what is to be graded\n * @param {object} param\n * @param {bool} [param.focusOnClose=null]\n */\nconst launchViewGrading = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const data = rootNode.dataset;\n const gradingPanelFunctions = await Grader.getGradingPanelFunctions(\n 'mod_forum',\n data.contextid,\n data.gradingComponent,\n data.gradingComponentSubtype,\n data.gradableItemtype\n );\n\n await Grader.view(\n gradingPanelFunctions.getter,\n data.userid,\n data.name,\n {\n focusOnClose,\n }\n );\n};\n\n/**\n * Register listeners to launch the grading panel.\n */\nexport const registerLaunchListeners = () => {\n document.addEventListener('click', async(e) => {\n if (e.target.matches(Selectors.launch)) {\n const rootNode = findGradableNode(e.target);\n\n if (!rootNode) {\n throw Error('Unable to find a gradable item');\n }\n\n if (rootNode.matches(Selectors.gradableItems.wholeForum)) {\n // Note: The preventDefault must be before any async function calls because the function becomes async\n // at that point and the default action is implemented.\n e.preventDefault();\n try {\n await launchWholeForumGrading(rootNode, {\n focusOnClose: e.target,\n });\n } catch (error) {\n Notification.exception(error);\n }\n } else {\n throw Error('Unable to find a valid gradable item');\n }\n }\n if (e.target.matches(Selectors.viewGrade)) {\n e.preventDefault();\n const rootNode = findGradableNode(e.target);\n\n if (!rootNode) {\n throw Error('Unable to find a gradable item');\n }\n\n if (rootNode.matches(Selectors.gradableItems.wholeForum)) {\n // Note: The preventDefault must be before any async function calls because the function becomes async\n // at that point and the default action is implemented.\n e.preventDefault();\n try {\n await launchViewGrading(rootNode, {\n focusOnClose: e.target,\n });\n } catch (error) {\n Notification.exception(error);\n }\n } else {\n throw Error('Unable to find a valid gradable item');\n }\n }\n });\n};\n"],"names":["templateNames","getContentForUserIdFunction","cmid","experimentalDisplayMode","userid","Repository","getDiscussionByUserID","then","context","discussions","map","discussionPostMapper","experimentaldisplaymode","Templates","render","catch","Notification","exception","getUsersForCmidFunction","groupID","onlyActive","async","CourseRepository","getUsersFromCourseModuleID","users","findGradableNode","node","closest","Selectors","gradableItem","discussion","parentMap","Map","posts","parentposts","forEach","post","set","id","userPosts","userposts","readonly","hasreplies","replies","parent","parentid","get","parentauthorname","author","fullname","document","addEventListener","e","target","matches","launch","rootNode","Error","gradableItems","wholeForum","preventDefault","focusOnClose","data","dataset","gradingPanelFunctions","Grader","getGradingPanelFunctions","contextid","gradingComponent","gradingComponentSubtype","gradableItemtype","group","gradeOnlyActiveUsers","getter","setter","groupid","initialUserId","initialuserid","moduleName","name","courseName","courseUrl","courseId","sendStudentNotifications","launchWholeForumGrading","error","viewGrade","view","launchViewGrading"],"mappings":";;;;;;;mYA8BMA,4BACa,2CAUbC,4BAA8B,CAACC,KAAMC,0BAA6BC,QAM7DC,oBAAWC,sBAAsBF,OAAQF,MAC3CK,MAAKC,UAEFA,QAAQC,YAAcD,QAAQC,YAAYC,IAAIC,sBAC9CH,QAAQI,0BAA0BT,wBAE3BU,mBAAUC,OAAOd,4BAA6BQ,YAExDO,MAAMC,sBAAaC,WAYtBC,wBAA0B,CAAChB,KAAMiB,QAASC,aAAeC,gBACrCC,qBAAiBC,2BAA2BrB,KAAMiB,QAASC,aAElEI,MAIbC,iBAAmBC,MAAQA,KAAKC,QAAQC,UAAUC,cAQlDlB,qBAAwBmB,mBAEpBC,UAAY,IAAIC,IACtBF,WAAWG,MAAMC,YAAYC,SAAQC,MAAQL,UAAUM,IAAID,KAAKE,GAAIF,cAC9DG,UAAYT,WAAWG,MAAMO,UAAU9B,KAAI0B,OAC7CA,KAAKK,UAAW,EAChBL,KAAKM,YAAa,EAClBN,KAAKO,QAAU,SAETC,OAASR,KAAKS,SAAWd,UAAUe,IAAIV,KAAKS,UAAY,YAC1DD,SACAA,OAAOF,YAAa,EACpBE,OAAOD,QAAU,GACjBC,OAAOH,UAAW,EAClBL,KAAKW,iBAAmBH,OAAOI,OAAOC,UAGnC,CACHL,OAAAA,OACAR,KAAAA,eAID,IACAN,WACHG,MAAOM,6CA2EwB,KACnCW,SAASC,iBAAiB,SAAS9B,MAAAA,OAC3B+B,EAAEC,OAAOC,QAAQ1B,UAAU2B,QAAS,OAC9BC,SAAW/B,iBAAiB2B,EAAEC,YAE/BG,eACKC,MAAM,sCAGZD,SAASF,QAAQ1B,UAAU8B,cAAcC,kBAYnCF,MAAM,wCATZL,EAAEQ,2BA5EcvC,eAAMmC,cAAUK,aAC5CA,aAAe,6DACf,SACMC,KAAON,SAASO,QAChBC,4BAA8BC,OAAOC,yBACvC,YACAJ,KAAKK,UACLL,KAAKM,iBACLN,KAAKO,wBACLP,KAAKQ,kBAGHnD,QAAU2C,KAAKS,MAAQT,KAAKS,MAAQ,EACpCnD,WAAa0C,KAAKU,2BAElBP,OAAOV,OACTrC,wBAAwB4C,KAAK5D,KAAMiB,QAASC,YAC5CnB,4BAA4B6D,KAAK5D,KAAsC,KAAhC4D,KAAK3D,yBAC5C6D,sBAAsBS,OACtBT,sBAAsBU,OACtB,CACIC,QAASb,KAAKa,QACdC,cAAed,KAAKe,cACpBC,WAAYhB,KAAKiB,KACjBC,WAAYlB,KAAKkB,WACjBC,WAAW,oBAAY,mBAAoB,CAAC3C,GAAIwB,KAAKoB,WACrDC,yBAA0BrB,KAAKqB,yBAC/BtB,aAAAA,eAmDcuB,CAAwB5B,SAAU,CACpCK,aAAcT,EAAEC,SAEtB,MAAOgC,6BACQpE,UAAUoE,WAM/BjC,EAAEC,OAAOC,QAAQ1B,UAAU0D,WAAY,CACvClC,EAAEQ,uBACIJ,SAAW/B,iBAAiB2B,EAAEC,YAE/BG,eACKC,MAAM,sCAGZD,SAASF,QAAQ1B,UAAU8B,cAAcC,kBAYnCF,MAAM,wCATZL,EAAEQ,2BA5DQvC,eAAMmC,cAAUK,aACtCA,aAAe,6DACf,SACMC,KAAON,SAASO,QAChBC,4BAA8BC,OAAOC,yBACvC,YACAJ,KAAKK,UACLL,KAAKM,iBACLN,KAAKO,wBACLP,KAAKQ,wBAGHL,OAAOsB,KACTvB,sBAAsBS,OACtBX,KAAK1D,OACL0D,KAAKiB,KACL,CACIlB,aAAAA,eA6Cc2B,CAAkBhC,SAAU,CAC9BK,aAAcT,EAAEC,SAEtB,MAAOgC,6BACQpE,UAAUoE"} \ No newline at end of file diff --git a/mod/forum/amd/src/grades/grader.js b/mod/forum/amd/src/grades/grader.js index 71c146d0650..5be32cf3f74 100644 --- a/mod/forum/amd/src/grades/grader.js +++ b/mod/forum/amd/src/grades/grader.js @@ -62,10 +62,11 @@ const getContentForUserIdFunction = (cmid, experimentalDisplayMode) => (userid) * * @param {Number} cmid * @param {Number} groupID + * @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course. * @return {Array} Array of users for a given context. */ -const getUsersForCmidFunction = (cmid, groupID) => async() => { - const context = await CourseRepository.getUsersFromCourseModuleID(cmid, groupID); +const getUsersForCmidFunction = (cmid, groupID, onlyActive) => async() => { + const context = await CourseRepository.getUsersFromCourseModuleID(cmid, groupID, onlyActive); return context.users; }; @@ -128,9 +129,10 @@ const launchWholeForumGrading = async(rootNode, { ); const groupID = data.group ? data.group : 0; + const onlyActive = data.gradeOnlyActiveUsers; await Grader.launch( - getUsersForCmidFunction(data.cmid, groupID), + getUsersForCmidFunction(data.cmid, groupID, onlyActive), getContentForUserIdFunction(data.cmid, data.experimentalDisplayMode == "1"), gradingPanelFunctions.getter, gradingPanelFunctions.setter, diff --git a/mod/forum/classes/grades/forum_gradeitem.php b/mod/forum/classes/grades/forum_gradeitem.php index 0094e578cf3..5760cb61b0e 100644 --- a/mod/forum/classes/grades/forum_gradeitem.php +++ b/mod/forum/classes/grades/forum_gradeitem.php @@ -236,6 +236,23 @@ class forum_gradeitem extends component_gradeitem { return (int) $this->forum->get_id(); } + /** + * Defines whether only active users in the course should be gradeable. + * + * @return bool Whether only active users in the course should be gradeable. + */ + public function should_grade_only_active_users(): bool { + global $CFG; + + $showonlyactiveenrolconfig = !empty($CFG->grade_report_showonlyactiveenrol); + // Grade only active users enrolled in the course either when the 'grade_report_showonlyactiveenrol' user + // preference is set to true or the current user does not have the capability to view suspended users in the + // course. In cases where the 'grade_report_showonlyactiveenrol' user preference is not set we are falling back + // to the set value for the 'grade_report_showonlyactiveenrol' config. + return get_user_preferences('grade_report_showonlyactiveenrol', $showonlyactiveenrolconfig) || + !has_capability('moodle/course:viewsuspendedusers', \context_course::instance($this->forum->get_course_id())); + } + /** * Create or update the grade. * diff --git a/mod/forum/classes/local/renderers/discussion_list.php b/mod/forum/classes/local/renderers/discussion_list.php index 6c57ab736b1..d2664393faa 100644 --- a/mod/forum/classes/local/renderers/discussion_list.php +++ b/mod/forum/classes/local/renderers/discussion_list.php @@ -197,6 +197,7 @@ class discussion_list { 'gradingcomponent' => $this->forumgradeitem->get_grading_component_name(), 'gradingcomponentsubtype' => $this->forumgradeitem->get_grading_component_subtype(), 'sendstudentnotifications' => $forum->should_notify_students_default_when_grade_for_forum(), + 'gradeonlyactiveusers' => $this->forumgradeitem->should_grade_only_active_users(), 'hasanyactions' => $hasanyactions, 'groupchangemenu' => groups_print_activity_menu( $cm, diff --git a/mod/forum/templates/grades/grade_button.mustache b/mod/forum/templates/grades/grade_button.mustache index e84e9a0fb82..ba51b3587b3 100644 --- a/mod/forum/templates/grades/grade_button.mustache +++ b/mod/forum/templates/grades/grade_button.mustache @@ -43,6 +43,7 @@ data-grading-component="{{gradingcomponent}}" data-grading-component-subtype="{{gradingcomponentsubtype}}" data-gradable-itemtype="forum" + data-grade-only-active-users="{{gradeonlyactiveusers}}" {{#sendstudentnotifications}}data-send-student-notifications="1" {{/sendstudentnotifications}} > {{#str}}gradeusers, forum{{/str}} diff --git a/mod/forum/tests/grades_forum_gradeitem_test.php b/mod/forum/tests/grades_forum_gradeitem_test.php index a7da3930c25..2e820fa4f7b 100644 --- a/mod/forum/tests/grades_forum_gradeitem_test.php +++ b/mod/forum/tests/grades_forum_gradeitem_test.php @@ -169,6 +169,91 @@ class grades_forum_gradeitem_test extends \advanced_testcase { ]); } + /** + * Test the logic in the should_grade_only_active_users() method. + * + * @covers ::should_grade_only_active_users + * @dataProvider should_grade_only_active_users_provider + * @param bool $showonlyactiveenrolconfig Whether the 'grade_report_showonlyactiveenrol' is enabled or not. + * @param bool $showonlyactiveenrolpreference Whether the 'grade_report_showonlyactiveenrol' preference is enabled or not. + * @param bool $viewsuspendeduserscapability Whether the 'moodle/course:viewsuspendedusers' capability is allowed or not. + * @param bool $expected The expected result. + */ + public function test_should_grade_only_active_users(bool $showonlyactiveenrolconfig, bool $showonlyactiveenrolpreference, + bool $viewsuspendeduserscapability, bool $expected): void { + global $DB; + + $this->resetAfterTest(); + + $forum = $this->get_forum_instance([ + 'grade_forum' => 100, + ]); + $course = $forum->get_course_record(); + $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); + + $user = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user->id, $course->id, 'editingteacher'); + $editingteacherroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); + + // Set the 'grade_report_showonlyactiveenrol' config setting. + set_config('grade_report_showonlyactiveenrol', $showonlyactiveenrolconfig); + + $this->setUser($user); + // Set the 'grade_report_showonlyactiveenrol' user preference. + set_user_preference('grade_report_showonlyactiveenrol', $showonlyactiveenrolpreference); + + // Set the 'moodle/course:viewsuspendedusers' user capability. + assign_capability('moodle/course:viewsuspendedusers', $viewsuspendeduserscapability ? + CAP_ALLOW : CAP_PROHIBIT, $editingteacherroleid, \context_course::instance($course->id)); + + $this->assertEquals($expected, $gradeitem->should_grade_only_active_users()); + } + + /** + * Data provider for test_should_grade_only_active_users. + * + * @return array + */ + public function should_grade_only_active_users_provider(): array { + return [ + 'Enabled showonlyactiveenrol setting; enabled showonlyactiveenrol preference; view suspended users capability' => + [ + 'showonlyactiveenrolconfig' => true, + 'showonlyactiveenrolpreference' => true, + 'viewsuspendeduserscapability' => true, + 'expected' => true, + ], + 'Disabled showonlyactiveenrol setting; enabled showonlyactiveenrol preference; view suspended users capability' => + [ + 'showonlyactiveenrolconfig' => false, + 'showonlyactiveenrolpreference' => true, + 'viewsuspendeduserscapability' => true, + 'expected' => true, + ], + 'Disabled showonlyactiveenrol setting; disabled showonlyactiveenrol preference; view suspended users capability' => + [ + 'showonlyactiveenrolconfig' => false, + 'showonlyactiveenrolpreference' => false, + 'viewsuspendeduserscapability' => true, + 'expected' => false, + ], + 'Disabled showonlyactiveenrol setting; disabled showonlyactiveenrol preference; no view suspended users capability' => + [ + 'showonlyactiveenrolconfig' => false, + 'showonlyactiveenrolpreference' => false, + 'viewsuspendeduserscapability' => false, + 'expected' => true, + ], + 'Enabled showonlyactiveenrol setting; enabled showonlyactiveenrol preference; no view suspended users capability' => + [ + 'showonlyactiveenrolconfig' => true, + 'showonlyactiveenrolpreference' => true, + 'viewsuspendeduserscapability' => false, + 'expected' => true, + ], + ]; + } + /** * Get a forum instance. * diff --git a/mod/forum/view.php b/mod/forum/view.php index fd7c14c132f..c45e8eba055 100644 --- a/mod/forum/view.php +++ b/mod/forum/view.php @@ -197,6 +197,7 @@ switch ($forum->get_type()) { 'gradingcomponent' => $forumgradeitem->get_grading_component_name(), 'gradingcomponentsubtype' => $forumgradeitem->get_grading_component_subtype(), 'sendstudentnotifications' => $forum->should_notify_students_default_when_grade_for_forum(), + 'gradeonlyactiveusers' => $forumgradeitem->should_grade_only_active_users(), ]; echo $OUTPUT->render_from_template('mod_forum/grades/grade_button', $gradeobj); }