first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
define("gradereport_grader/collapse/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* A repo for the collapsing in the grader report.
*
* @module gradereport_grader/collapse/repository
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.gradeItems=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.gradeItems=courseid=>{const request={methodname:"core_grades_get_gradeitems",args:{courseid:courseid}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=repository.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../../src/collapse/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 <http://www.gnu.org/licenses/>.\n\n/**\n * A repo for the collapsing in the grader report.\n *\n * @module gradereport_grader/collapse/repository\n * @copyright 2022 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\n\n/**\n * Fetch all the information on gradeitems we'll need in the column collapser.\n *\n * @method gradeItems\n * @param {Number} courseid What course to fetch the gradeitems for\n * @return {object} jQuery promise\n */\nexport const gradeItems = (courseid) => {\n const request = {\n methodname: 'core_grades_get_gradeitems',\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,6BACZC,KAAM,CACFH,SAAUA,kBAGXI,cAAKC,KAAK,CAACJ,UAAU"}
+10
View File
@@ -0,0 +1,10 @@
define("gradereport_grader/feedback_modal",["exports","core/modal","core/notification","core/ajax","core/templates"],(function(_exports,_modal,_notification,_ajax,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Javascript module for displaying feedback in a modal window
*
* @module gradereport_grader/feedback_modal
* @copyright 2023 Kevin Percy <kevin.percy@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modal=_interopRequireDefault(_modal),_notification=_interopRequireDefault(_notification),_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates);const Selectors_showFeedback='[data-action="feedback"]',fetchFeedback=(courseid,userid,itemid)=>{const request={methodname:"core_grades_get_feedback",args:{courseid:courseid,userid:userid,itemid:itemid}};return _ajax.default.call([request])[0]},registerEventListeners=()=>{document.addEventListener("click",(e=>{const showFeedbackTrigger=e.target.closest(Selectors_showFeedback);if(showFeedbackTrigger){e.preventDefault();(async(courseid,userid,itemid)=>{let feedbackData;try{feedbackData=await fetchFeedback(courseid,userid,itemid)}catch(e){return Promise.reject(e)}return _modal.default.create({removeOnClose:!0,large:!0,verticallyCentered:!0}).then((modal=>{const body=_templates.default.render("core_grades/feedback_modal",{feedbacktext:feedbackData.feedbacktext,user:{picture:feedbackData.picture,fullname:feedbackData.fullname,additionalfield:feedbackData.additionalfield}});return modal.setBody(body),modal.setTitle(feedbackData.title),modal.show(),modal}))})(showFeedbackTrigger.dataset.courseid,e.target.closest("tr").dataset.uid,e.target.closest("td").dataset.itemid).catch(_notification.default.exception)}}))};_exports.init=()=>{registerEventListeners()}}));
//# sourceMappingURL=feedback_modal.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"feedback_modal.min.js","sources":["../src/feedback_modal.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Javascript module for displaying feedback in a modal window\n *\n * @module gradereport_grader/feedback_modal\n * @copyright 2023 Kevin Percy <kevin.percy@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Modal from 'core/modal';\nimport Notification from 'core/notification';\nimport ajax from 'core/ajax';\nimport Templates from 'core/templates';\n\nconst Selectors = {\n showFeedback: '[data-action=\"feedback\"]'\n};\n\n/**\n * Create the modal to display the feedback.\n *\n * @param {int} courseid\n * @param {int} userid\n * @param {int} itemid\n * @returns {Promise}\n */\nconst getModal = async(courseid, userid, itemid) => {\n let feedbackData;\n\n try {\n feedbackData = await fetchFeedback(courseid, userid, itemid);\n } catch (e) {\n return Promise.reject(e);\n }\n\n return Modal.create({\n removeOnClose: true,\n large: true,\n verticallyCentered: true,\n })\n .then(modal => {\n const body = Templates.render('core_grades/feedback_modal', {\n feedbacktext: feedbackData.feedbacktext,\n user: {\n picture: feedbackData.picture,\n fullname: feedbackData.fullname,\n additionalfield: feedbackData.additionalfield,\n },\n });\n\n modal.setBody(body);\n modal.setTitle(feedbackData.title);\n modal.show();\n\n return modal;\n });\n};\n\n/**\n * Fetch the feedback data.\n *\n * @param {int} courseid\n * @param {int} userid\n * @param {int} itemid\n * @returns {Promise}\n */\nconst fetchFeedback = (courseid, userid, itemid) => {\n const request = {\n methodname: 'core_grades_get_feedback',\n args: {\n courseid: courseid,\n userid: userid,\n itemid: itemid,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Register event listeners for the View Feedback links.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', e => {\n const showFeedbackTrigger = e.target.closest(Selectors.showFeedback);\n if (showFeedbackTrigger) {\n e.preventDefault();\n\n const courseid = showFeedbackTrigger.dataset.courseid;\n const userid = e.target.closest('tr').dataset.uid;\n const itemid = e.target.closest('td').dataset.itemid;\n\n getModal(courseid, userid, itemid)\n .catch(Notification.exception);\n }\n });\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n registerEventListeners();\n};\n"],"names":["Selectors","fetchFeedback","courseid","userid","itemid","request","methodname","args","ajax","call","registerEventListeners","document","addEventListener","e","showFeedbackTrigger","target","closest","preventDefault","async","feedbackData","Promise","reject","Modal","create","removeOnClose","large","verticallyCentered","then","modal","body","Templates","render","feedbacktext","user","picture","fullname","additionalfield","setBody","setTitle","title","show","getModal","dataset","uid","catch","Notification","exception"],"mappings":";;;;;;;oQA2BMA,uBACY,2BAmDZC,cAAgB,CAACC,SAAUC,OAAQC,gBAC/BC,QAAU,CACZC,WAAY,2BACZC,KAAM,CACFL,SAAUA,SACVC,OAAQA,OACRC,OAAQA,gBAGTI,cAAKC,KAAK,CAACJ,UAAU,IAM1BK,uBAAyB,KAC3BC,SAASC,iBAAiB,SAASC,UACzBC,oBAAsBD,EAAEE,OAAOC,QAAQhB,2BACzCc,oBAAqB,CACrBD,EAAEI,iBA3DGC,OAAMhB,SAAUC,OAAQC,cACjCe,iBAGAA,mBAAqBlB,cAAcC,SAAUC,OAAQC,QACvD,MAAOS,UACEO,QAAQC,OAAOR,UAGnBS,eAAMC,OAAO,CAChBC,eAAe,EACfC,OAAO,EACPC,oBAAoB,IAEvBC,MAAKC,cACIC,KAAOC,mBAAUC,OAAO,6BAA8B,CACxDC,aAAcb,aAAaa,aAC3BC,KAAM,CACFC,QAASf,aAAae,QACtBC,SAAUhB,aAAagB,SACvBC,gBAAiBjB,aAAaiB,0BAItCR,MAAMS,QAAQR,MACdD,MAAMU,SAASnB,aAAaoB,OAC5BX,MAAMY,OAECZ,UAqCHa,CAJiB3B,oBAAoB4B,QAAQxC,SAC9BW,EAAEE,OAAOC,QAAQ,MAAM0B,QAAQC,IAC/B9B,EAAEE,OAAOC,QAAQ,MAAM0B,QAAQtC,QAGzCwC,MAAMC,sBAAaC,8BAQhB,KAChBpC"}
+3
View File
@@ -0,0 +1,3 @@
define("gradereport_grader/group",["exports","core_group/comboboxsearch/group","core/url"],(function(_exports,_group,_url){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_group=_interopRequireDefault(_group),_url=_interopRequireDefault(_url);class Group extends _group.default{constructor(){var obj,key,value;super(),value=void 0,(key="courseID")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.selectors={...this.selectors,courseid:'[data-region="courseid"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid}static init(){return new Group}selectOneLink(groupID){return _url.default.relativeUrl("/grade/report/grader/index.php",{id:this.courseID,groupsearchvalue:this.getSearchTerm(),group:groupID},!1)}}return _exports.default=Group,_exports.default}));
//# sourceMappingURL=group.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"group.min.js","sources":["../src/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 <http://www.gnu.org/licenses/>.\n\n/**\n * Allow the user to search for groups within the grader report.\n *\n * @module gradereport_grader/group\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GroupSearch from 'core_group/comboboxsearch/group';\nimport Url from 'core/url';\n\nexport default class Group extends GroupSearch {\n\n courseID;\n\n constructor() {\n super();\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n }\n\n static init() {\n return new Group();\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 return Url.relativeUrl('/grade/report/grader/index.php', {\n id: this.courseID,\n groupsearchvalue: this.getSearchTerm(),\n group: groupID,\n }, false);\n }\n}\n"],"names":["Group","GroupSearch","constructor","selectors","this","courseid","component","document","querySelector","componentSelector","courseID","dataset","selectOneLink","groupID","Url","relativeUrl","id","groupsearchvalue","getSearchTerm","group"],"mappings":"2WAyBqBA,cAAcC,eAI/BC,6LAISC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,kCAERC,UAAYC,SAASC,cAAcJ,KAAKK,0BACzCC,SAAWJ,UAAUE,cAAcJ,KAAKD,UAAUE,UAAUM,QAAQN,8BAIlE,IAAIL,MASfY,cAAcC,gBACHC,aAAIC,YAAY,iCAAkC,CACrDC,GAAIZ,KAAKM,SACTO,iBAAkBb,KAAKc,gBACvBC,MAAON,UACR"}
@@ -0,0 +1,10 @@
define("gradereport_grader/local/user/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* A repo for the search partial in the grader report.
*
* @module gradereport_grader/local/user/repository
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.userFetch=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.userFetch=courseid=>{const request={methodname:"gradereport_grader_get_users_in_report",args:{courseid:courseid}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=repository.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../../../src/local/user/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 <http://www.gnu.org/licenses/>.\n\n/**\n * A repo for the search partial in the grader report.\n *\n * @module gradereport_grader/local/user/repository\n * @copyright 2022 Mathew May <mathew.solutions>\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 learners within this report.\n *\n * @method userFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @return {object} jQuery promise\n */\nexport const userFetch = (courseid) => {\n const request = {\n methodname: 'gradereport_grader_get_users_in_report',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","request","methodname","args","ajax","call"],"mappings":";;;;;;;8JAgC0BA,iBAChBC,QAAU,CACZC,WAAY,yCACZC,KAAM,CACFH,SAAUA,kBAGXI,cAAKC,KAAK,CAACJ,UAAU"}
+10
View File
@@ -0,0 +1,10 @@
define("gradereport_grader/stickycolspan",["exports","jquery","core/sticky-footer"],(function(_exports,_jquery,_stickyFooter){var obj;
/**
* Javascript module for fixing the position of sticky headers with multiple colspans
*
* @module gradereport_grader/stickycolspan
* @copyright 2022 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};const SELECTORS_GRADEPARENT=".gradeparent",SELECTORS_STUDENTHEADER="#studentheader",SELECTORS_TABLEHEADER="th.header",SELECTORS_BEHAT="body.behat-site",SELECTORS_USERDROPDOWN=".userrow th .dropdown",SELECTORS_LASTROW=".lastrow";_exports.init=()=>{if((0,_jquery.default)(SELECTORS_USERDROPDOWN).on("show.bs.dropdown hide.bs.dropdown",(e=>{e.target.closest(SELECTORS_TABLEHEADER).classList.toggle("actions-menu-active")})),defineLastRowIntersectionObserver(!0),document.addEventListener(_stickyFooter.eventTypes.stickyFooterStateChanged,(e=>{defineLastRowIntersectionObserver(e.detail.enabled)})),!document.querySelector(SELECTORS_BEHAT)){const grader=document.querySelector(SELECTORS_GRADEPARENT),tableHeaders=grader.querySelectorAll(SELECTORS_TABLEHEADER),studentHeader=grader.querySelector(SELECTORS_STUDENTHEADER),leftOffset=getComputedStyle(studentHeader).getPropertyValue("left"),rightOffset=getComputedStyle(studentHeader).getPropertyValue("right");tableHeaders.forEach((tableHeader=>{if(tableHeader.colSpan>1){const addOffset=tableHeader.offsetWidth-studentHeader.offsetWidth;window.right_to_left()?tableHeader.style.right="calc("+rightOffset+" - "+addOffset+"px )":tableHeader.style.left="calc("+leftOffset+" - "+addOffset+"px )"}}))}};const defineLastRowIntersectionObserver=stickyFooterEnabled=>{const lastRow=document.querySelector(SELECTORS_LASTROW);if(!lastRow.classList.contains("userrow")){const stickyFooterHeight=stickyFooterEnabled?document.querySelector(_stickyFooter.SELECTORS.STICKYFOOTER).offsetHeight:null;new IntersectionObserver((_ref=>{let[e]=_ref;return lastRow.classList.toggle("pinned",e.intersectionRatio<1)}),{rootMargin:stickyFooterHeight?"0px 0px -".concat(stickyFooterHeight,"px 0px"):"0px",threshold:[1]}).observe(lastRow.querySelector("th"))}}}));
//# sourceMappingURL=stickycolspan.min.js.map
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("gradereport_grader/user",["exports","core_user/comboboxsearch/user","core/url","gradereport_grader/local/user/repository"],(function(_exports,_user,_url,Repository){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Allow the user to search for learners within the grader report.
*
* @module gradereport_grader/user
* @copyright 2023 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_user=_interopRequireDefault(_user),_url=_interopRequireDefault(_url),Repository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository);const selectors_component=".user-search",selectors_courseid='[data-region="courseid"]',courseID=document.querySelector(selectors_component).querySelector(selectors_courseid).dataset.courseid;class User extends _user.default{constructor(){super()}static init(){return new User}fetchDataset(){return Repository.userFetch(courseID).then((r=>r.users))}selectAllResultsLink(){return _url.default.relativeUrl("/grade/report/grader/index.php",{id:courseID,gpr_search:this.getSearchTerm()},!1)}selectOneLink(userID){return _url.default.relativeUrl("/grade/report/grader/index.php",{id:courseID,gpr_search:this.getSearchTerm(),gpr_userid:userID},!1)}}return _exports.default=User,_exports.default}));
//# sourceMappingURL=user.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"user.min.js","sources":["../src/user.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Allow the user to search for learners within the grader report.\n *\n * @module gradereport_grader/user\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport UserSearch from 'core_user/comboboxsearch/user';\nimport Url from 'core/url';\nimport * as Repository from 'gradereport_grader/local/user/repository';\n\n// Define our standard lookups.\nconst selectors = {\n component: '.user-search',\n courseid: '[data-region=\"courseid\"]',\n};\nconst component = document.querySelector(selectors.component);\nconst courseID = component.querySelector(selectors.courseid).dataset.courseid;\n\nexport default class User extends UserSearch {\n\n constructor() {\n super();\n }\n\n static init() {\n return new User();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n return Repository.userFetch(courseID).then((r) => r.users);\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm()\n }, false);\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm(),\n gpr_userid: userID,\n }, false);\n }\n}\n"],"names":["selectors","courseID","document","querySelector","dataset","courseid","User","UserSearch","constructor","fetchDataset","Repository","userFetch","then","r","users","selectAllResultsLink","Url","relativeUrl","id","gpr_search","this","getSearchTerm","selectOneLink","userID","gpr_userid"],"mappings":";;;;;;;q0BA2BMA,oBACS,eADTA,mBAEQ,2BAGRC,SADYC,SAASC,cAAcH,qBACdG,cAAcH,oBAAoBI,QAAQC,eAEhDC,aAAaC,cAE9BC,2CAKW,IAAIF,KAQfG,sBACWC,WAAWC,UAAUV,UAAUW,MAAMC,GAAMA,EAAEC,QAQxDC,8BACWC,aAAIC,YAAY,iCAAkC,CACrDC,GAAIjB,SACJkB,WAAYC,KAAKC,kBAClB,GASPC,cAAcC,eACHP,aAAIC,YAAY,iCAAkC,CACrDC,GAAIjB,SACJkB,WAAYC,KAAKC,gBACjBG,WAAYD,SACb"}
+574
View File
@@ -0,0 +1,574 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Allow the user to show and hide columns of the report at will.
*
* @module gradereport_grader/collapse
* @copyright 2023 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as Repository from 'gradereport_grader/collapse/repository';
import search_combobox from 'core/comboboxsearch/search_combobox';
import {renderForPromise, replaceNodeContents, replaceNode} from 'core/templates';
import {debounce} from 'core/utils';
import $ from 'jquery';
import {getStrings} from 'core/str';
import CustomEvents from "core/custom_interaction_events";
import storage from 'core/localstorage';
import {addIconToContainer} from 'core/loadingicon';
import Notification from 'core/notification';
import Pending from 'core/pending';
// Contain our selectors within this file until they could be of use elsewhere.
const selectors = {
component: '.collapse-columns',
formDropdown: '.columnsdropdownform',
formItems: {
cancel: 'cancel',
save: 'save',
checked: 'input[type="checkbox"]:checked',
currentlyUnchecked: 'input[type="checkbox"]:not([data-action="selectall"])',
},
hider: 'hide',
expand: 'expand',
colVal: '[data-col]',
itemVal: '[data-itemid]',
content: '[data-collapse="content"]',
sort: '[data-collapse="sort"]',
expandbutton: '[data-collapse="expandbutton"]',
rangerowcell: '[data-collapse="rangerowcell"]',
avgrowcell: '[data-collapse="avgrowcell"]',
menu: '[data-collapse="menu"]',
icons: '.data-collapse_gradeicons',
count: '[data-collapse="count"]',
placeholder: '.collapsecolumndropdown [data-region="placeholder"]',
fullDropdown: '.collapsecolumndropdown',
searchResultContainer: '.searchresultitemscontainer',
};
const countIndicator = document.querySelector(selectors.count);
export default class ColumnSearch extends search_combobox {
userID = -1;
courseID = null;
defaultSort = '';
nodes = [];
gradeStrings = null;
userStrings = null;
stringMap = [];
static init(userID, courseID, defaultSort) {
return new ColumnSearch(userID, courseID, defaultSort);
}
constructor(userID, courseID, defaultSort) {
super();
this.userID = userID;
this.courseID = courseID;
this.defaultSort = defaultSort;
this.component = document.querySelector(selectors.component);
const pendingPromise = new Pending();
// Display a loader whilst collapsing appropriate columns (based on the locally stored state for the current user).
addIconToContainer(document.querySelector('.gradeparent')).then((loader) => {
setTimeout(() => {
// Get the users' checked columns to change.
this.getDataset().forEach((item) => {
this.nodesUpdate(item);
});
this.renderDefault();
// Once the grade categories have been re-collapsed, remove the loader and display the Gradebook setup content.
loader.remove();
document.querySelector('.gradereport-grader-table').classList.remove('d-none');
}, 10);
}).then(() => pendingPromise.resolve()).catch(Notification.exception);
this.$component.on('hide.bs.dropdown', () => {
const searchResultContainer = this.component.querySelector(selectors.searchResultContainer);
searchResultContainer.scrollTop = 0;
// Use setTimeout to make sure the following code is executed after the click event is handled.
setTimeout(() => {
if (this.searchInput.value !== '') {
this.searchInput.value = '';
this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));
}
});
});
}
/**
* The overall div that contains the searching widget.
*
* @returns {string}
*/
componentSelector() {
return '.collapse-columns';
}
/**
* The dropdown div that contains the searching widget result space.
*
* @returns {string}
*/
dropdownSelector() {
return '.searchresultitemscontainer';
}
/**
* Return the dataset that we will be searching upon.
*
* @returns {Array}
*/
getDataset() {
if (!this.dataset) {
const cols = this.fetchDataset();
this.dataset = JSON.parse(cols) ? JSON.parse(cols).split(',') : [];
}
this.datasetSize = this.dataset.length;
return this.dataset;
}
/**
* Get the data we will be searching against in this component.
*
* @returns {string}
*/
fetchDataset() {
return storage.get(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`);
}
/**
* Given a user performs an action, update the users' preferences.
*/
setPreferences() {
storage.set(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`,
JSON.stringify(this.getDataset().join(','))
);
}
/**
* Register clickable event listeners.
*/
registerClickHandlers() {
// Register click events within the component.
this.component.addEventListener('click', this.clickHandler.bind(this));
document.addEventListener('click', this.docClickHandler.bind(this));
}
/**
* The handler for when a user interacts with the component.
*
* @param {MouseEvent} e The triggering event that we are working with.
*/
clickHandler(e) {
super.clickHandler(e);
// Prevent BS from closing the dropdown if they click elsewhere within the dropdown besides the form.
if (e.target.closest(selectors.fullDropdown)) {
e.stopPropagation();
}
}
/**
* Externally defined click function to improve memory handling.
*
* @param {MouseEvent} e
* @returns {Promise<void>}
*/
async docClickHandler(e) {
if (e.target.dataset.hider === selectors.hider) {
e.preventDefault();
const desiredToHide = e.target.closest(selectors.colVal) ?
e.target.closest(selectors.colVal)?.dataset.col :
e.target.closest(selectors.itemVal)?.dataset.itemid;
const idx = this.getDataset().indexOf(desiredToHide);
if (idx === -1) {
this.getDataset().push(desiredToHide);
}
await this.prefcountpipe();
this.nodesUpdate(desiredToHide);
}
if (e.target.closest('button')?.dataset.hider === selectors.expand) {
e.preventDefault();
const desiredToHide = e.target.closest(selectors.colVal) ?
e.target.closest(selectors.colVal)?.dataset.col :
e.target.closest(selectors.itemVal)?.dataset.itemid;
const idx = this.getDataset().indexOf(desiredToHide);
this.getDataset().splice(idx, 1);
await this.prefcountpipe();
this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.col);
this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.itemid);
}
}
/**
* Handle any keyboard inputs.
*/
registerInputEvents() {
// Register & handle the text input.
this.searchInput.addEventListener('input', debounce(async() => {
if (this.getSearchTerm() === this.searchInput.value && this.searchResultsVisible()) {
window.console.warn(`Search term matches input value - skipping`);
// Debounce can happen multiple times quickly.
return;
}
this.setSearchTerms(this.searchInput.value);
// We can also require a set amount of input before search.
if (this.searchInput.value === '') {
// Hide the "clear" search button in the search bar.
this.clearSearchButton.classList.add('d-none');
} else {
// Display the "clear" search button in the search bar.
this.clearSearchButton.classList.remove('d-none');
}
const pendingPromise = new Pending();
// User has given something for us to filter against.
await this.filterrenderpipe().then(() => {
pendingPromise.resolve();
return true;
});
}, 300, {pending: true}));
}
/**
* Handle the form submission within the dropdown.
*/
registerFormEvents() {
const form = this.component.querySelector(selectors.formDropdown);
const events = [
'click',
CustomEvents.events.activate,
CustomEvents.events.keyboardActivate
];
CustomEvents.define(document, events);
const selectall = form.querySelector('[data-action="selectall"]');
// Register clicks & keyboard form handling.
events.forEach((event) => {
const submitBtn = form.querySelector(`[data-action="${selectors.formItems.save}"`);
form.addEventListener(event, (e) => {
// Stop Bootstrap from being clever.
e.stopPropagation();
const input = e.target.closest('input');
if (input) {
// If the user is unchecking an item, we need to uncheck the select all if it's checked.
if (selectall.checked && !input.checked) {
selectall.checked = false;
}
const checkedCount = Array.from(form.querySelectorAll(selectors.formItems.checked)).length;
// Check if any are clicked or not then change disabled.
submitBtn.disabled = checkedCount <= 0;
}
}, false);
// Stop Bootstrap from being clever.
this.searchInput.addEventListener(event, e => e.stopPropagation());
this.clearSearchButton.addEventListener(event, async(e) => {
e.stopPropagation();
this.searchInput.value = '';
this.setSearchTerms(this.searchInput.value);
await this.filterrenderpipe();
});
selectall.addEventListener(event, (e) => {
// Stop Bootstrap from being clever.
e.stopPropagation();
if (!selectall.checked) {
const touncheck = Array.from(form.querySelectorAll(selectors.formItems.checked));
touncheck.forEach(item => {
item.checked = false;
});
submitBtn.disabled = true;
} else {
const currentUnchecked = Array.from(form.querySelectorAll(selectors.formItems.currentlyUnchecked));
currentUnchecked.forEach(item => {
item.checked = true;
});
submitBtn.disabled = false;
}
});
});
form.addEventListener('submit', async(e) => {
e.preventDefault();
if (e.submitter.dataset.action === selectors.formItems.cancel) {
$(this.component).dropdown('toggle');
return;
}
// Get the users' checked columns to change.
const checkedItems = [...form.elements].filter(item => item.checked);
checkedItems.forEach((item) => {
const idx = this.getDataset().indexOf(item.dataset.collapse);
this.getDataset().splice(idx, 1);
this.nodesUpdate(item.dataset.collapse);
});
// Reset the check all & submit to false just in case.
selectall.checked = false;
e.submitter.disabled = true;
await this.prefcountpipe();
});
}
nodesUpdate(item) {
const colNodesToHide = [...document.querySelectorAll(`[data-col="${item}"]`)];
const itemIDNodesToHide = [...document.querySelectorAll(`[data-itemid="${item}"]`)];
this.nodes = [...colNodesToHide, ...itemIDNodesToHide];
this.updateDisplay();
}
/**
* Update the user preferences, count display then render the results.
*
* @returns {Promise<void>}
*/
async prefcountpipe() {
this.setPreferences();
this.countUpdate();
await this.filterrenderpipe();
}
/**
* Dictate to the search component how and what we want to match upon.
*
* @param {Array} filterableData
* @returns {Array} An array of objects containing the system reference and the user readable value.
*/
async filterDataset(filterableData) {
const stringUserMap = await this.fetchRequiredUserStrings();
const stringGradeMap = await this.fetchRequiredGradeStrings();
// Custom user profile fields are not in our string map and need a bit of extra love.
const customFieldMap = this.fetchCustomFieldValues();
this.stringMap = new Map([...stringGradeMap, ...stringUserMap, ...customFieldMap]);
const searching = filterableData.map(s => {
const mapObj = this.stringMap.get(s);
if (mapObj === undefined) {
return {key: s, string: s};
}
return {
key: s,
string: mapObj.itemname ?? this.stringMap.get(s),
category: mapObj.category ?? '',
};
});
// Sometimes we just want to show everything.
if (this.getPreppedSearchTerm() === '') {
return searching;
}
// Other times we want to actually filter the content.
return searching.filter((col) => {
return col.string.toString().toLowerCase().includes(this.getPreppedSearchTerm());
});
}
/**
* Given we have a subset of the dataset, set the field that we matched upon to inform the end user.
*/
filterMatchDataset() {
this.setMatchedResults(
this.getMatchedResults().map((column) => {
return {
name: column.key,
displayName: column.string ?? column.key,
category: column.category ?? '',
};
})
);
}
/**
* With an array of nodes, switch their classes and values.
*/
updateDisplay() {
this.nodes.forEach((element) => {
const content = element.querySelector(selectors.content);
const sort = element.querySelector(selectors.sort);
const expandButton = element.querySelector(selectors.expandbutton);
const rangeRowCell = element.querySelector(selectors.rangerowcell);
const avgRowCell = element.querySelector(selectors.avgrowcell);
const nodeSet = [
element.querySelector(selectors.menu),
element.querySelector(selectors.icons),
content
];
// This can be further improved to reduce redundant similar calls.
if (element.classList.contains('cell')) {
// The column is actively being sorted, lets reset that and reload the page.
if (sort !== null) {
window.location = this.defaultSort;
}
if (content === null) {
// If it's not a content cell, it must be an overall average or a range cell.
const rowCell = avgRowCell ?? rangeRowCell;
rowCell?.classList.toggle('d-none');
} else if (content.classList.contains('d-none')) {
// We should always have content but some cells do not contain menus or other actions.
element.classList.remove('collapsed');
// If there are many nodes, apply the following.
if (content.childNodes.length > 1) {
content.classList.add('d-flex');
}
nodeSet.forEach(node => {
node?.classList.remove('d-none');
});
expandButton?.classList.add('d-none');
} else {
element.classList.add('collapsed');
content.classList.remove('d-flex');
nodeSet.forEach(node => {
node?.classList.add('d-none');
});
expandButton?.classList.remove('d-none');
}
}
});
}
/**
* Update the visual count of collapsed columns or hide the count all together.
*/
countUpdate() {
countIndicator.textContent = this.getDatasetSize();
if (this.getDatasetSize() > 0) {
this.component.parentElement.classList.add('d-flex');
this.component.parentElement.classList.remove('d-none');
} else {
this.component.parentElement.classList.remove('d-flex');
this.component.parentElement.classList.add('d-none');
}
}
/**
* Build the content then replace the node by default we want our form to exist.
*/
async renderDefault() {
this.setMatchedResults(await this.filterDataset(this.getDataset()));
this.filterMatchDataset();
// Update the collapsed button pill.
this.countUpdate();
const {html, js} = await renderForPromise('gradereport_grader/collapse/collapsebody', {
'instance': this.instance,
'results': this.getMatchedResults(),
'userid': this.userID,
});
replaceNode(selectors.placeholder, html, js);
this.updateNodes();
// Given we now have the body, we can set up more triggers.
this.registerFormEvents();
this.registerInputEvents();
// Add a small BS listener so that we can set the focus correctly on open.
this.$component.on('shown.bs.dropdown', () => {
this.searchInput.focus({preventScroll: true});
this.selectallEnable();
});
}
/**
* Build the content then replace the node.
*/
async renderDropdown() {
const {html, js} = await renderForPromise('gradereport_grader/collapse/collapseresults', {
instance: this.instance,
'results': this.getMatchedResults(),
'searchTerm': this.getSearchTerm(),
});
replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);
this.selectallEnable();
// Reset the expand button to be disabled as we have re-rendered the dropdown.
const form = this.component.querySelector(selectors.formDropdown);
const expandButton = form.querySelector(`[data-action="${selectors.formItems.save}"`);
expandButton.disabled = true;
}
/**
* Given we render the dropdown, Determine if we want to enable the select all checkbox.
*/
selectallEnable() {
const form = this.component.querySelector(selectors.formDropdown);
const selectall = form.querySelector('[data-action="selectall"]');
selectall.disabled = this.getMatchedResults().length === 0;
}
/**
* If we have any custom user profile fields, grab their system & readable names to add to our string map.
*
* @returns {array<string,*>} An array of associated string arrays ready for our map.
*/
fetchCustomFieldValues() {
const customFields = document.querySelectorAll('[data-collapse-name]');
// Cast from NodeList to array to grab all the values.
return [...customFields].map(field => [field.parentElement.dataset.col, field.dataset.collapseName]);
}
/**
* Given the set of profile fields we can possibly search, fetch their strings,
* so we can report to screen readers the field that matched.
*
* @returns {Promise<void>}
*/
fetchRequiredUserStrings() {
if (!this.userStrings) {
const requiredStrings = [
'username',
'firstname',
'lastname',
'email',
'city',
'country',
'department',
'institution',
'idnumber',
'phone1',
'phone2',
];
this.userStrings = getStrings(requiredStrings.map((key) => ({key})))
.then((stringArray) => new Map(
requiredStrings.map((key, index) => ([key, stringArray[index]]))
));
}
return this.userStrings;
}
/**
* Given the set of gradable items we can possibly search, fetch their strings,
* so we can report to screen readers the field that matched.
*
* @returns {Promise<void>}
*/
fetchRequiredGradeStrings() {
if (!this.gradeStrings) {
this.gradeStrings = Repository.gradeItems(this.courseID)
.then((result) => new Map(
result.gradeItems.map(key => ([key.id, key]))
));
}
return this.gradeStrings;
}
}
@@ -0,0 +1,41 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A repo for the collapsing in the grader report.
*
* @module gradereport_grader/collapse/repository
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ajax from 'core/ajax';
/**
* Fetch all the information on gradeitems we'll need in the column collapser.
*
* @method gradeItems
* @param {Number} courseid What course to fetch the gradeitems for
* @return {object} jQuery promise
*/
export const gradeItems = (courseid) => {
const request = {
methodname: 'core_grades_get_gradeitems',
args: {
courseid: courseid,
},
};
return ajax.call([request])[0];
};
@@ -0,0 +1,116 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Javascript module for displaying feedback in a modal window
*
* @module gradereport_grader/feedback_modal
* @copyright 2023 Kevin Percy <kevin.percy@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Modal from 'core/modal';
import Notification from 'core/notification';
import ajax from 'core/ajax';
import Templates from 'core/templates';
const Selectors = {
showFeedback: '[data-action="feedback"]'
};
/**
* Create the modal to display the feedback.
*
* @param {int} courseid
* @param {int} userid
* @param {int} itemid
* @returns {Promise}
*/
const getModal = async(courseid, userid, itemid) => {
let feedbackData;
try {
feedbackData = await fetchFeedback(courseid, userid, itemid);
} catch (e) {
return Promise.reject(e);
}
return Modal.create({
removeOnClose: true,
large: true,
verticallyCentered: true,
})
.then(modal => {
const body = Templates.render('core_grades/feedback_modal', {
feedbacktext: feedbackData.feedbacktext,
user: {
picture: feedbackData.picture,
fullname: feedbackData.fullname,
additionalfield: feedbackData.additionalfield,
},
});
modal.setBody(body);
modal.setTitle(feedbackData.title);
modal.show();
return modal;
});
};
/**
* Fetch the feedback data.
*
* @param {int} courseid
* @param {int} userid
* @param {int} itemid
* @returns {Promise}
*/
const fetchFeedback = (courseid, userid, itemid) => {
const request = {
methodname: 'core_grades_get_feedback',
args: {
courseid: courseid,
userid: userid,
itemid: itemid,
},
};
return ajax.call([request])[0];
};
/**
* Register event listeners for the View Feedback links.
*/
const registerEventListeners = () => {
document.addEventListener('click', e => {
const showFeedbackTrigger = e.target.closest(Selectors.showFeedback);
if (showFeedbackTrigger) {
e.preventDefault();
const courseid = showFeedbackTrigger.dataset.courseid;
const userid = e.target.closest('tr').dataset.uid;
const itemid = e.target.closest('td').dataset.itemid;
getModal(courseid, userid, itemid)
.catch(Notification.exception);
}
});
};
/**
* Initialize module
*/
export const init = () => {
registerEventListeners();
};
+58
View File
@@ -0,0 +1,58 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Allow the user to search for groups within the grader report.
*
* @module gradereport_grader/group
* @copyright 2023 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import GroupSearch from 'core_group/comboboxsearch/group';
import Url from 'core/url';
export default class Group extends GroupSearch {
courseID;
constructor() {
super();
// Define our standard lookups.
this.selectors = {...this.selectors,
courseid: '[data-region="courseid"]',
};
const component = document.querySelector(this.componentSelector());
this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;
}
static init() {
return new Group();
}
/**
* Build up the link that is dedicated to a particular result.
*
* @param {Number} groupID The ID of the group selected.
* @returns {string|*}
*/
selectOneLink(groupID) {
return Url.relativeUrl('/grade/report/grader/index.php', {
id: this.courseID,
groupsearchvalue: this.getSearchTerm(),
group: groupID,
}, false);
}
}
@@ -0,0 +1,41 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A repo for the search partial in the grader report.
*
* @module gradereport_grader/local/user/repository
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ajax from 'core/ajax';
/**
* Given a course ID, we want to fetch the learners within this report.
*
* @method userFetch
* @param {int} courseid ID of the course to fetch the users of.
* @return {object} jQuery promise
*/
export const userFetch = (courseid) => {
const request = {
methodname: 'gradereport_grader_get_users_in_report',
args: {
courseid: courseid,
},
};
return ajax.call([request])[0];
};
@@ -0,0 +1,104 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Javascript module for fixing the position of sticky headers with multiple colspans
*
* @module gradereport_grader/stickycolspan
* @copyright 2022 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import $ from 'jquery';
import {SELECTORS as stickyFooterSelectors, eventTypes as stickyFooterEvents} from 'core/sticky-footer';
const SELECTORS = {
GRADEPARENT: '.gradeparent',
STUDENTHEADER: '#studentheader',
TABLEHEADER: 'th.header',
BEHAT: 'body.behat-site',
USERDROPDOWN: '.userrow th .dropdown',
LASTROW: '.lastrow',
};
/**
* Initialize module
*/
export const init = () => {
// The sticky positioning attributed to the user column cells affects the stacking context and makes the dropdowns
// within these cells to be cut off. To solve this problem, whenever one of these action menus (dropdowns) is opened
// we need to manually bump up the z-index value of the parent container element and revert once closed.
$(SELECTORS.USERDROPDOWN).on('show.bs.dropdown hide.bs.dropdown', (e) => {
// The closest heading element has sticky positioning which affects the stacking context in this case.
e.target.closest(SELECTORS.TABLEHEADER).classList.toggle('actions-menu-active');
});
defineLastRowIntersectionObserver(true);
// Add an event listener to the sticky footer toggled event to re-define the average row intersection observer
// accordingly. This is needed as on narrow screens when scrolling vertically the sticky footer is enabled and
// disabled dynamically.
document.addEventListener(stickyFooterEvents.stickyFooterStateChanged, (e) => {
defineLastRowIntersectionObserver(e.detail.enabled);
});
if (!document.querySelector(SELECTORS.BEHAT)) {
const grader = document.querySelector(SELECTORS.GRADEPARENT);
const tableHeaders = grader.querySelectorAll(SELECTORS.TABLEHEADER);
const studentHeader = grader.querySelector(SELECTORS.STUDENTHEADER);
const leftOffset = getComputedStyle(studentHeader).getPropertyValue('left');
const rightOffset = getComputedStyle(studentHeader).getPropertyValue('right');
tableHeaders.forEach((tableHeader) => {
if (tableHeader.colSpan > 1) {
const addOffset = (tableHeader.offsetWidth - studentHeader.offsetWidth);
if (window.right_to_left()) {
tableHeader.style.right = 'calc(' + rightOffset + ' - ' + addOffset + 'px )';
} else {
tableHeader.style.left = 'calc(' + leftOffset + ' - ' + addOffset + 'px )';
}
}
});
}
};
/**
* Define the intersection observer that will make sure that the last row is properly pinned.
*
* In certain scenarios, such as when both 'Overall average' and 'Range' are set not to be shown in the Grader report,
* a user row will end up being the last row in the Grader report table. In this particular case, we want to avoid
* pinning the last row.
*
* @param {boolean} stickyFooterEnabled Whether the page shows a sticky footer or not.
*/
const defineLastRowIntersectionObserver = (stickyFooterEnabled) => {
const lastRow = document.querySelector(SELECTORS.LASTROW);
// Ensure that the last row is not a user row before defining the intersection observer.
if (!lastRow.classList.contains('userrow')) {
const stickyFooterHeight = stickyFooterEnabled ?
document.querySelector(stickyFooterSelectors.STICKYFOOTER).offsetHeight : null;
// Register an observer that will bump up the z-index value of the last row when it's pinned to prevent the row
// being cut-off by the user column cells or other components within the report table that have higher z-index
// values. If the page has a sticky footer, we need to make sure that the bottom root margin of the observer
// subtracts the height of the sticky footer to prevent the row being cut-off by the footer.
const intersectionObserver = new IntersectionObserver(
([e]) => lastRow.classList.toggle('pinned', e.intersectionRatio < 1),
{
rootMargin: stickyFooterHeight ? `0px 0px -${stickyFooterHeight}px 0px` : "0px",
threshold: [1]
}
);
intersectionObserver.observe(lastRow.querySelector('th'));
}
};
+79
View File
@@ -0,0 +1,79 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Allow the user to search for learners within the grader report.
*
* @module gradereport_grader/user
* @copyright 2023 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import UserSearch from 'core_user/comboboxsearch/user';
import Url from 'core/url';
import * as Repository from 'gradereport_grader/local/user/repository';
// Define our standard lookups.
const selectors = {
component: '.user-search',
courseid: '[data-region="courseid"]',
};
const component = document.querySelector(selectors.component);
const courseID = component.querySelector(selectors.courseid).dataset.courseid;
export default class User extends UserSearch {
constructor() {
super();
}
static init() {
return new User();
}
/**
* Get the data we will be searching against in this component.
*
* @returns {Promise<*>}
*/
fetchDataset() {
return Repository.userFetch(courseID).then((r) => r.users);
}
/**
* Build up the view all link.
*
* @returns {string|*}
*/
selectAllResultsLink() {
return Url.relativeUrl('/grade/report/grader/index.php', {
id: courseID,
gpr_search: this.getSearchTerm()
}, false);
}
/**
* Build up the link that is dedicated to a particular result.
*
* @param {Number} userID The ID of the user selected.
* @returns {string|*}
*/
selectOneLink(userID) {
return Url.relativeUrl('/grade/report/grader/index.php', {
id: courseID,
gpr_search: this.getSearchTerm(),
gpr_userid: userID,
}, false);
}
}