From 4da5dbcb522faef5b0e137f98672fb8530e537b4 Mon Sep 17 00:00:00 2001 From: Mihail Geshoski Date: Mon, 8 Jul 2024 10:40:43 +0800 Subject: [PATCH 1/4] MDL-82393 course: The group selector component should be context-aware The group selector component should be sensitive to the current context to ensure accurate validation and retrieval of group settings (e.g. group mode). --- course/amd/build/actionbar/group.min.js | 2 +- course/amd/build/actionbar/group.min.js.map | 2 +- course/amd/src/actionbar/group.js | 11 ++-- .../output/actionbar/group_selector.php | 51 +++++++++++++++---- .../grader/classes/output/action_bar.php | 3 +- .../singleview/classes/report/singleview.php | 2 +- .../report/user/classes/output/action_bar.php | 2 +- group/amd/build/comboboxsearch/group.min.js | 2 +- .../amd/build/comboboxsearch/group.min.js.map | 2 +- .../build/comboboxsearch/repository.min.js | 2 +- .../comboboxsearch/repository.min.js.map | 2 +- group/amd/src/comboboxsearch/group.js | 20 ++++++-- group/amd/src/comboboxsearch/repository.js | 8 +-- .../external/get_groups_for_selector.php | 47 ++++++++++++++--- .../classes/output/grading_actionmenu.php | 3 +- mod/assign/locallib.php | 4 +- 16 files changed, 123 insertions(+), 40 deletions(-) diff --git a/course/amd/build/actionbar/group.min.js b/course/amd/build/actionbar/group.min.js index 8286e61c733..862cc18b85a 100644 --- a/course/amd/build/actionbar/group.min.js +++ b/course/amd/build/actionbar/group.min.js @@ -6,6 +6,6 @@ define("core_course/actionbar/group",["exports","core_group/comboboxsearch/group * @copyright 2024 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class Group extends _group.default{constructor(baseUrl){super(),function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(this,"baseUrl",void 0),this.baseUrl=baseUrl}static init(baseUrl){return new Group(baseUrl)}selectOneLink(groupID){const url=new URL(this.baseUrl);return url.searchParams.set("groupsearchvalue",this.getSearchTerm()),url.searchParams.set("group",groupID),url.toString()}}return _exports.default=Group,_exports.default})); +class Group extends _group.default{constructor(baseUrl){super(arguments.length>1&&void 0!==arguments[1]?arguments[1]:null),function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(this,"baseUrl",void 0),this.baseUrl=baseUrl}static init(baseUrl){return new Group(baseUrl,arguments.length>1&&void 0!==arguments[1]?arguments[1]:null)}selectOneLink(groupID){const url=new URL(this.baseUrl);return url.searchParams.set("groupsearchvalue",this.getSearchTerm()),url.searchParams.set("group",groupID),url.toString()}}return _exports.default=Group,_exports.default})); //# sourceMappingURL=group.min.js.map \ No newline at end of file diff --git a/course/amd/build/actionbar/group.min.js.map b/course/amd/build/actionbar/group.min.js.map index b5f395a2ef9..c77ab0b4bfc 100644 --- a/course/amd/build/actionbar/group.min.js.map +++ b/course/amd/build/actionbar/group.min.js.map @@ -1 +1 @@ -{"version":3,"file":"group.min.js","sources":["../../src/actionbar/group.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\nimport GroupSearch from 'core_group/comboboxsearch/group';\n\n/**\n * Allow the user to search for groups in the action bar.\n *\n * @module core_course/actionbar/group\n * @copyright 2024 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class Group extends GroupSearch {\n\n baseUrl;\n\n /**\n * Construct the class.\n * @param {string} baseUrl The base URL for the page.\n */\n constructor(baseUrl) {\n super();\n this.baseUrl = baseUrl;\n }\n\n /**\n * Allow the class to be invoked via PHP.\n *\n * @param {string} baseUrl The base URL for the page.\n * @returns {Group}\n */\n static init(baseUrl) {\n return new Group(baseUrl);\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @returns {string}\n */\n selectOneLink(groupID) {\n const url = new URL(this.baseUrl);\n url.searchParams.set('groupsearchvalue', this.getSearchTerm());\n url.searchParams.set('group', groupID);\n\n return url.toString();\n }\n}\n"],"names":["Group","GroupSearch","constructor","baseUrl","selectOneLink","groupID","url","URL","this","searchParams","set","getSearchTerm","toString"],"mappings":";;;;;;;;MAwBqBA,cAAcC,eAQ/BC,YAAYC,uLAEHA,QAAUA,oBASPA,gBACD,IAAIH,MAAMG,SASrBC,cAAcC,eACJC,IAAM,IAAIC,IAAIC,KAAKL,gBACzBG,IAAIG,aAAaC,IAAI,mBAAoBF,KAAKG,iBAC9CL,IAAIG,aAAaC,IAAI,QAASL,SAEvBC,IAAIM"} \ No newline at end of file +{"version":3,"file":"group.min.js","sources":["../../src/actionbar/group.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\nimport GroupSearch from 'core_group/comboboxsearch/group';\n\n/**\n * Allow the user to search for groups in the action bar.\n *\n * @module core_course/actionbar/group\n * @copyright 2024 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class Group extends GroupSearch {\n\n baseUrl;\n\n /**\n * Construct the class.\n *\n * @param {string} baseUrl The base URL for the page.\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n constructor(baseUrl, cmid = null) {\n super(cmid);\n this.baseUrl = baseUrl;\n }\n\n /**\n * Allow the class to be invoked via PHP.\n *\n * @param {string} baseUrl The base URL for the page.\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n * @returns {Group}\n */\n static init(baseUrl, cmid = null) {\n return new Group(baseUrl, cmid);\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @returns {string}\n */\n selectOneLink(groupID) {\n const url = new URL(this.baseUrl);\n url.searchParams.set('groupsearchvalue', this.getSearchTerm());\n url.searchParams.set('group', groupID);\n\n return url.toString();\n }\n}\n"],"names":["Group","GroupSearch","constructor","baseUrl","selectOneLink","groupID","url","URL","this","searchParams","set","getSearchTerm","toString"],"mappings":";;;;;;;;MAwBqBA,cAAcC,eAU/BC,YAAYC,sEAAgB,4KAEnBA,QAAUA,oBAUPA,gBACD,IAAIH,MAAMG,+DADO,MAU5BC,cAAcC,eACJC,IAAM,IAAIC,IAAIC,KAAKL,gBACzBG,IAAIG,aAAaC,IAAI,mBAAoBF,KAAKG,iBAC9CL,IAAIG,aAAaC,IAAI,QAASL,SAEvBC,IAAIM"} \ No newline at end of file diff --git a/course/amd/src/actionbar/group.js b/course/amd/src/actionbar/group.js index 6b175735a2d..2f7ef4ea9f8 100644 --- a/course/amd/src/actionbar/group.js +++ b/course/amd/src/actionbar/group.js @@ -28,10 +28,12 @@ export default class Group extends GroupSearch { /** * Construct the class. + * * @param {string} baseUrl The base URL for the page. + * @param {int|null} cmid ID of the course module initiating the group search (optional). */ - constructor(baseUrl) { - super(); + constructor(baseUrl, cmid = null) { + super(cmid); this.baseUrl = baseUrl; } @@ -39,10 +41,11 @@ export default class Group extends GroupSearch { * Allow the class to be invoked via PHP. * * @param {string} baseUrl The base URL for the page. + * @param {int|null} cmid ID of the course module initiating the group search (optional). * @returns {Group} */ - static init(baseUrl) { - return new Group(baseUrl); + static init(baseUrl, cmid = null) { + return new Group(baseUrl, cmid); } /** diff --git a/course/classes/output/actionbar/group_selector.php b/course/classes/output/actionbar/group_selector.php index e5450786d38..6196ebf57ae 100644 --- a/course/classes/output/actionbar/group_selector.php +++ b/course/classes/output/actionbar/group_selector.php @@ -36,13 +36,20 @@ class group_selector implements renderable, templatable { */ protected $course; + /** + * @var stdClass The context object. + */ + private stdClass $context; + /** * The class constructor. * * @param stdClass $course The course object. + * @param stdClass $context The context object. */ - public function __construct(stdClass $course) { + public function __construct(stdClass $course, stdClass $context) { $this->course = $course; + $this->context = $context; } /** @@ -55,10 +62,22 @@ class group_selector implements renderable, templatable { global $USER, $OUTPUT; $course = $this->course; - $groupmode = $course->groupmode; + // Based on the current context level, retrieve the correct group mode and grouping ID. + // Also, specify whether only groups with the participation field set to true should be returned. + if ($this->context->contextlevel === CONTEXT_MODULE) { // Module context. + $cm = get_coursemodule_from_id(false, $this->context->instanceid); + $groupmode = groups_get_activity_groupmode($cm); + $groupingid = $cm->groupingid; + $participationonly = true; + } else { // Course context. + $groupmode = $course->groupmode; + $groupingid = $course->defaultgroupingid; + $participationonly = false; + } $sbody = $OUTPUT->render_from_template('core_group/comboboxsearch/searchbody', [ 'courseid' => $course->id, + 'cmid' => $this->context->contextlevel === CONTEXT_MODULE ? $this->context->instanceid : null, 'currentvalue' => optional_param('groupsearchvalue', '', PARAM_NOTAGS), 'instance' => rand(), ]); @@ -67,20 +86,34 @@ class group_selector implements renderable, templatable { $buttondata = ['label' => $label]; - $context = \context_course::instance($course->id); - - if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $context)) { - $allowedgroups = groups_get_all_groups($course->id, 0, $course->defaultgroupingid); + if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $this->context)) { + $allowedgroups = groups_get_all_groups( + courseid: $course->id, + userid: 0, + groupingid: $groupingid, + participationonly: $participationonly + ); } else { - $allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid); + $allowedgroups = groups_get_all_groups( + courseid: $course->id, + userid: $USER->id, + groupingid: $groupingid, + participationonly: $participationonly + ); + } + + if ($this->context->contextlevel === CONTEXT_MODULE) { + $cm = get_coursemodule_from_id(false, $this->context->instanceid); + $activegroup = groups_get_activity_group($cm, true, $allowedgroups); + } else { + $activegroup = groups_get_course_group($course, true, $allowedgroups); } - $activegroup = groups_get_course_group($course, true, $allowedgroups); $buttondata['group'] = $activegroup; if ($activegroup) { $group = groups_get_group($activegroup); - $buttondata['selectedgroup'] = format_string($group->name, true, ['context' => $context]); + $buttondata['selectedgroup'] = format_string($group->name, true, ['context' => $this->context]); } else if ($activegroup === 0) { $buttondata['selectedgroup'] = get_string('allparticipants'); } diff --git a/grade/report/grader/classes/output/action_bar.php b/grade/report/grader/classes/output/action_bar.php index b6483093926..11e870b01ed 100644 --- a/grade/report/grader/classes/output/action_bar.php +++ b/grade/report/grader/classes/output/action_bar.php @@ -113,7 +113,8 @@ class action_bar extends \core_grades\output\action_bar { if ($course->groupmode) { $actionbarrenderer = $PAGE->get_renderer('core_course', 'actionbar'); - $data['groupselector'] = $actionbarrenderer->render(new \core_course\output\actionbar\group_selector($course)); + $data['groupselector'] = $actionbarrenderer->render( + new \core_course\output\actionbar\group_selector($course, $this->context)); } $resetlink = new moodle_url('/grade/report/grader/index.php', ['id' => $courseid]); diff --git a/grade/report/singleview/classes/report/singleview.php b/grade/report/singleview/classes/report/singleview.php index 23b10e9620c..be29d4a9f2c 100644 --- a/grade/report/singleview/classes/report/singleview.php +++ b/grade/report/singleview/classes/report/singleview.php @@ -147,7 +147,7 @@ class singleview extends grade_report { global $PAGE; $renderer = $PAGE->get_renderer('core_course', 'actionbar'); - return $renderer->render(new \core_course\output\actionbar\group_selector($course)); + return $renderer->render(new \core_course\output\actionbar\group_selector($course, $PAGE->context)); } /** diff --git a/grade/report/user/classes/output/action_bar.php b/grade/report/user/classes/output/action_bar.php index e21c58ee7fb..5017e9b2fcb 100644 --- a/grade/report/user/classes/output/action_bar.php +++ b/grade/report/user/classes/output/action_bar.php @@ -94,7 +94,7 @@ class action_bar extends \core_grades\output\action_bar { $userreportrenderer = $PAGE->get_renderer('gradereport_user'); $course = get_course($courseid); if ($course->groupmode) { - $groupselector = new \core_course\output\actionbar\group_selector($course); + $groupselector = new \core_course\output\actionbar\group_selector($course, $this->context); $data['groupselector'] = $PAGE->get_renderer('core_course', 'actionbar')->render($groupselector); } $data['userselector'] = [ diff --git a/group/amd/build/comboboxsearch/group.min.js b/group/amd/build/comboboxsearch/group.min.js index f44c5dc9af6..ee1a6951c64 100644 --- a/group/amd/build/comboboxsearch/group.min.js +++ b/group/amd/build/comboboxsearch/group.min.js @@ -1,3 +1,3 @@ -define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.instance=component.querySelector(this.selectors.instance).dataset.instance;const searchValueElement=this.component.querySelector("#".concat(this.searchInput.dataset.inputElement));searchValueElement.addEventListener("change",(()=>{this.toggleDropdown();const valueElement=this.component.querySelector("#".concat(this.combobox.dataset.inputElement));valueElement.value!==searchValueElement.value&&(valueElement.value=searchValueElement.value,valueElement.dispatchEvent(new Event("change",{bubbles:!0}))),searchValueElement.value=""})),this.$component.on("hide.bs.dropdown",(()=>{this.searchInput.removeAttribute("aria-activedescendant");const listbox=document.querySelector("#".concat(this.searchInput.getAttribute("aria-controls"),'[role="listbox"]'));listbox.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})),listbox.scrollTop=0,setTimeout((()=>{""!==this.searchInput.value&&(this.searchInput.value="",this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}))})),this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,instance:this.instance,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,groupimageurl:group.groupimageurl}))))}async clickHandler(e){e.target.closest(this.selectors.clearSearch)&&(e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),this.searchInput.focus(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe())}changeHandler(e){window.location=this.selectOneLink(e.target.value)}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}selectOneLink(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default})); +define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){let cmid=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"cmID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.instance=component.querySelector(this.selectors.instance).dataset.instance,this.cmID=cmid;const searchValueElement=this.component.querySelector("#".concat(this.searchInput.dataset.inputElement));searchValueElement.addEventListener("change",(()=>{this.toggleDropdown();const valueElement=this.component.querySelector("#".concat(this.combobox.dataset.inputElement));valueElement.value!==searchValueElement.value&&(valueElement.value=searchValueElement.value,valueElement.dispatchEvent(new Event("change",{bubbles:!0}))),searchValueElement.value=""})),this.$component.on("hide.bs.dropdown",(()=>{this.searchInput.removeAttribute("aria-activedescendant");const listbox=document.querySelector("#".concat(this.searchInput.getAttribute("aria-controls"),'[role="listbox"]'));listbox.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})),listbox.scrollTop=0,setTimeout((()=>{""!==this.searchInput.value&&(this.searchInput.value="",this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}))})),this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch(arguments.length>0&&void 0!==arguments[0]?arguments[0]:null)}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,instance:this.instance,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID,this.cmID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,groupimageurl:group.groupimageurl}))))}async clickHandler(e){e.target.closest(this.selectors.clearSearch)&&(e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),this.searchInput.focus(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe())}changeHandler(e){window.location=this.selectOneLink(e.target.value)}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}selectOneLink(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default})); //# sourceMappingURL=group.min.js.map \ No newline at end of file diff --git a/group/amd/build/comboboxsearch/group.min.js.map b/group/amd/build/comboboxsearch/group.min.js.map index d88faa7607d..5a5c17602e5 100644 --- a/group/amd/build/comboboxsearch/group.min.js.map +++ b/group/amd/build/comboboxsearch/group.min.js.map @@ -1 +1 @@ -{"version":3,"file":"group.min.js","sources":["../../src/comboboxsearch/group.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 * Allow the user to search for groups.\n *\n * @module core_group/comboboxsearch/group\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {groupFetch} from 'core_group/comboboxsearch/repository';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport Notification from 'core/notification';\n\nexport default class GroupSearch extends search_combobox {\n\n courseID;\n bannedFilterFields = ['id', 'link', 'groupimageurl'];\n\n constructor() {\n super();\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.groupsearchdropdown [data-region=\"searchplaceholder\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n // Override the instance since the body is built outside the constructor for the combobox.\n this.instance = component.querySelector(this.selectors.instance).dataset.instance;\n\n const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);\n searchValueElement.addEventListener('change', () => {\n this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);\n if (valueElement.value !== searchValueElement.value) {\n valueElement.value = searchValueElement.value;\n valueElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n\n searchValueElement.value = '';\n });\n\n this.$component.on('hide.bs.dropdown', () => {\n this.searchInput.removeAttribute('aria-activedescendant');\n\n const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role=\"listbox\"]`);\n listbox.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n listbox.scrollTop = 0;\n\n // Use setTimeout to make sure the following code is executed after the click event is handled.\n setTimeout(() => {\n if (this.searchInput.value !== '') {\n this.searchInput.value = '';\n this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));\n }\n });\n });\n\n this.renderDefault().catch(Notification.exception);\n }\n\n static init() {\n return new GroupSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.group-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.groupsearchdropdown';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {\n groups: this.getMatchedResults(),\n hasresults: this.getMatchedResults().length > 0,\n instance: this.instance,\n searchterm: this.getSearchTerm(),\n });\n replaceNodeContents(this.selectors.placeholder, html, js);\n // Remove aria-activedescendant when the available options change.\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n\n await this.renderDropdown();\n\n this.updateNodes();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n async fetchDataset() {\n return await groupFetch(this.courseID).then((r) => r.groups);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return filterableData;\n }\n return filterableData.filter((group) => Object.keys(group).some((key) => {\n if (group[key] === \"\" || this.bannedFilterFields.includes(key)) {\n return false;\n }\n return group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n */\n filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((group) => {\n return {\n id: group.id,\n name: group.name,\n groupimageurl: group.groupimageurl,\n };\n })\n );\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n async clickHandler(e) {\n if (e.target.closest(this.selectors.clearSearch)) {\n e.stopPropagation();\n // Clear the entered search query in the search bar.\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n this.searchInput.focus();\n this.clearSearchButton.classList.add('d-none');\n // Display results.\n await this.filterrenderpipe();\n }\n }\n\n /**\n * The handler for when a user changes the value of the component (selects an option from the dropdown).\n *\n * @param {Event} e The change event.\n */\n changeHandler(e) {\n window.location = this.selectOneLink(e.target.value);\n }\n\n /**\n * Override the input event listener for the text input area.\n */\n registerInputHandlers() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n // User has given something for us to filter against.\n await this.filterrenderpipe();\n }, 300));\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n * We will call this function when a user interacts with the combobox to redirect them to show their results in the page.\n *\n * @param {Number} groupID The ID of the group selected.\n */\n selectOneLink(groupID) {\n throw new Error(`selectOneLink(${groupID}) must be implemented in ${this.constructor.name}`);\n }\n}\n"],"names":["GroupSearch","search_combobox","constructor","selectors","this","courseid","placeholder","component","document","querySelector","componentSelector","courseID","dataset","instance","searchValueElement","searchInput","inputElement","addEventListener","toggleDropdown","valueElement","combobox","value","dispatchEvent","Event","bubbles","$component","on","removeAttribute","listbox","getAttribute","querySelectorAll","forEach","option","classList","remove","scrollTop","setTimeout","renderDefault","catch","Notification","exception","dropdownSelector","html","js","groups","getMatchedResults","hasresults","length","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","then","r","filterableData","getPreppedSearchTerm","filter","group","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","map","id","name","groupimageurl","e","target","closest","clearSearch","stopPropagation","setSearchTerms","focus","clearSearchButton","add","filterrenderpipe","changeHandler","window","location","selectOneLink","registerInputHandlers","async","groupID","Error"],"mappings":"+rBA4BqBA,oBAAoBC,yBAKrCC,wGAFqB,CAAC,KAAM,OAAQ,uBAI3BC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,2BACVC,YAAa,gEAEXC,UAAYC,SAASC,cAAcL,KAAKM,0BACzCC,SAAWJ,UAAUE,cAAcL,KAAKD,UAAUE,UAAUO,QAAQP,cAEpEQ,SAAWN,UAAUE,cAAcL,KAAKD,UAAUU,UAAUD,QAAQC,eAEnEC,mBAAqBV,KAAKG,UAAUE,yBAAkBL,KAAKW,YAAYH,QAAQI,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAef,KAAKG,UAAUE,yBAAkBL,KAAKgB,SAASR,QAAQI,eACxEG,aAAaE,QAAUP,mBAAmBO,QAC1CF,aAAaE,MAAQP,mBAAmBO,MACxCF,aAAaG,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS,MAG7DV,mBAAmBO,MAAQ,WAG1BI,WAAWC,GAAG,oBAAoB,UAC9BX,YAAYY,gBAAgB,+BAE3BC,QAAUpB,SAASC,yBAAkBL,KAAKW,YAAYc,aAAa,sCACzED,QAAQE,iBAAiB,0BAA0BC,SAAQC,SACvDA,OAAOC,UAAUC,OAAO,aAE5BN,QAAQO,UAAY,EAGpBC,YAAW,KACwB,KAA3BhC,KAAKW,YAAYM,aACZN,YAAYM,MAAQ,QACpBN,YAAYO,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,iBAKnEa,gBAAgBC,MAAMC,sBAAaC,gCAIjC,IAAIxC,YAQfU,0BACW,gBAQX+B,yBACW,oDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,OAAQxC,KAAKyC,oBACbC,WAAY1C,KAAKyC,oBAAoBE,OAAS,EAC9ClC,SAAUT,KAAKS,SACfmC,WAAY5C,KAAK6C,qDAED7C,KAAKD,UAAUG,YAAaoC,KAAMC,SAEjD5B,YAAYY,gBAAgB,oDAO5BuB,wBAAwB9C,KAAK+C,oBAAoB/C,KAAKgD,oBACtDC,2BAECjD,KAAKkD,sBAENC,gDASQ,0BAAWnD,KAAKO,UAAU6C,MAAMC,GAAMA,EAAEb,6BASrCc,sBAEoB,KAAhCtD,KAAKuD,uBACED,eAEJA,eAAeE,QAAQC,OAAUC,OAAOC,KAAKF,OAAOG,MAAMC,KAC1C,KAAfJ,MAAMI,OAAe7D,KAAK8D,mBAAmBC,SAASF,MAGnDJ,MAAMI,KAAKG,WAAWC,cAAcF,SAAS/D,KAAKuD,4BAOjEN,0BACSH,kBACD9C,KAAKyC,oBAAoByB,KAAKT,QACnB,CACHU,GAAIV,MAAMU,GACVC,KAAMX,MAAMW,KACZC,cAAeZ,MAAMY,sCAWlBC,GACXA,EAAEC,OAAOC,QAAQxE,KAAKD,UAAU0E,eAChCH,EAAEI,uBAEG/D,YAAYM,MAAQ,QACpB0D,eAAe3E,KAAKW,YAAYM,YAChCN,YAAYiE,aACZC,kBAAkBhD,UAAUiD,IAAI,gBAE/B9E,KAAK+E,oBASnBC,cAAcV,GACVW,OAAOC,SAAWlF,KAAKmF,cAAcb,EAAEC,OAAOtD,OAMlDmE,6BAESzE,YAAYE,iBAAiB,SAAS,oBAASwE,eAC3CV,eAAe3E,KAAKW,YAAYM,OAER,KAAzBjB,KAAK6C,qBAEAgC,kBAAkBhD,UAAUiD,IAAI,eAGhCD,kBAAkBhD,UAAUC,OAAO,gBAGtC9B,KAAK+E,qBACZ,MASPI,cAAcG,eACJ,IAAIC,8BAAuBD,4CAAmCtF,KAAKF,YAAYsE"} \ No newline at end of file +{"version":3,"file":"group.min.js","sources":["../../src/comboboxsearch/group.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 * Allow the user to search for groups.\n *\n * @module core_group/comboboxsearch/group\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {groupFetch} from 'core_group/comboboxsearch/repository';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport Notification from 'core/notification';\n\nexport default class GroupSearch extends search_combobox {\n\n courseID;\n cmID;\n bannedFilterFields = ['id', 'link', 'groupimageurl'];\n\n /**\n * Construct the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n constructor(cmid = null) {\n super();\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.groupsearchdropdown [data-region=\"searchplaceholder\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n // Override the instance since the body is built outside the constructor for the combobox.\n this.instance = component.querySelector(this.selectors.instance).dataset.instance;\n this.cmID = cmid;\n\n const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);\n searchValueElement.addEventListener('change', () => {\n this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);\n if (valueElement.value !== searchValueElement.value) {\n valueElement.value = searchValueElement.value;\n valueElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n\n searchValueElement.value = '';\n });\n\n this.$component.on('hide.bs.dropdown', () => {\n this.searchInput.removeAttribute('aria-activedescendant');\n\n const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role=\"listbox\"]`);\n listbox.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n listbox.scrollTop = 0;\n\n // Use setTimeout to make sure the following code is executed after the click event is handled.\n setTimeout(() => {\n if (this.searchInput.value !== '') {\n this.searchInput.value = '';\n this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));\n }\n });\n });\n\n this.renderDefault().catch(Notification.exception);\n }\n\n /**\n * Initialise an instance of the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n static init(cmid = null) {\n return new GroupSearch(cmid);\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.group-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.groupsearchdropdown';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {\n groups: this.getMatchedResults(),\n hasresults: this.getMatchedResults().length > 0,\n instance: this.instance,\n searchterm: this.getSearchTerm(),\n });\n replaceNodeContents(this.selectors.placeholder, html, js);\n // Remove aria-activedescendant when the available options change.\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n\n await this.renderDropdown();\n\n this.updateNodes();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n async fetchDataset() {\n return await groupFetch(this.courseID, this.cmID).then((r) => r.groups);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return filterableData;\n }\n return filterableData.filter((group) => Object.keys(group).some((key) => {\n if (group[key] === \"\" || this.bannedFilterFields.includes(key)) {\n return false;\n }\n return group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n */\n filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((group) => {\n return {\n id: group.id,\n name: group.name,\n groupimageurl: group.groupimageurl,\n };\n })\n );\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n async clickHandler(e) {\n if (e.target.closest(this.selectors.clearSearch)) {\n e.stopPropagation();\n // Clear the entered search query in the search bar.\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n this.searchInput.focus();\n this.clearSearchButton.classList.add('d-none');\n // Display results.\n await this.filterrenderpipe();\n }\n }\n\n /**\n * The handler for when a user changes the value of the component (selects an option from the dropdown).\n *\n * @param {Event} e The change event.\n */\n changeHandler(e) {\n window.location = this.selectOneLink(e.target.value);\n }\n\n /**\n * Override the input event listener for the text input area.\n */\n registerInputHandlers() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n // User has given something for us to filter against.\n await this.filterrenderpipe();\n }, 300));\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n * We will call this function when a user interacts with the combobox to redirect them to show their results in the page.\n *\n * @param {Number} groupID The ID of the group selected.\n */\n selectOneLink(groupID) {\n throw new Error(`selectOneLink(${groupID}) must be implemented in ${this.constructor.name}`);\n }\n}\n"],"names":["GroupSearch","search_combobox","constructor","cmid","selectors","this","courseid","placeholder","component","document","querySelector","componentSelector","courseID","dataset","instance","cmID","searchValueElement","searchInput","inputElement","addEventListener","toggleDropdown","valueElement","combobox","value","dispatchEvent","Event","bubbles","$component","on","removeAttribute","listbox","getAttribute","querySelectorAll","forEach","option","classList","remove","scrollTop","setTimeout","renderDefault","catch","Notification","exception","dropdownSelector","html","js","groups","getMatchedResults","hasresults","length","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","then","r","filterableData","getPreppedSearchTerm","filter","group","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","map","id","name","groupimageurl","e","target","closest","clearSearch","stopPropagation","setSearchTerms","focus","clearSearchButton","add","filterrenderpipe","changeHandler","window","location","selectOneLink","registerInputHandlers","async","groupID","Error"],"mappings":"+rBA4BqBA,oBAAoBC,yBAWrCC,kBAAYC,4DAAO,mIAPE,CAAC,KAAM,OAAQ,uBAS3BC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,2BACVC,YAAa,gEAEXC,UAAYC,SAASC,cAAcL,KAAKM,0BACzCC,SAAWJ,UAAUE,cAAcL,KAAKD,UAAUE,UAAUO,QAAQP,cAEpEQ,SAAWN,UAAUE,cAAcL,KAAKD,UAAUU,UAAUD,QAAQC,cACpEC,KAAOZ,WAENa,mBAAqBX,KAAKG,UAAUE,yBAAkBL,KAAKY,YAAYJ,QAAQK,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAehB,KAAKG,UAAUE,yBAAkBL,KAAKiB,SAAST,QAAQK,eACxEG,aAAaE,QAAUP,mBAAmBO,QAC1CF,aAAaE,MAAQP,mBAAmBO,MACxCF,aAAaG,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS,MAG7DV,mBAAmBO,MAAQ,WAG1BI,WAAWC,GAAG,oBAAoB,UAC9BX,YAAYY,gBAAgB,+BAE3BC,QAAUrB,SAASC,yBAAkBL,KAAKY,YAAYc,aAAa,sCACzED,QAAQE,iBAAiB,0BAA0BC,SAAQC,SACvDA,OAAOC,UAAUC,OAAO,aAE5BN,QAAQO,UAAY,EAGpBC,YAAW,KACwB,KAA3BjC,KAAKY,YAAYM,aACZN,YAAYM,MAAQ,QACpBN,YAAYO,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,iBAKnEa,gBAAgBC,MAAMC,sBAAaC,gCASjC,IAAI1C,mEADI,MASnBW,0BACW,gBAQXgC,yBACW,oDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,OAAQzC,KAAK0C,oBACbC,WAAY3C,KAAK0C,oBAAoBE,OAAS,EAC9CnC,SAAUT,KAAKS,SACfoC,WAAY7C,KAAK8C,qDAED9C,KAAKD,UAAUG,YAAaqC,KAAMC,SAEjD5B,YAAYY,gBAAgB,oDAO5BuB,wBAAwB/C,KAAKgD,oBAAoBhD,KAAKiD,oBACtDC,2BAEClD,KAAKmD,sBAENC,gDASQ,0BAAWpD,KAAKO,SAAUP,KAAKU,MAAM2C,MAAMC,GAAMA,EAAEb,6BAShDc,sBAEoB,KAAhCvD,KAAKwD,uBACED,eAEJA,eAAeE,QAAQC,OAAUC,OAAOC,KAAKF,OAAOG,MAAMC,KAC1C,KAAfJ,MAAMI,OAAe9D,KAAK+D,mBAAmBC,SAASF,MAGnDJ,MAAMI,KAAKG,WAAWC,cAAcF,SAAShE,KAAKwD,4BAOjEN,0BACSH,kBACD/C,KAAK0C,oBAAoByB,KAAKT,QACnB,CACHU,GAAIV,MAAMU,GACVC,KAAMX,MAAMW,KACZC,cAAeZ,MAAMY,sCAWlBC,GACXA,EAAEC,OAAOC,QAAQzE,KAAKD,UAAU2E,eAChCH,EAAEI,uBAEG/D,YAAYM,MAAQ,QACpB0D,eAAe5E,KAAKY,YAAYM,YAChCN,YAAYiE,aACZC,kBAAkBhD,UAAUiD,IAAI,gBAE/B/E,KAAKgF,oBASnBC,cAAcV,GACVW,OAAOC,SAAWnF,KAAKoF,cAAcb,EAAEC,OAAOtD,OAMlDmE,6BAESzE,YAAYE,iBAAiB,SAAS,oBAASwE,eAC3CV,eAAe5E,KAAKY,YAAYM,OAER,KAAzBlB,KAAK8C,qBAEAgC,kBAAkBhD,UAAUiD,IAAI,eAGhCD,kBAAkBhD,UAAUC,OAAO,gBAGtC/B,KAAKgF,qBACZ,MASPI,cAAcG,eACJ,IAAIC,8BAAuBD,4CAAmCvF,KAAKH,YAAYwE"} \ No newline at end of file diff --git a/group/amd/build/comboboxsearch/repository.min.js b/group/amd/build/comboboxsearch/repository.min.js index b0ebbbeff33..e41dcb66669 100644 --- a/group/amd/build/comboboxsearch/repository.min.js +++ b/group/amd/build/comboboxsearch/repository.min.js @@ -5,6 +5,6 @@ define("core_group/comboboxsearch/repository",["exports","core/ajax"],(function( * @module core_group/comboboxsearch/repository * @copyright 2023 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.groupFetch=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.groupFetch=courseid=>{const request={methodname:"core_group_get_groups_for_selector",args:{courseid:courseid}};return _ajax.default.call([request])[0]}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.groupFetch=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.groupFetch=function(courseid){let cmid=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const request={methodname:"core_group_get_groups_for_selector",args:{courseid:courseid,cmid:cmid}};return _ajax.default.call([request])[0]}})); //# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/group/amd/build/comboboxsearch/repository.min.js.map b/group/amd/build/comboboxsearch/repository.min.js.map index 0209f155db6..ab127443c2b 100644 --- a/group/amd/build/comboboxsearch/repository.min.js.map +++ b/group/amd/build/comboboxsearch/repository.min.js.map @@ -1 +1 @@ -{"version":3,"file":"repository.min.js","sources":["../../src/comboboxsearch/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 repo for the comboboxsearch group type.\n *\n * @module core_group/comboboxsearch/repository\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from \"core/ajax\";\n\n/**\n * Given a course ID, we want to fetch the groups, so we may fetch their users.\n *\n * @method groupFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @return {object} jQuery promise\n */\nexport const groupFetch = (courseid) => {\n const request = {\n methodname: 'core_group_get_groups_for_selector',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","request","methodname","args","ajax","call"],"mappings":";;;;;;;gKAgC2BA,iBACjBC,QAAU,CACZC,WAAY,qCACZC,KAAM,CACFH,SAAUA,kBAGXI,cAAKC,KAAK,CAACJ,UAAU"} \ No newline at end of file +{"version":3,"file":"repository.min.js","sources":["../../src/comboboxsearch/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 repo for the comboboxsearch group type.\n *\n * @module core_group/comboboxsearch/repository\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from \"core/ajax\";\n\n/**\n * Given a course ID and optionally a module ID, we want to fetch the groups, so we may fetch their users.\n *\n * @method groupFetch\n * @param {int} courseid ID of the course to fetch the groups of.\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n * @return {object} jQuery promise\n */\nexport const groupFetch = (courseid, cmid = null) => {\n const request = {\n methodname: 'core_group_get_groups_for_selector',\n args: {\n courseid: courseid,\n cmid: cmid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","cmid","request","methodname","args","ajax","call"],"mappings":";;;;;;;gKAiC0B,SAACA,cAAUC,4DAAO,WAClCC,QAAU,CACZC,WAAY,qCACZC,KAAM,CACFJ,SAAUA,SACVC,KAAMA,cAGPI,cAAKC,KAAK,CAACJ,UAAU"} \ No newline at end of file diff --git a/group/amd/src/comboboxsearch/group.js b/group/amd/src/comboboxsearch/group.js index 9e7b93ea055..7789ebbd79f 100644 --- a/group/amd/src/comboboxsearch/group.js +++ b/group/amd/src/comboboxsearch/group.js @@ -29,9 +29,15 @@ import Notification from 'core/notification'; export default class GroupSearch extends search_combobox { courseID; + cmID; bannedFilterFields = ['id', 'link', 'groupimageurl']; - constructor() { + /** + * Construct the class. + * + * @param {int|null} cmid ID of the course module initiating the group search (optional). + */ + constructor(cmid = null) { super(); this.selectors = {...this.selectors, courseid: '[data-region="courseid"]', @@ -41,6 +47,7 @@ export default class GroupSearch extends search_combobox { this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid; // Override the instance since the body is built outside the constructor for the combobox. this.instance = component.querySelector(this.selectors.instance).dataset.instance; + this.cmID = cmid; const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`); searchValueElement.addEventListener('change', () => { @@ -76,8 +83,13 @@ export default class GroupSearch extends search_combobox { this.renderDefault().catch(Notification.exception); } - static init() { - return new GroupSearch(); + /** + * Initialise an instance of the class. + * + * @param {int|null} cmid ID of the course module initiating the group search (optional). + */ + static init(cmid = null) { + return new GroupSearch(cmid); } /** @@ -131,7 +143,7 @@ export default class GroupSearch extends search_combobox { * @returns {Promise<*>} */ async fetchDataset() { - return await groupFetch(this.courseID).then((r) => r.groups); + return await groupFetch(this.courseID, this.cmID).then((r) => r.groups); } /** diff --git a/group/amd/src/comboboxsearch/repository.js b/group/amd/src/comboboxsearch/repository.js index 93d6f8043a9..faf3e5a9b14 100644 --- a/group/amd/src/comboboxsearch/repository.js +++ b/group/amd/src/comboboxsearch/repository.js @@ -24,17 +24,19 @@ import ajax from "core/ajax"; /** - * Given a course ID, we want to fetch the groups, so we may fetch their users. + * Given a course ID and optionally a module ID, we want to fetch the groups, so we may fetch their users. * * @method groupFetch - * @param {int} courseid ID of the course to fetch the users of. + * @param {int} courseid ID of the course to fetch the groups of. + * @param {int|null} cmid ID of the course module initiating the group search (optional). * @return {object} jQuery promise */ -export const groupFetch = (courseid) => { +export const groupFetch = (courseid, cmid = null) => { const request = { methodname: 'core_group_get_groups_for_selector', args: { courseid: courseid, + cmid: cmid, }, }; return ajax.call([request])[0]; diff --git a/group/classes/external/get_groups_for_selector.php b/group/classes/external/get_groups_for_selector.php index 8dc43009bd5..cb2a9ee40c2 100644 --- a/group/classes/external/get_groups_for_selector.php +++ b/group/classes/external/get_groups_for_selector.php @@ -17,6 +17,7 @@ namespace core_group\external; use context_course; +use context_module; use core_external\external_api; use core_external\external_description; use core_external\external_function_parameters; @@ -49,6 +50,7 @@ class get_groups_for_selector extends external_api { return new external_function_parameters ( [ 'courseid' => new external_value(PARAM_INT, 'Course Id', VALUE_REQUIRED), + 'cmid' => new external_value(PARAM_INT, 'Course module Id', VALUE_DEFAULT, 0), ] ); } @@ -56,38 +58,66 @@ class get_groups_for_selector extends external_api { /** * Given a course ID find the existing user groups and map some fields to the returned array of group objects. * + * If a course module ID is provided, this function will return only the available groups within the given course + * module, adhering to the set group mode for that context. All validation checks will be performed within this + * specific context. + * * @param int $courseid + * @param int|null $cmid The course module ID (optional). * @return array Groups and warnings to pass back to the calling widget. */ - public static function execute(int $courseid): array { + public static function execute(int $courseid, ?int $cmid = null): array { global $DB, $USER, $OUTPUT; $params = self::validate_parameters( self::execute_parameters(), [ 'courseid' => $courseid, + 'cmid' => $cmid, ] ); $warnings = []; - $context = context_course::instance($params['courseid']); + $course = $DB->get_record('course', ['id' => $params['courseid']]); + + if ($params['cmid']) { + $context = context_module::instance($params['cmid']); + $cm = get_coursemodule_from_id('', $params['cmid']); + $groupmode = groups_get_activity_groupmode($cm, $course); + $groupingid = $cm->groupingid; + $participationonly = true; + } else { + $context = context_course::instance($params['courseid']); + $groupmode = $course->groupmode; + $groupingid = $course->defaultgroupingid; + $participationonly = false; + } parent::validate_context($context); $mappedgroups = []; - $course = $DB->get_record('course', ['id' => $params['courseid']]); // Initialise the grade tracking object. - if ($groupmode = $course->groupmode) { + if ($groupmode) { $aag = has_capability('moodle/site:accessallgroups', $context); $usergroups = []; - $groupuserid = 0; if ($groupmode == VISIBLEGROUPS || $aag) { + $groupuserid = 0; // Get user's own groups and put to the top. - $usergroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid); + $usergroups = groups_get_all_groups( + courseid: $course->id, + userid: $USER->id, + groupingid: $groupingid, + participationonly: $participationonly + ); } else { $groupuserid = $USER->id; } - $allowedgroups = groups_get_all_groups($course->id, $groupuserid, $course->defaultgroupingid); + $allowedgroups = groups_get_all_groups( + courseid: $course->id, + userid: $groupuserid, + groupingid: $groupingid, + participationonly: $participationonly + ); $allgroups = array_merge($allowedgroups, $usergroups); // Filter out any duplicate groups. @@ -103,7 +133,8 @@ class get_groups_for_selector extends external_api { $mappedgroups = array_map(function($group) use ($context, $OUTPUT) { if ($group->id) { // Particular group. Get the group picture if it exists, otherwise return a generic image. $picture = get_group_picture_url($group, $group->courseid, true) ?? - moodle_url::make_pluginfile_url($context->id, 'group', 'generated', $group->id, '/', 'group.svg'); + moodle_url::make_pluginfile_url($context->get_course_context()->id, 'group', 'generated', $group->id, + '/', 'group.svg'); } else { // All participants. $picture = $OUTPUT->image_url('g/g1'); } diff --git a/mod/assign/classes/output/grading_actionmenu.php b/mod/assign/classes/output/grading_actionmenu.php index c574eeb6fbc..9e34454f74a 100644 --- a/mod/assign/classes/output/grading_actionmenu.php +++ b/mod/assign/classes/output/grading_actionmenu.php @@ -101,7 +101,8 @@ class grading_actionmenu implements templatable, renderable { $data['userselector'] = $actionbarrenderer->render($userselector); if ($course->groupmode) { - $data['groupselector'] = $actionbarrenderer->render(new \core_course\output\actionbar\group_selector($course)); + $data['groupselector'] = $actionbarrenderer->render( + new \core_course\output\actionbar\group_selector($course, $PAGE->context)); } if (groups_get_course_group($course)) { diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 760cf577e73..6bde5666419 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -4575,8 +4575,8 @@ class assign { $PAGE->requires->js_call_amd('mod_assign/user', 'init', [$currenturl->out(false)]); // Conditionally add the group JS if we have groups enabled. - if ($this->get_course()->groupmode) { - $PAGE->requires->js_call_amd('core_course/actionbar/group', 'init', [$currenturl->out(false)]); + if (groups_get_activity_groupmode($this->get_course_module(), $this->get_course())) { + $PAGE->requires->js_call_amd('core_course/actionbar/group', 'init', [$currenturl->out(false), $cmid]); } $header = new assign_header($this->get_instance(), From 19786070c709c639c7b174b2c2cfdcce33983223 Mon Sep 17 00:00:00 2001 From: Mihail Geshoski Date: Fri, 5 Jul 2024 11:21:46 +0800 Subject: [PATCH 2/4] MDL-82393 mod_assign: The group selector follows the correct group mode This change ensures that the group selector correctly appears on the submission page, adhering to the assignment's group mode settings or the enforced group mode set by the course. --- .../classes/output/grading_actionmenu.php | 6 ++- .../tests/behat/group_submission.feature | 44 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/mod/assign/classes/output/grading_actionmenu.php b/mod/assign/classes/output/grading_actionmenu.php index 9e34454f74a..7fac7f10a93 100644 --- a/mod/assign/classes/output/grading_actionmenu.php +++ b/mod/assign/classes/output/grading_actionmenu.php @@ -80,7 +80,9 @@ class grading_actionmenu implements templatable, renderable { global $PAGE; $course = $this->assign->get_course(); + $cm = get_coursemodule_from_id('assign', $this->cmid); $actionbarrenderer = $PAGE->get_renderer('core_course', 'actionbar'); + $data = []; $userid = optional_param('userid', null, PARAM_INT); @@ -100,12 +102,12 @@ class grading_actionmenu implements templatable, renderable { ); $data['userselector'] = $actionbarrenderer->render($userselector); - if ($course->groupmode) { + if (groups_get_activity_groupmode($cm, $course)) { $data['groupselector'] = $actionbarrenderer->render( new \core_course\output\actionbar\group_selector($course, $PAGE->context)); } - if (groups_get_course_group($course)) { + if (groups_get_activity_group($cm)) { $url = new moodle_url('/mod/assign/view.php', [ 'id' => $this->cmid, 'action' => 'grading', diff --git a/mod/assign/tests/behat/group_submission.feature b/mod/assign/tests/behat/group_submission.feature index e0bccf4dd0f..a098e4e804f 100644 --- a/mod/assign/tests/behat/group_submission.feature +++ b/mod/assign/tests/behat/group_submission.feature @@ -69,6 +69,50 @@ Feature: Group assignment submissions And I am on the "Homepage" page logged in as student1 And I should not see "Test assignment name" in the "Timeline" "block" + @javascript + Scenario: Confirm that the group switching option is available only when the group settings are correctly configured + Given the following "activity" exists: + | activity | assign | + | course | C1 | + | name | Test assignment | + And I am on the "Test assignment" "assign activity editing" page logged in as teacher1 + # The assignment does not have a specified group mode. + When I set the following fields to these values: + | Group mode | No groups | + And I press "Save and display" + And I follow "View all submissions" + Then ".groupsearchwidget" "css_element" should not exist + # The course has a specified group mode, but not enforced on modules. + And I am on the "C1" "course editing" page + And I set the following fields to these values: + | Group mode | Separate groups | + | Force group mode | No | + And I press "Save and display" + And I am on the "Test assignment" Activity page + And I follow "View all submissions" + And ".groupsearchwidget" "css_element" should not exist + # The assignment has a specified group mode. + And I am on the "Test assignment" "assign activity editing" page + And I set the following fields to these values: + | Group mode | Visible groups | + And I press "Save and display" + And I follow "View all submissions" + And ".groupsearchwidget" "css_element" should exist + And I should see "Select visible groups" in the ".groupsearchwidget" "css_element" + And I confirm "All participants" in "group" search within the gradebook widget exists + And I confirm "Group 1" in "group" search within the gradebook widget exists + # The course enforces its group mode on modules. + And I am on the "C1" "course editing" page + And I set the following fields to these values: + | Force group mode | Yes | + And I press "Save and display" + And I am on the "Test assignment" Activity page + And I follow "View all submissions" + And ".groupsearchwidget" "css_element" should exist + And I should see "Select separate groups" in the ".groupsearchwidget" "css_element" + And I confirm "All participants" in "group" search within the gradebook widget exists + And I confirm "Group 1" in "group" search within the gradebook widget exists + @javascript Scenario: Switch between group modes Given the following "activity" exists: From 02755c4f952f23518a02ca6a2397d3213d791018 Mon Sep 17 00:00:00 2001 From: Mihail Geshoski Date: Mon, 8 Jul 2024 13:29:13 +0800 Subject: [PATCH 3/4] MDL-82393 course: Refactor the renderable class for the group selector The group selector renderable class should extend the comboboxsearch renderable class to eliminate the need for initializing the comboboxsearch object within the export_for_template() method and to prevent duplicating the get_template() method. --- .../output/actionbar/group_selector.php | 160 +++++++++--------- 1 file changed, 83 insertions(+), 77 deletions(-) diff --git a/course/classes/output/actionbar/group_selector.php b/course/classes/output/actionbar/group_selector.php index 6196ebf57ae..107fbde39e2 100644 --- a/course/classes/output/actionbar/group_selector.php +++ b/course/classes/output/actionbar/group_selector.php @@ -17,10 +17,7 @@ namespace core_course\output\actionbar; use core\output\comboboxsearch; -use renderable; -use renderer_base; use stdClass; -use templatable; /** * Renderable class for the group selector element in the action bar. @@ -29,7 +26,7 @@ use templatable; * @copyright 2024 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class group_selector implements renderable, templatable { +class group_selector extends comboboxsearch { /** * @var stdClass The course object. @@ -50,97 +47,106 @@ class group_selector implements renderable, templatable { public function __construct(stdClass $course, stdClass $context) { $this->course = $course; $this->context = $context; + parent::__construct(false, $this->get_button_content(), $this->get_dropdown_content(), 'group-search', + 'groupsearchwidget', 'groupsearchdropdown overflow-auto', null, true, $this->get_label(), 'group', + $this->get_active_group()); } /** - * Export the data for the mustache template. + * Returns the output for the button (trigger) element of the group selector. * - * @param renderer_base $output The renderer that will be used to render the output. - * @return array + * @return string HTML fragment */ - public function export_for_template(renderer_base $output) { - global $USER, $OUTPUT; + private function get_button_content(): string { + global $OUTPUT; - $course = $this->course; - // Based on the current context level, retrieve the correct group mode and grouping ID. - // Also, specify whether only groups with the participation field set to true should be returned. - if ($this->context->contextlevel === CONTEXT_MODULE) { // Module context. - $cm = get_coursemodule_from_id(false, $this->context->instanceid); - $groupmode = groups_get_activity_groupmode($cm); - $groupingid = $cm->groupingid; - $participationonly = true; - } else { // Course context. - $groupmode = $course->groupmode; - $groupingid = $course->defaultgroupingid; - $participationonly = false; - } - - $sbody = $OUTPUT->render_from_template('core_group/comboboxsearch/searchbody', [ - 'courseid' => $course->id, - 'cmid' => $this->context->contextlevel === CONTEXT_MODULE ? $this->context->instanceid : null, - 'currentvalue' => optional_param('groupsearchvalue', '', PARAM_NOTAGS), - 'instance' => rand(), - ]); - - $label = $groupmode == VISIBLEGROUPS ? get_string('selectgroupsvisible') : get_string('selectgroupsseparate'); - - $buttondata = ['label' => $label]; - - if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $this->context)) { - $allowedgroups = groups_get_all_groups( - courseid: $course->id, - userid: 0, - groupingid: $groupingid, - participationonly: $participationonly - ); - } else { - $allowedgroups = groups_get_all_groups( - courseid: $course->id, - userid: $USER->id, - groupingid: $groupingid, - participationonly: $participationonly - ); - } - - if ($this->context->contextlevel === CONTEXT_MODULE) { - $cm = get_coursemodule_from_id(false, $this->context->instanceid); - $activegroup = groups_get_activity_group($cm, true, $allowedgroups); - } else { - $activegroup = groups_get_course_group($course, true, $allowedgroups); - } - - $buttondata['group'] = $activegroup; + $activegroup = $this->get_active_group(); + $buttondata = [ + 'label' => $this->get_label(), + 'group' => $activegroup, + ]; if ($activegroup) { $group = groups_get_group($activegroup); - $buttondata['selectedgroup'] = format_string($group->name, true, ['context' => $this->context]); + $buttondata['selectedgroup'] = format_string($group->name, true, + ['context' => \context_course::instance($this->course->id)]); } else if ($activegroup === 0) { $buttondata['selectedgroup'] = get_string('allparticipants'); } - $groupdropdown = new comboboxsearch( - false, - $OUTPUT->render_from_template('core_group/comboboxsearch/group_selector', $buttondata), - $sbody, - 'group-search', - 'groupsearchwidget', - 'groupsearchdropdown overflow-auto', - null, - true, - $label, - 'group', - $activegroup - ); - - return $groupdropdown->export_for_template($OUTPUT); + return $OUTPUT->render_from_template('core_group/comboboxsearch/group_selector', $buttondata); } /** - * Returns the template for the group selector. + * Returns the output of the content rendered within the dropdown (search body area) of the group selector. + * + * @return string HTML fragment + */ + private function get_dropdown_content(): string { + global $OUTPUT; + + return $OUTPUT->render_from_template('core_group/comboboxsearch/searchbody', [ + 'courseid' => $this->course->id, + 'currentvalue' => optional_param('groupsearchvalue', '', PARAM_NOTAGS), + 'instance' => rand(), + ]); + } + + /** + * Returns the label text for the group selector based on specified group mode. * * @return string */ - public function get_template(): string { - return 'core/comboboxsearch'; + private function get_label(): string { + return $this->get_group_mode() === VISIBLEGROUPS ? get_string('selectgroupsvisible') : + get_string('selectgroupsseparate'); + } + + /** + * Returns the active group based on the context level. + * + * @return int|bool The active group (false if groups not used, int if groups used) + */ + private function get_active_group(): int|bool { + global $USER; + + $canaccessallgroups = has_capability('moodle/site:accessallgroups', $this->context); + $userid = $this->get_group_mode() == VISIBLEGROUPS || $canaccessallgroups ? 0 : $USER->id; + // Based on the current context level, retrieve the correct grouping ID and specify whether only groups with the + // participation field set to true should be returned. + if ($this->context->contextlevel === CONTEXT_MODULE) { + $cm = get_coursemodule_from_id(false, $this->context->instanceid); + $groupingid = $cm->groupingid; + $participationonly = true; + } else { + $cm = null; + $groupingid = $this->course->defaultgroupingid; + $participationonly = false; + } + + $allowedgroups = groups_get_all_groups( + courseid: $this->course->id, + userid: $userid, + groupingid: $groupingid, + participationonly: $participationonly + ); + + if ($cm) { + return groups_get_activity_group($cm, true, $allowedgroups); + } + return groups_get_course_group($this->course, true, $allowedgroups); + } + + /** + * Returns the group mode based on the context level. + * + * @return int The group mode + */ + private function get_group_mode(): int { + if ($this->context->contextlevel == CONTEXT_MODULE) { + $cm = get_coursemodule_from_id(false, $this->context->instanceid); + return groups_get_activity_groupmode($cm); + } + return $this->course->groupmode; } } From b7481cf4aaa192b0e0b71f60a317a4cab1e240c8 Mon Sep 17 00:00:00 2001 From: Mihail Geshoski Date: Fri, 12 Jul 2024 13:50:04 +0800 Subject: [PATCH 4/4] MDL-82393 course: Deprecate the $course argument in group_selector Deprecates the $course parameter in the constructor of the core_course\output\actionbar\group_selector class. This parameter is no longer used, as the $course object can now be obtained through the $context class property. Additionaly, the $course class property has been removed in accordance with this change. --- .upgradenotes/MDL-82393-2024071205253416.yml | 12 ++++++++ .../output/actionbar/group_selector.php | 30 ++++++++++--------- .../grader/classes/output/action_bar.php | 2 +- .../singleview/classes/report/singleview.php | 2 +- .../report/user/classes/output/action_bar.php | 2 +- .../classes/output/grading_actionmenu.php | 2 +- 6 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 .upgradenotes/MDL-82393-2024071205253416.yml diff --git a/.upgradenotes/MDL-82393-2024071205253416.yml b/.upgradenotes/MDL-82393-2024071205253416.yml new file mode 100644 index 00000000000..7a8fb25c665 --- /dev/null +++ b/.upgradenotes/MDL-82393-2024071205253416.yml @@ -0,0 +1,12 @@ +issueNumber: MDL-82393 +notes: + core_course: + - message: >- + The $course parameter in the constructor of the + core_course\output\actionbar\group_selector class has been deprecated + and is no longer used. + type: deprecated + - message: >- + The $course class property in the + core_course\output\actionbar\group_selector class has been removed. + type: removed diff --git a/course/classes/output/actionbar/group_selector.php b/course/classes/output/actionbar/group_selector.php index 107fbde39e2..8b80d85874b 100644 --- a/course/classes/output/actionbar/group_selector.php +++ b/course/classes/output/actionbar/group_selector.php @@ -28,11 +28,6 @@ use stdClass; */ class group_selector extends comboboxsearch { - /** - * @var stdClass The course object. - */ - protected $course; - /** * @var stdClass The context object. */ @@ -41,11 +36,16 @@ class group_selector extends comboboxsearch { /** * The class constructor. * - * @param stdClass $course The course object. + * @param null|stdClass $course This parameter has been deprecated since Moodle 4.5 and should not be used anymore. * @param stdClass $context The context object. */ - public function __construct(stdClass $course, stdClass $context) { - $this->course = $course; + public function __construct(null|stdClass $course = null, stdClass $context) { + if ($course !== null) { + debugging( + 'The course argument has been deprecated. Please remove it from your group_selector class instances.', + DEBUG_DEVELOPER, + ); + } $this->context = $context; parent::__construct(false, $this->get_button_content(), $this->get_dropdown_content(), 'group-search', 'groupsearchwidget', 'groupsearchdropdown overflow-auto', null, true, $this->get_label(), 'group', @@ -69,7 +69,7 @@ class group_selector extends comboboxsearch { if ($activegroup) { $group = groups_get_group($activegroup); $buttondata['selectedgroup'] = format_string($group->name, true, - ['context' => \context_course::instance($this->course->id)]); + ['context' => $this->context->get_course_context()]); } else if ($activegroup === 0) { $buttondata['selectedgroup'] = get_string('allparticipants'); } @@ -86,7 +86,7 @@ class group_selector extends comboboxsearch { global $OUTPUT; return $OUTPUT->render_from_template('core_group/comboboxsearch/searchbody', [ - 'courseid' => $this->course->id, + 'courseid' => $this->context->get_course_context()->instanceid, 'currentvalue' => optional_param('groupsearchvalue', '', PARAM_NOTAGS), 'instance' => rand(), ]); @@ -112,6 +112,7 @@ class group_selector extends comboboxsearch { $canaccessallgroups = has_capability('moodle/site:accessallgroups', $this->context); $userid = $this->get_group_mode() == VISIBLEGROUPS || $canaccessallgroups ? 0 : $USER->id; + $course = get_course($this->context->get_course_context()->instanceid); // Based on the current context level, retrieve the correct grouping ID and specify whether only groups with the // participation field set to true should be returned. if ($this->context->contextlevel === CONTEXT_MODULE) { @@ -120,12 +121,12 @@ class group_selector extends comboboxsearch { $participationonly = true; } else { $cm = null; - $groupingid = $this->course->defaultgroupingid; + $groupingid = $course->defaultgroupingid; $participationonly = false; } $allowedgroups = groups_get_all_groups( - courseid: $this->course->id, + courseid: $course->id, userid: $userid, groupingid: $groupingid, participationonly: $participationonly @@ -134,7 +135,7 @@ class group_selector extends comboboxsearch { if ($cm) { return groups_get_activity_group($cm, true, $allowedgroups); } - return groups_get_course_group($this->course, true, $allowedgroups); + return groups_get_course_group($course, true, $allowedgroups); } /** @@ -147,6 +148,7 @@ class group_selector extends comboboxsearch { $cm = get_coursemodule_from_id(false, $this->context->instanceid); return groups_get_activity_groupmode($cm); } - return $this->course->groupmode; + $course = get_course($this->context->instanceid); + return $course->groupmode; } } diff --git a/grade/report/grader/classes/output/action_bar.php b/grade/report/grader/classes/output/action_bar.php index 11e870b01ed..d63b4678d47 100644 --- a/grade/report/grader/classes/output/action_bar.php +++ b/grade/report/grader/classes/output/action_bar.php @@ -114,7 +114,7 @@ class action_bar extends \core_grades\output\action_bar { if ($course->groupmode) { $actionbarrenderer = $PAGE->get_renderer('core_course', 'actionbar'); $data['groupselector'] = $actionbarrenderer->render( - new \core_course\output\actionbar\group_selector($course, $this->context)); + new \core_course\output\actionbar\group_selector(null, $this->context)); } $resetlink = new moodle_url('/grade/report/grader/index.php', ['id' => $courseid]); diff --git a/grade/report/singleview/classes/report/singleview.php b/grade/report/singleview/classes/report/singleview.php index be29d4a9f2c..717a3b2cf20 100644 --- a/grade/report/singleview/classes/report/singleview.php +++ b/grade/report/singleview/classes/report/singleview.php @@ -147,7 +147,7 @@ class singleview extends grade_report { global $PAGE; $renderer = $PAGE->get_renderer('core_course', 'actionbar'); - return $renderer->render(new \core_course\output\actionbar\group_selector($course, $PAGE->context)); + return $renderer->render(new \core_course\output\actionbar\group_selector(null, $PAGE->context)); } /** diff --git a/grade/report/user/classes/output/action_bar.php b/grade/report/user/classes/output/action_bar.php index 5017e9b2fcb..ceede74769a 100644 --- a/grade/report/user/classes/output/action_bar.php +++ b/grade/report/user/classes/output/action_bar.php @@ -94,7 +94,7 @@ class action_bar extends \core_grades\output\action_bar { $userreportrenderer = $PAGE->get_renderer('gradereport_user'); $course = get_course($courseid); if ($course->groupmode) { - $groupselector = new \core_course\output\actionbar\group_selector($course, $this->context); + $groupselector = new \core_course\output\actionbar\group_selector(null, $this->context); $data['groupselector'] = $PAGE->get_renderer('core_course', 'actionbar')->render($groupselector); } $data['userselector'] = [ diff --git a/mod/assign/classes/output/grading_actionmenu.php b/mod/assign/classes/output/grading_actionmenu.php index 7fac7f10a93..a652e6d83e8 100644 --- a/mod/assign/classes/output/grading_actionmenu.php +++ b/mod/assign/classes/output/grading_actionmenu.php @@ -104,7 +104,7 @@ class grading_actionmenu implements templatable, renderable { if (groups_get_activity_groupmode($cm, $course)) { $data['groupselector'] = $actionbarrenderer->render( - new \core_course\output\actionbar\group_selector($course, $PAGE->context)); + new \core_course\output\actionbar\group_selector(null, $PAGE->context)); } if (groups_get_activity_group($cm)) {