From 6b5154286e5e14127f820a78198a36a090750978 Mon Sep 17 00:00:00 2001 From: Simon Thornett Date: Tue, 3 Dec 2024 10:48:09 +0000 Subject: [PATCH] Further updates --- README.md | 9 ++- amd/build/dashboard.min.js | 11 ++- amd/build/dashboard.min.js.map | 2 +- amd/build/table_handler.min.js | 2 +- amd/build/table_handler.min.js.map | 2 +- amd/src/dashboard.js | 18 +++++ amd/src/table_handler.js | 11 ++- classes/frequency.php | 11 +-- classes/source_base.php | 31 ++++++-- .../classes/output/renderer.php | 2 +- ...ssessfreqreport_activities_in_progress.php | 4 -- .../assessfreqreport_activity_dashboard.php | 6 +- report/activity_dashboard/settings.php | 7 ++ report/heatmap/templates/filters.mustache | 2 +- .../classes/output/user_table.php | 2 +- source/assign/classes/output/user_table.php | 13 +++- source/assign/classes/source.php | 2 +- styles.css | 72 +++++++++++++++++++ templates/index.mustache | 5 +- 19 files changed, 182 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 68fe1e62..d3d20a77 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ has the following capabilities: - assessfreqreport/summary_graphs:view however each future subplugin can define their own access checks by using the abstract `has_access` method. -Accessing the reports from a course (link now added to the course context menu) will do the capability check at the +Accessing the reports from a course (link now added to the course report screen) will do the capability check at the course context level, otherwise system level will be used. The reports themselves should also be restricted based on the $PAGE->course if it is not the SITEID as this is set @@ -72,3 +72,10 @@ during the intial load of the index.php file. Each module is now a subplugin within the `source` directory The subplugins source class should extend from the \local_assessfreq\source_base class + + +Along with general performance improvements additional settings have been added to reduce the load on the reports: + +* Assign and quiz sources have a setting called "windowexclusion" which will allow the admin to specify a length of time to exclude long running assessments +* The activity dashboard has "trendlimit" and "trendcount" settings to reduce the number of trend records it attempts to load +* User tables have been update to only return users that have attempt data diff --git a/amd/build/dashboard.min.js b/amd/build/dashboard.min.js index 39f4b56f..eb2054c7 100644 --- a/amd/build/dashboard.min.js +++ b/amd/build/dashboard.min.js @@ -1,3 +1,12 @@ -define("local_assessfreq/dashboard",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=()=>{require(["core/form-autocomplete","core/str"],(function(Autocomplete,Str){Str.get_string("courseselect","local_assessfreq").then((loading=>{Autocomplete.enhance("#local-assessfreq-course-filter",!1,"local_assessfreq/course_selector",loading,!1,!0);document.getElementById("local-assessfreq-course-filter").addEventListener("change",(event=>{let courseid=event.target.value,url=new URL(window.location);url.searchParams.set("courseid",courseid),window.location=url}))}))})),tabs()};const tabs=()=>{document.getElementsByClassName("tablinks").forEach((el=>el.addEventListener("click",(event=>{let target=event.target.dataset.target,tabcontent=document.getElementsByClassName("tabcontent");for(let i=0;i1?urlParts[1]:null;anchor&&null!==document.querySelector('[data-target="tab-'+anchor+'"]')?document.querySelector('[data-target="tab-'+anchor+'"]').click():document.querySelector('[data-target="tab-heatmap"]').click()}})); +define("local_assessfreq/dashboard",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0, +/** + * Chart data JS module. + * + * @module local_assessfreq/dashboard + * @package + * @copyright Simon Thornett + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +globalThis.reports=0;_exports.init=()=>{require(["core/form-autocomplete","core/str"],(function(Autocomplete,Str){Str.get_string("courseselect","local_assessfreq").then((loading=>{Autocomplete.enhance("#local-assessfreq-course-filter",!1,"local_assessfreq/course_selector",loading,!1,!0);document.getElementById("local-assessfreq-course-filter").addEventListener("change",(event=>{let courseid=event.target.value,url=new URL(window.location);url.searchParams.set("courseid",courseid),window.location=url}))}))})),tabs(),window.setTimeout(loading,2e3)};const loading=()=>{let loaderwrapper=document.getElementById("loader-wrapper"),index=document.getElementById("local-assessfreq-index");0===globalThis.reports?(loaderwrapper.style.display="none",index.style.display="block"):window.setTimeout(loading,1e3)},tabs=()=>{document.getElementsByClassName("tablinks").forEach((el=>el.addEventListener("click",(event=>{let target=event.target.dataset.target,tabcontent=document.getElementsByClassName("tabcontent");for(let i=0;i1?urlParts[1]:null;anchor&&null!==document.querySelector('[data-target="tab-'+anchor+'"]')?document.querySelector('[data-target="tab-'+anchor+'"]').click():document.querySelector('[data-target="tab-heatmap"]').click()}})); //# sourceMappingURL=dashboard.min.js.map \ No newline at end of file diff --git a/amd/build/dashboard.min.js.map b/amd/build/dashboard.min.js.map index 3f33ccac..e15b07a5 100644 --- a/amd/build/dashboard.min.js.map +++ b/amd/build/dashboard.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dashboard.min.js","sources":["../src/dashboard.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 * Chart data JS module.\n *\n * @module local_assessfreq/dashboard\n * @package\n * @copyright Simon Thornett \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport const init = () => {\n\n // Create the course search filter.\n require(['core/form-autocomplete', 'core/str'], function(Autocomplete, Str) {\n Str.get_string('courseselect', 'local_assessfreq').then((loading) => {\n Autocomplete.enhance(\n '#local-assessfreq-course-filter',\n false,\n 'local_assessfreq/course_selector',\n loading,\n false,\n true,\n );\n const course_filter = document.getElementById(\"local-assessfreq-course-filter\");\n course_filter.addEventListener('change', event => {\n let courseid = event.target.value;\n let url = new URL(window.location);\n url.searchParams.set('courseid', courseid);\n window.location = url;\n });\n });\n });\n\n // Load the tab functionality.\n tabs();\n\n};\n\nconst tabs = () => {\n\n const tabcontent = document.getElementsByClassName(\"tablinks\");\n\n tabcontent.forEach(el => el.addEventListener('click', event => {\n let target = event.target.dataset.target;\n\n let tabcontent = document.getElementsByClassName(\"tabcontent\");\n for (let i = 0; i < tabcontent.length; i++) {\n tabcontent[i].style.display = \"none\";\n }\n\n // Get all elements with class=\"tablinks\" and remove the class \"active\"\n let tablinks = document.getElementsByClassName(\"tablinks\");\n for (let i = 0; i < tablinks.length; i++) {\n tablinks[i].className = tablinks[i].className.replace(\" active\", \"\");\n }\n\n // Show the current tab, and add an \"active\" class to the button that opened the tab\n document.getElementById(target).style.display = \"block\";\n event.currentTarget.className += \" active\";\n }));\n\n const currentUrl = document.URL;\n const urlParts = currentUrl.split('#');\n\n const anchor = (urlParts.length > 1) ? urlParts[1] : null;\n // First tab should be open by default unless we have an anchor.\n if (!anchor || document.querySelector('[data-target=\"tab-' + anchor + '\"]') === null) {\n document.querySelector('[data-target=\"tab-heatmap\"]').click();\n } else {\n document.querySelector('[data-target=\"tab-' + anchor + '\"]').click();\n }\n};\n"],"names":["require","Autocomplete","Str","get_string","then","loading","enhance","document","getElementById","addEventListener","event","courseid","target","value","url","URL","window","location","searchParams","set","tabs","getElementsByClassName","forEach","el","dataset","tabcontent","i","length","style","display","tablinks","className","replace","currentTarget","urlParts","split","anchor","querySelector","click"],"mappings":"+JAwBoB,KAGhBA,QAAQ,CAAC,yBAA0B,aAAa,SAASC,aAAcC,KACnEA,IAAIC,WAAW,eAAgB,oBAAoBC,MAAMC,UACrDJ,aAAaK,QACT,mCACA,EACA,mCACAD,SACA,GACA,GAEkBE,SAASC,eAAe,kCAChCC,iBAAiB,UAAUC,YACjCC,SAAWD,MAAME,OAAOC,MACxBC,IAAM,IAAIC,IAAIC,OAAOC,UACzBH,IAAII,aAAaC,IAAI,WAAYR,UACjCK,OAAOC,SAAWH,aAM9BM,cAIEA,KAAO,KAEUb,SAASc,uBAAuB,YAExCC,SAAQC,IAAMA,GAAGd,iBAAiB,SAASC,YAC9CE,OAASF,MAAME,OAAOY,QAAQZ,OAE9Ba,WAAalB,SAASc,uBAAuB,kBAC5C,IAAIK,EAAI,EAAGA,EAAID,WAAWE,OAAQD,IACnCD,WAAWC,GAAGE,MAAMC,QAAU,WAI9BC,SAAWvB,SAASc,uBAAuB,gBAC1C,IAAIK,EAAI,EAAGA,EAAII,SAASH,OAAQD,IACjCI,SAASJ,GAAGK,UAAYD,SAASJ,GAAGK,UAAUC,QAAQ,UAAW,IAIrEzB,SAASC,eAAeI,QAAQgB,MAAMC,QAAU,QAChDnB,MAAMuB,cAAcF,WAAa,qBAI/BG,SADa3B,SAASQ,IACAoB,MAAM,KAE5BC,OAAUF,SAASP,OAAS,EAAKO,SAAS,GAAK,KAEhDE,QAA2E,OAAjE7B,SAAS8B,cAAc,qBAAuBD,OAAS,MAGlE7B,SAAS8B,cAAc,qBAAuBD,OAAS,MAAME,QAF7D/B,SAAS8B,cAAc,+BAA+BC"} \ No newline at end of file +{"version":3,"file":"dashboard.min.js","sources":["../src/dashboard.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 * Chart data JS module.\n *\n * @module local_assessfreq/dashboard\n * @package\n * @copyright Simon Thornett \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nglobalThis.reports = 0;\n\nexport const init = () => {\n\n // Create the course search filter.\n require(['core/form-autocomplete', 'core/str'], function(Autocomplete, Str) {\n Str.get_string('courseselect', 'local_assessfreq').then((loading) => {\n Autocomplete.enhance(\n '#local-assessfreq-course-filter',\n false,\n 'local_assessfreq/course_selector',\n loading,\n false,\n true,\n );\n const course_filter = document.getElementById(\"local-assessfreq-course-filter\");\n course_filter.addEventListener('change', event => {\n let courseid = event.target.value;\n let url = new URL(window.location);\n url.searchParams.set('courseid', courseid);\n window.location = url;\n });\n });\n });\n\n // Load the tab functionality.\n tabs();\n\n // Load the loading page whilst we wait for the reports to complete.\n window.setTimeout(loading, 2000);\n};\n\nconst loading = () => {\n\n let loaderwrapper = document.getElementById('loader-wrapper');\n let index = document.getElementById('local-assessfreq-index');\n\n // No reports loading, then show the index page.\n if (globalThis.reports === 0) {\n loaderwrapper.style.display = 'none';\n index.style.display = 'block';\n } else {\n window.setTimeout(loading, 1000);\n }\n};\n\nconst tabs = () => {\n\n const tabcontent = document.getElementsByClassName(\"tablinks\");\n\n tabcontent.forEach(el => el.addEventListener('click', event => {\n let target = event.target.dataset.target;\n\n let tabcontent = document.getElementsByClassName(\"tabcontent\");\n for (let i = 0; i < tabcontent.length; i++) {\n tabcontent[i].style.display = \"none\";\n }\n\n // Get all elements with class=\"tablinks\" and remove the class \"active\"\n let tablinks = document.getElementsByClassName(\"tablinks\");\n for (let i = 0; i < tablinks.length; i++) {\n tablinks[i].className = tablinks[i].className.replace(\" active\", \"\");\n }\n\n // Show the current tab, and add an \"active\" class to the button that opened the tab\n document.getElementById(target).style.display = \"block\";\n event.currentTarget.className += \" active\";\n }));\n\n const currentUrl = document.URL;\n const urlParts = currentUrl.split('#');\n\n const anchor = (urlParts.length > 1) ? urlParts[1] : null;\n // First tab should be open by default unless we have an anchor.\n if (!anchor || document.querySelector('[data-target=\"tab-' + anchor + '\"]') === null) {\n document.querySelector('[data-target=\"tab-heatmap\"]').click();\n } else {\n document.querySelector('[data-target=\"tab-' + anchor + '\"]').click();\n }\n};\n"],"names":["globalThis","reports","require","Autocomplete","Str","get_string","then","loading","enhance","document","getElementById","addEventListener","event","courseid","target","value","url","URL","window","location","searchParams","set","tabs","setTimeout","loaderwrapper","index","style","display","getElementsByClassName","forEach","el","dataset","tabcontent","i","length","tablinks","className","replace","currentTarget","urlParts","split","anchor","querySelector","click"],"mappings":";;;;;;;;;AAwBAA,WAAWC,QAAU,gBAED,KAGhBC,QAAQ,CAAC,yBAA0B,aAAa,SAASC,aAAcC,KACnEA,IAAIC,WAAW,eAAgB,oBAAoBC,MAAMC,UACrDJ,aAAaK,QACT,mCACA,EACA,mCACAD,SACA,GACA,GAEkBE,SAASC,eAAe,kCAChCC,iBAAiB,UAAUC,YACjCC,SAAWD,MAAME,OAAOC,MACxBC,IAAM,IAAIC,IAAIC,OAAOC,UACzBH,IAAII,aAAaC,IAAI,WAAYR,UACjCK,OAAOC,SAAWH,aAM9BM,OAGAJ,OAAOK,WAAWhB,QAAS,YAGzBA,QAAU,SAERiB,cAAgBf,SAASC,eAAe,kBACxCe,MAAQhB,SAASC,eAAe,0BAGT,IAAvBV,WAAWC,SACXuB,cAAcE,MAAMC,QAAU,OAC9BF,MAAMC,MAAMC,QAAU,SAEtBT,OAAOK,WAAWhB,QAAS,MAI7Be,KAAO,KAEUb,SAASmB,uBAAuB,YAExCC,SAAQC,IAAMA,GAAGnB,iBAAiB,SAASC,YAC9CE,OAASF,MAAME,OAAOiB,QAAQjB,OAE9BkB,WAAavB,SAASmB,uBAAuB,kBAC5C,IAAIK,EAAI,EAAGA,EAAID,WAAWE,OAAQD,IACnCD,WAAWC,GAAGP,MAAMC,QAAU,WAI9BQ,SAAW1B,SAASmB,uBAAuB,gBAC1C,IAAIK,EAAI,EAAGA,EAAIE,SAASD,OAAQD,IACjCE,SAASF,GAAGG,UAAYD,SAASF,GAAGG,UAAUC,QAAQ,UAAW,IAIrE5B,SAASC,eAAeI,QAAQY,MAAMC,QAAU,QAChDf,MAAM0B,cAAcF,WAAa,qBAI/BG,SADa9B,SAASQ,IACAuB,MAAM,KAE5BC,OAAUF,SAASL,OAAS,EAAKK,SAAS,GAAK,KAEhDE,QAA2E,OAAjEhC,SAASiC,cAAc,qBAAuBD,OAAS,MAGlEhC,SAASiC,cAAc,qBAAuBD,OAAS,MAAME,QAF7DlC,SAASiC,cAAc,+BAA+BC"} \ No newline at end of file diff --git a/amd/build/table_handler.min.js b/amd/build/table_handler.min.js index 674d05ff..3ca3a6a0 100644 --- a/amd/build/table_handler.min.js +++ b/amd/build/table_handler.min.js @@ -6,6 +6,6 @@ define("local_assessfreq/table_handler",["exports","core/ajax","core/fragment"," * @package * @copyright 2020 Guillermo Gomez * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=_interopRequireDefault(_ajax),_fragment=_interopRequireDefault(_fragment),_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates),Debouncer=_interopRequireWildcard(Debouncer),_override_modal=_interopRequireDefault(_override_modal),UserPreference=_interopRequireWildcard(UserPreference);return _exports.default=class{constructor(activity,context,tableElementId,tableFragmentComponent,tableFragmentValue,tableRowPreference,tableSortPreference,tableSearchElement){let tableId=arguments.length>8&&void 0!==arguments[8]?arguments[8]:null,tableMethodName=arguments.length>9&&void 0!==arguments[9]?arguments[9]:null;this.activityId=activity,this.contextId=context,this.elementId=tableElementId,this.fragmentComponent=tableFragmentComponent,this.fragmentValue=tableFragmentValue,this.rowPreference=tableRowPreference,this.sortPreference=tableSortPreference,this.searchElement=tableSearchElement,this.id=tableId,this.methodName=tableMethodName,this.overridden=!1}getTable=(()=>{var _this=this;return function(){let page=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;_this.overridden=!1;let search=document.getElementById(_this.searchElement).value.trim(),tableElement=document.getElementById(_this.elementId),spinner=tableElement.getElementsByClassName("overlay-icon-container")[0],tableBody=tableElement.getElementsByClassName("table-body")[0],values={search:search,page:page};_this.activityId>0&&(values.activityid=_this.activityId);let params={data:JSON.stringify(values)};spinner.classList.remove("hide"),_fragment.default.loadFragment(_this.fragmentComponent,_this.fragmentValue,_this.contextId,params).done(((response,js)=>{tableBody.innerHTML=response,js&&_templates.default.runTemplateJS(js),spinner.classList.add("hide"),_this.tableEventListeners()})).fail((()=>{_notification.default.exception(new Error("Failed to update table."))}))}})();debounceTable=Debouncer.debouncer((()=>{this.getTable()}),750);tableSort=event=>{event.preventDefault();let sortArray={};const linkUrl=new URL(event.target.closest("a").href),targetSortBy=linkUrl.searchParams.get("tsort");let targetSortOrder=linkUrl.searchParams.get("tdir");""===targetSortOrder&&(targetSortOrder="4"),sortArray[targetSortBy]=targetSortOrder,_ajax.default.call([{methodname:this.methodName,args:{tableid:this.id,preference:"sortby",values:JSON.stringify(sortArray)}}])[0].then((()=>{this.getTable()}))};tableHide=event=>{event.preventDefault();let hideArray={};const linkUrl=new URL(event.target.closest("a").href),links=document.getElementById(this.elementId).querySelectorAll("a");let targetAction,targetColumn,action,column;-1!==linkUrl.search.indexOf("thide")?(targetAction="hide",targetColumn=linkUrl.searchParams.get("thide")):(targetAction="show",targetColumn=linkUrl.searchParams.get("tshow"));for(let i=0;i{this.getTable()}))};tableReset=event=>{event.preventDefault(),_ajax.default.call([{methodname:this.methodName,args:{tableid:this.id,preference:"reset",values:JSON.stringify({})}}])[0].then((()=>{this.getTable()}))};tableSearch=event=>"Meta"!==event.key&&!event.ctrlKey&&((0===event.target.value.length||event.target.value.length>2)&&this.debounceTable(),!0);tableSearchReset=()=>{let tableSearchInputElement=document.getElementById(this.searchElement);tableSearchInputElement.value="",tableSearchInputElement.focus(),this.getTable()};tableSearchRowSet=event=>{if(event.preventDefault(),"a"===event.target.tagName.toLowerCase()){let rows=event.target.dataset.metric;UserPreference.setUserPreference(this.rowPreference,rows).then((()=>{this.getTable()})).fail((()=>{_notification.default.exception(new Error("Failed to update user preference: rows"))}))}};tableNav=event=>{event.preventDefault();const page=new URL(event.target.closest("a").href).searchParams.get("page");page&&this.getTable(page)};tableSortButtonAction=event=>{event.preventDefault();var element=event.target;if("a"===element.tagName.toLowerCase()&&element.dataset.sort!==this.sortValue){this.sortValue=element.dataset.sort;let links=element.parentNode.getElementsByTagName("a");for(let i=0;i{const tableElement=document.getElementById(this.elementId),links=tableElement.querySelectorAll("a"),resetLink=tableElement.getElementsByClassName("resettable"),overrideLinks=tableElement.getElementsByClassName("action-icon override"),disabledLinks=tableElement.getElementsByClassName("action-icon disabled"),tableNavElement=tableElement.querySelectorAll("nav");for(let i=0;i0&&resetLink[0].addEventListener("click",this.tableReset);for(let i=0;i{event.preventDefault()}));tableNavElement.forEach((navElement=>{navElement.addEventListener("click",this.tableNav)}))};triggerOverrideModal=event=>{event.preventDefault();let userid=event.target.closest("a").id.substring(25);if(userid.includes("-")){let elements=userid.split("-");this.activityId=elements.pop(),userid=elements.pop()}_override_modal.default.displayModalForm(this.activityId,userid,this.hoursFilter)}},_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=_interopRequireDefault(_ajax),_fragment=_interopRequireDefault(_fragment),_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates),Debouncer=_interopRequireWildcard(Debouncer),_override_modal=_interopRequireDefault(_override_modal),UserPreference=_interopRequireWildcard(UserPreference);return _exports.default=class{constructor(activity,context,tableElementId,tableFragmentComponent,tableFragmentValue,tableRowPreference,tableSortPreference,tableSearchElement){let tableId=arguments.length>8&&void 0!==arguments[8]?arguments[8]:null,tableMethodName=arguments.length>9&&void 0!==arguments[9]?arguments[9]:null;this.activityId=activity,this.contextId=context,this.elementId=tableElementId,this.fragmentComponent=tableFragmentComponent,this.fragmentValue=tableFragmentValue,this.rowPreference=tableRowPreference,this.sortPreference=tableSortPreference,this.searchElement=tableSearchElement,this.id=tableId,this.methodName=tableMethodName,this.overridden=!1}getTable=(()=>{var _this=this;return function(){let page=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;globalThis.reports++,_this.overridden=!1;let search=document.getElementById(_this.searchElement).value.trim(),tableElement=document.getElementById(_this.elementId),spinner=tableElement.getElementsByClassName("overlay-icon-container")[0],tableBody=tableElement.getElementsByClassName("table-body")[0],values={search:search,page:page};_this.activityId>0&&(values.activityid=_this.activityId);let params={data:JSON.stringify(values)};spinner.classList.remove("hide"),_fragment.default.loadFragment(_this.fragmentComponent,_this.fragmentValue,_this.contextId,params).done(((response,js)=>{tableBody.innerHTML=response,js&&_templates.default.runTemplateJS(js),spinner.classList.add("hide"),_this.tableEventListeners(),globalThis.reports--})).fail((()=>{globalThis.reports--,_notification.default.exception(new Error("Failed to update table."))}))}})();debounceTable=Debouncer.debouncer((()=>{this.getTable()}),750);tableSort=event=>{event.preventDefault();let sortArray={};const linkUrl=new URL(event.target.closest("a").href),targetSortBy=linkUrl.searchParams.get("tsort");let targetSortOrder=linkUrl.searchParams.get("tdir");""===targetSortOrder&&(targetSortOrder="4"),sortArray[targetSortBy]=targetSortOrder,_ajax.default.call([{methodname:this.methodName,args:{tableid:this.id,preference:"sortby",values:JSON.stringify(sortArray)}}])[0].then((()=>{this.getTable()}))};tableHide=event=>{event.preventDefault();let hideArray={};const linkUrl=new URL(event.target.closest("a").href),links=document.getElementById(this.elementId).querySelectorAll("a");let targetAction,targetColumn,action,column;-1!==linkUrl.search.indexOf("thide")?(targetAction="hide",targetColumn=linkUrl.searchParams.get("thide")):(targetAction="show",targetColumn=linkUrl.searchParams.get("tshow"));for(let i=0;i{this.getTable()}))};tableReset=event=>{event.preventDefault(),_ajax.default.call([{methodname:this.methodName,args:{tableid:this.id,preference:"reset",values:JSON.stringify({})}}])[0].then((()=>{this.getTable()}))};tableSearch=event=>"Meta"!==event.key&&!event.ctrlKey&&((0===event.target.value.length||event.target.value.length>2)&&this.debounceTable(),!0);tableSearchReset=()=>{let tableSearchInputElement=document.getElementById(this.searchElement);tableSearchInputElement.value="",tableSearchInputElement.focus(),this.getTable()};tableSearchRowSet=event=>{if(event.preventDefault(),"a"===event.target.tagName.toLowerCase()){let rows=event.target.dataset.metric;UserPreference.setUserPreference(this.rowPreference,rows).then((()=>{this.getTable()})).fail((()=>{_notification.default.exception(new Error("Failed to update user preference: rows"))}))}};tableNav=event=>{event.preventDefault();const page=new URL(event.target.closest("a").href).searchParams.get("page");page&&this.getTable(page)};tableSortButtonAction=event=>{event.preventDefault();var element=event.target;if("a"===element.tagName.toLowerCase()&&element.dataset.sort!==this.sortValue){this.sortValue=element.dataset.sort;let links=element.parentNode.getElementsByTagName("a");for(let i=0;i{const tableElement=document.getElementById(this.elementId),links=tableElement.querySelectorAll("a"),resetLink=tableElement.getElementsByClassName("resettable"),overrideLinks=tableElement.getElementsByClassName("action-icon override"),disabledLinks=tableElement.getElementsByClassName("action-icon disabled"),tableNavElement=tableElement.querySelectorAll("nav");for(let i=0;i0&&resetLink[0].addEventListener("click",this.tableReset);for(let i=0;i{event.preventDefault()}));tableNavElement.forEach((navElement=>{navElement.addEventListener("click",this.tableNav)}))};triggerOverrideModal=event=>{event.preventDefault();let userid=event.target.closest("a").id.substring(25);if(userid.includes("-")){let elements=userid.split("-");this.activityId=elements.pop(),userid=elements.pop()}_override_modal.default.displayModalForm(this.activityId,userid,this.hoursFilter)}},_exports.default})); //# sourceMappingURL=table_handler.min.js.map \ No newline at end of file diff --git a/amd/build/table_handler.min.js.map b/amd/build/table_handler.min.js.map index 3cb88dd6..7c82789d 100644 --- a/amd/build/table_handler.min.js.map +++ b/amd/build/table_handler.min.js.map @@ -1 +1 @@ -{"version":3,"file":"table_handler.min.js","sources":["../src/table_handler.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 * Table handler JS module.\n *\n * @module local_assessfreq/table_handler\n * @package\n * @copyright 2020 Guillermo Gomez \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Fragment from 'core/fragment';\nimport Notification from 'core/notification';\nimport Templates from 'core/templates';\nimport * as Debouncer from 'local_assessfreq/debouncer';\nimport OverrideModal from 'local_assessfreq/override_modal';\nimport * as UserPreference from 'local_assessfreq/user_preferences';\n\nexport default class TableHandler {\n\n constructor(activity,\n context,\n tableElementId,\n tableFragmentComponent,\n tableFragmentValue,\n tableRowPreference,\n tableSortPreference,\n tableSearchElement,\n tableId = null,\n tableMethodName = null) {\n this.activityId = activity;\n this.contextId = context;\n this.elementId = tableElementId;\n this.fragmentComponent = tableFragmentComponent;\n this.fragmentValue = tableFragmentValue;\n this.rowPreference = tableRowPreference;\n this.sortPreference = tableSortPreference;\n this.searchElement = tableSearchElement;\n this.id = tableId;\n this.methodName = tableMethodName;\n this.overridden = false;\n }\n\n /**\n * Display the table that contains all the students in the exam as well as their attempts.\n *\n * @param {int|string|null} page Page number.\n */\n getTable = (page = 0) => {\n this.overridden = false;\n\n let search = document.getElementById(this.searchElement).value.trim();\n let tableElement = document.getElementById(this.elementId);\n let spinner = tableElement.getElementsByClassName('overlay-icon-container')[0];\n let tableBody = tableElement.getElementsByClassName('table-body')[0];\n let values = {'search': search, 'page': page};\n\n // Add values to Object depending on dashboard type.\n if (this.activityId > 0) {\n values.activityid = this.activityId;\n }\n\n let params = {'data': JSON.stringify(values)};\n\n spinner.classList.remove('hide'); // Show spinner if not already shown.\n Fragment.loadFragment(this.fragmentComponent, this.fragmentValue, this.contextId, params)\n .done((response, js) => {\n tableBody.innerHTML = response;\n if (js) {\n Templates.runTemplateJS(js); // Magic call the initialises JS from template included in response template HTML.\n }\n spinner.classList.add('hide');\n this.tableEventListeners(); // Re-add table event listeners.\n\n }).fail(() => {\n Notification.exception(new Error('Failed to update table.'));\n });\n };\n\n /**\n * This stops the ajax method that updates the table from being updated\n * while the user is still checking options.\n *\n */\n debounceTable = Debouncer.debouncer(() => {\n this.getTable();\n }, 750);\n\n /**\n * Process the sort click events from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableSort = (event) => {\n event.preventDefault();\n\n let sortArray = {};\n const linkUrl = new URL(event.target.closest('a').href);\n const targetSortBy = linkUrl.searchParams.get('tsort');\n let targetSortOrder = linkUrl.searchParams.get('tdir');\n\n // We want to flip the clicked column.\n if (targetSortOrder === '') {\n targetSortOrder = \"4\";\n }\n\n sortArray[targetSortBy] = targetSortOrder;\n\n // Set option via ajax.\n // eslint-disable-next-line promise/catch-or-return\n Ajax.call([{\n methodname: this.methodName,\n args: {\n tableid: this.id,\n preference: 'sortby',\n values: JSON.stringify(sortArray)\n },\n // eslint-disable-next-line promise/always-return\n }])[0].then(() => {\n this.getTable(); // Reload the table.\n });\n\n };\n\n /**\n * Process the sort click events from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableHide = (event) => {\n event.preventDefault();\n\n let hideArray = {};\n const linkUrl = new URL(event.target.closest('a').href);\n const tableElement = document.getElementById(this.elementId);\n const links = tableElement.querySelectorAll('a');\n let targetAction;\n let targetColumn;\n let action;\n let column;\n\n if (linkUrl.search.indexOf('thide') !== -1) {\n targetAction = 'hide';\n targetColumn = linkUrl.searchParams.get('thide');\n } else {\n targetAction = 'show';\n targetColumn = linkUrl.searchParams.get('tshow');\n }\n\n for (let i = 0; i < links.length; i++) {\n let hideLinkUrl = new URL(links[i].href);\n if (hideLinkUrl.search.indexOf('thide') !== -1) {\n action = 'hide';\n column = hideLinkUrl.searchParams.get('thide');\n } else {\n action = 'show';\n column = hideLinkUrl.searchParams.get('tshow');\n }\n\n if (action === 'show') {\n hideArray[column] = 1;\n }\n }\n\n hideArray[targetColumn] = (targetAction === 'hide') ? 1 : 0; // We want to flip the clicked column.\n\n // Set option via ajax.\n // eslint-disable-next-line promise/catch-or-return\n Ajax.call([{\n methodname: this.methodName,\n args: {\n tableid: this.id,\n preference: 'collapse',\n values: JSON.stringify(hideArray)\n },\n // eslint-disable-next-line promise/always-return\n }])[0].then(() => {\n this.getTable(); // Reload the table.\n });\n\n };\n\n /**\n * Process the reset click event from the table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableReset = (event) => {\n event.preventDefault();\n\n // Set option via ajax.\n // eslint-disable-next-line promise/catch-or-return\n Ajax.call([{\n methodname: this.methodName,\n args: {\n tableid: this.id,\n preference: 'reset',\n values: JSON.stringify({})\n },\n // eslint-disable-next-line promise/always-return\n }])[0].then(() => {\n this.getTable(); // Reload the table.\n });\n\n };\n\n /**\n * Process the search events from the student table.\n *\n * @param {Event} event\n * @return {Boolean}\n */\n tableSearch = (event) => {\n if (event.key === 'Meta' || event.ctrlKey) {\n return false;\n }\n\n if (event.target.value.length === 0 || event.target.value.length > 2) {\n this.debounceTable();\n }\n return true;\n };\n\n /**\n * Process the search reset click event from the student table.\n *\n */\n tableSearchReset = () => {\n let tableSearchInputElement = document.getElementById(this.searchElement);\n tableSearchInputElement.value = '';\n tableSearchInputElement.focus();\n this.getTable();\n };\n\n /**\n * Process the row set event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableSearchRowSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let rows = event.target.dataset.metric;\n UserPreference.setUserPreference(this.rowPreference, rows)\n // eslint-disable-next-line promise/always-return\n .then(() => {\n this.getTable(); // Reload the table.\n })\n .fail(() => {\n Notification.exception(new Error('Failed to update user preference: rows'));\n });\n }\n };\n\n /**\n * Process the nav event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableNav = (event) => {\n event.preventDefault();\n\n const linkUrl = new URL(event.target.closest('a').href);\n const page = linkUrl.searchParams.get('page');\n\n if (page) {\n this.getTable(page);\n }\n };\n\n /**\n * Get and process the selected assessment metric from the dropdown for the heatmap display,\n * and update the corresponding user preference.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableSortButtonAction = (event) => {\n event.preventDefault();\n var element = event.target;\n\n if (element.tagName.toLowerCase() === 'a' && element.dataset.sort !== this.sortValue) {\n this.sortValue = element.dataset.sort;\n\n let links = element.parentNode.getElementsByTagName('a');\n for (let i = 0; i < links.length; i++) {\n links[i].classList.remove('active');\n }\n\n element.classList.add('active');\n\n // Save selection as a user preference.\n UserPreference.setUserPreference(this.sortPreference, this.sortValue);\n\n this.debounceTable(); // Call function to update table.\n }\n };\n\n /**\n * Re-add event listeners when the student table is updated.\n */\n tableEventListeners = () => {\n const tableElement = document.getElementById(this.elementId);\n const links = tableElement.querySelectorAll('a');\n const resetLink = tableElement.getElementsByClassName('resettable');\n const overrideLinks = tableElement.getElementsByClassName('action-icon override');\n const disabledLinks = tableElement.getElementsByClassName('action-icon disabled');\n const tableNavElement = tableElement.querySelectorAll('nav'); // There are two nav paging elements per table.\n\n for (let i = 0; i < links.length; i++) {\n let linkUrl = new URL(links[i].href);\n if (linkUrl.search.indexOf('thide') !== -1 || linkUrl.search.indexOf('tshow') !== -1) {\n links[i].addEventListener('click', this.tableHide);\n } else if (linkUrl.search.indexOf('tsort') !== -1) {\n links[i].addEventListener('click', this.tableSort);\n }\n }\n\n if (resetLink.length > 0) {\n resetLink[0].addEventListener('click', this.tableReset);\n }\n\n for (let i = 0; i < overrideLinks.length; i++) {\n overrideLinks[i].addEventListener('click', this.triggerOverrideModal);\n }\n\n for (let i = 0; i < disabledLinks.length; i++) {\n disabledLinks[i].addEventListener('click', (event) => {\n event.preventDefault();\n });\n }\n\n tableNavElement.forEach((navElement) => {\n navElement.addEventListener('click', this.tableNav);\n });\n };\n\n /**\n * Trigger the override modal form. Thin wrapper to add extra data to click event.\n *\n * @param {Event} event The triggered event for the element.\n */\n triggerOverrideModal = (event) => {\n event.preventDefault();\n let userid = event.target.closest('a').id.substring(25);\n if (userid.includes('-')) {\n let elements = userid.split('-');\n this.activityId = elements.pop();\n userid = elements.pop();\n }\n\n OverrideModal.displayModalForm(this.activityId, userid, this.hoursFilter);\n };\n}\n"],"names":["constructor","activity","context","tableElementId","tableFragmentComponent","tableFragmentValue","tableRowPreference","tableSortPreference","tableSearchElement","tableId","tableMethodName","activityId","contextId","elementId","fragmentComponent","fragmentValue","rowPreference","sortPreference","searchElement","id","methodName","overridden","getTable","page","_this","search","document","getElementById","value","trim","tableElement","spinner","getElementsByClassName","tableBody","values","activityid","params","JSON","stringify","classList","remove","loadFragment","done","response","js","innerHTML","runTemplateJS","add","tableEventListeners","fail","exception","Error","debounceTable","Debouncer","debouncer","tableSort","event","preventDefault","sortArray","linkUrl","URL","target","closest","href","targetSortBy","searchParams","get","targetSortOrder","call","methodname","this","args","tableid","preference","then","tableHide","hideArray","links","querySelectorAll","targetAction","targetColumn","action","column","indexOf","i","length","hideLinkUrl","tableReset","tableSearch","key","ctrlKey","tableSearchReset","tableSearchInputElement","focus","tableSearchRowSet","tagName","toLowerCase","rows","dataset","metric","UserPreference","setUserPreference","tableNav","tableSortButtonAction","element","sort","sortValue","parentNode","getElementsByTagName","resetLink","overrideLinks","disabledLinks","tableNavElement","addEventListener","triggerOverrideModal","forEach","navElement","userid","substring","includes","elements","split","pop","displayModalForm","hoursFilter"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"table_handler.min.js","sources":["../src/table_handler.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 * Table handler JS module.\n *\n * @module local_assessfreq/table_handler\n * @package\n * @copyright 2020 Guillermo Gomez \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Fragment from 'core/fragment';\nimport Notification from 'core/notification';\nimport Templates from 'core/templates';\nimport * as Debouncer from 'local_assessfreq/debouncer';\nimport OverrideModal from 'local_assessfreq/override_modal';\nimport * as UserPreference from 'local_assessfreq/user_preferences';\n\nexport default class TableHandler {\n\n constructor(activity,\n context,\n tableElementId,\n tableFragmentComponent,\n tableFragmentValue,\n tableRowPreference,\n tableSortPreference,\n tableSearchElement,\n tableId = null,\n tableMethodName = null) {\n this.activityId = activity;\n this.contextId = context;\n this.elementId = tableElementId;\n this.fragmentComponent = tableFragmentComponent;\n this.fragmentValue = tableFragmentValue;\n this.rowPreference = tableRowPreference;\n this.sortPreference = tableSortPreference;\n this.searchElement = tableSearchElement;\n this.id = tableId;\n this.methodName = tableMethodName;\n this.overridden = false;\n }\n\n /**\n * Display the table that contains all the students in the exam as well as their attempts.\n *\n * @param {int|string|null} page Page number.\n */\n getTable = (page = 0) => {\n\n globalThis.reports++;\n\n this.overridden = false;\n\n let search = document.getElementById(this.searchElement).value.trim();\n let tableElement = document.getElementById(this.elementId);\n let spinner = tableElement.getElementsByClassName('overlay-icon-container')[0];\n let tableBody = tableElement.getElementsByClassName('table-body')[0];\n let values = {'search': search, 'page': page};\n\n // Add values to Object depending on dashboard type.\n if (this.activityId > 0) {\n values.activityid = this.activityId;\n }\n\n let params = {'data': JSON.stringify(values)};\n\n spinner.classList.remove('hide'); // Show spinner if not already shown.\n Fragment.loadFragment(this.fragmentComponent, this.fragmentValue, this.contextId, params)\n .done((response, js) => {\n tableBody.innerHTML = response;\n if (js) {\n Templates.runTemplateJS(js); // Magic call the initialises JS from template included in response template HTML.\n }\n spinner.classList.add('hide');\n this.tableEventListeners(); // Re-add table event listeners.\n globalThis.reports--;\n })\n .fail(() => {\n globalThis.reports--;\n Notification.exception(new Error('Failed to update table.'));\n });\n };\n\n /**\n * This stops the ajax method that updates the table from being updated\n * while the user is still checking options.\n *\n */\n debounceTable = Debouncer.debouncer(() => {\n this.getTable();\n }, 750);\n\n /**\n * Process the sort click events from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableSort = (event) => {\n event.preventDefault();\n\n let sortArray = {};\n const linkUrl = new URL(event.target.closest('a').href);\n const targetSortBy = linkUrl.searchParams.get('tsort');\n let targetSortOrder = linkUrl.searchParams.get('tdir');\n\n // We want to flip the clicked column.\n if (targetSortOrder === '') {\n targetSortOrder = \"4\";\n }\n\n sortArray[targetSortBy] = targetSortOrder;\n\n // Set option via ajax.\n // eslint-disable-next-line promise/catch-or-return\n Ajax.call([{\n methodname: this.methodName,\n args: {\n tableid: this.id,\n preference: 'sortby',\n values: JSON.stringify(sortArray)\n },\n // eslint-disable-next-line promise/always-return\n }])[0].then(() => {\n this.getTable(); // Reload the table.\n });\n\n };\n\n /**\n * Process the sort click events from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableHide = (event) => {\n event.preventDefault();\n\n let hideArray = {};\n const linkUrl = new URL(event.target.closest('a').href);\n const tableElement = document.getElementById(this.elementId);\n const links = tableElement.querySelectorAll('a');\n let targetAction;\n let targetColumn;\n let action;\n let column;\n\n if (linkUrl.search.indexOf('thide') !== -1) {\n targetAction = 'hide';\n targetColumn = linkUrl.searchParams.get('thide');\n } else {\n targetAction = 'show';\n targetColumn = linkUrl.searchParams.get('tshow');\n }\n\n for (let i = 0; i < links.length; i++) {\n let hideLinkUrl = new URL(links[i].href);\n if (hideLinkUrl.search.indexOf('thide') !== -1) {\n action = 'hide';\n column = hideLinkUrl.searchParams.get('thide');\n } else {\n action = 'show';\n column = hideLinkUrl.searchParams.get('tshow');\n }\n\n if (action === 'show') {\n hideArray[column] = 1;\n }\n }\n\n hideArray[targetColumn] = (targetAction === 'hide') ? 1 : 0; // We want to flip the clicked column.\n\n // Set option via ajax.\n // eslint-disable-next-line promise/catch-or-return\n Ajax.call([{\n methodname: this.methodName,\n args: {\n tableid: this.id,\n preference: 'collapse',\n values: JSON.stringify(hideArray)\n },\n // eslint-disable-next-line promise/always-return\n }])[0].then(() => {\n this.getTable(); // Reload the table.\n });\n\n };\n\n /**\n * Process the reset click event from the table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableReset = (event) => {\n event.preventDefault();\n\n // Set option via ajax.\n // eslint-disable-next-line promise/catch-or-return\n Ajax.call([{\n methodname: this.methodName,\n args: {\n tableid: this.id,\n preference: 'reset',\n values: JSON.stringify({})\n },\n // eslint-disable-next-line promise/always-return\n }])[0].then(() => {\n this.getTable(); // Reload the table.\n });\n\n };\n\n /**\n * Process the search events from the student table.\n *\n * @param {Event} event\n * @return {Boolean}\n */\n tableSearch = (event) => {\n if (event.key === 'Meta' || event.ctrlKey) {\n return false;\n }\n\n if (event.target.value.length === 0 || event.target.value.length > 2) {\n this.debounceTable();\n }\n return true;\n };\n\n /**\n * Process the search reset click event from the student table.\n *\n */\n tableSearchReset = () => {\n let tableSearchInputElement = document.getElementById(this.searchElement);\n tableSearchInputElement.value = '';\n tableSearchInputElement.focus();\n this.getTable();\n };\n\n /**\n * Process the row set event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableSearchRowSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let rows = event.target.dataset.metric;\n UserPreference.setUserPreference(this.rowPreference, rows)\n // eslint-disable-next-line promise/always-return\n .then(() => {\n this.getTable(); // Reload the table.\n })\n .fail(() => {\n Notification.exception(new Error('Failed to update user preference: rows'));\n });\n }\n };\n\n /**\n * Process the nav event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableNav = (event) => {\n event.preventDefault();\n\n const linkUrl = new URL(event.target.closest('a').href);\n const page = linkUrl.searchParams.get('page');\n\n if (page) {\n this.getTable(page);\n }\n };\n\n /**\n * Get and process the selected assessment metric from the dropdown for the heatmap display,\n * and update the corresponding user preference.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableSortButtonAction = (event) => {\n event.preventDefault();\n var element = event.target;\n\n if (element.tagName.toLowerCase() === 'a' && element.dataset.sort !== this.sortValue) {\n this.sortValue = element.dataset.sort;\n\n let links = element.parentNode.getElementsByTagName('a');\n for (let i = 0; i < links.length; i++) {\n links[i].classList.remove('active');\n }\n\n element.classList.add('active');\n\n // Save selection as a user preference.\n UserPreference.setUserPreference(this.sortPreference, this.sortValue);\n\n this.debounceTable(); // Call function to update table.\n }\n };\n\n /**\n * Re-add event listeners when the student table is updated.\n */\n tableEventListeners = () => {\n const tableElement = document.getElementById(this.elementId);\n const links = tableElement.querySelectorAll('a');\n const resetLink = tableElement.getElementsByClassName('resettable');\n const overrideLinks = tableElement.getElementsByClassName('action-icon override');\n const disabledLinks = tableElement.getElementsByClassName('action-icon disabled');\n const tableNavElement = tableElement.querySelectorAll('nav'); // There are two nav paging elements per table.\n\n for (let i = 0; i < links.length; i++) {\n let linkUrl = new URL(links[i].href);\n if (linkUrl.search.indexOf('thide') !== -1 || linkUrl.search.indexOf('tshow') !== -1) {\n links[i].addEventListener('click', this.tableHide);\n } else if (linkUrl.search.indexOf('tsort') !== -1) {\n links[i].addEventListener('click', this.tableSort);\n }\n }\n\n if (resetLink.length > 0) {\n resetLink[0].addEventListener('click', this.tableReset);\n }\n\n for (let i = 0; i < overrideLinks.length; i++) {\n overrideLinks[i].addEventListener('click', this.triggerOverrideModal);\n }\n\n for (let i = 0; i < disabledLinks.length; i++) {\n disabledLinks[i].addEventListener('click', (event) => {\n event.preventDefault();\n });\n }\n\n tableNavElement.forEach((navElement) => {\n navElement.addEventListener('click', this.tableNav);\n });\n };\n\n /**\n * Trigger the override modal form. Thin wrapper to add extra data to click event.\n *\n * @param {Event} event The triggered event for the element.\n */\n triggerOverrideModal = (event) => {\n event.preventDefault();\n let userid = event.target.closest('a').id.substring(25);\n if (userid.includes('-')) {\n let elements = userid.split('-');\n this.activityId = elements.pop();\n userid = elements.pop();\n }\n\n OverrideModal.displayModalForm(this.activityId, userid, this.hoursFilter);\n };\n}\n"],"names":["constructor","activity","context","tableElementId","tableFragmentComponent","tableFragmentValue","tableRowPreference","tableSortPreference","tableSearchElement","tableId","tableMethodName","activityId","contextId","elementId","fragmentComponent","fragmentValue","rowPreference","sortPreference","searchElement","id","methodName","overridden","getTable","page","globalThis","reports","_this","search","document","getElementById","value","trim","tableElement","spinner","getElementsByClassName","tableBody","values","activityid","params","JSON","stringify","classList","remove","loadFragment","done","response","js","innerHTML","runTemplateJS","add","tableEventListeners","fail","exception","Error","debounceTable","Debouncer","debouncer","tableSort","event","preventDefault","sortArray","linkUrl","URL","target","closest","href","targetSortBy","searchParams","get","targetSortOrder","call","methodname","this","args","tableid","preference","then","tableHide","hideArray","links","querySelectorAll","targetAction","targetColumn","action","column","indexOf","i","length","hideLinkUrl","tableReset","tableSearch","key","ctrlKey","tableSearchReset","tableSearchInputElement","focus","tableSearchRowSet","tagName","toLowerCase","rows","dataset","metric","UserPreference","setUserPreference","tableNav","tableSortButtonAction","element","sort","sortValue","parentNode","getElementsByTagName","resetLink","overrideLinks","disabledLinks","tableNavElement","addEventListener","triggerOverrideModal","forEach","navElement","userid","substring","includes","elements","split","pop","displayModalForm","hoursFilter"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/dashboard.js b/amd/src/dashboard.js index 85d822f9..212abf00 100644 --- a/amd/src/dashboard.js +++ b/amd/src/dashboard.js @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +globalThis.reports = 0; + export const init = () => { // Create the course search filter. @@ -48,6 +50,22 @@ export const init = () => { // Load the tab functionality. tabs(); + // Load the loading page whilst we wait for the reports to complete. + window.setTimeout(loading, 2000); +}; + +const loading = () => { + + let loaderwrapper = document.getElementById('loader-wrapper'); + let index = document.getElementById('local-assessfreq-index'); + + // No reports loading, then show the index page. + if (globalThis.reports === 0) { + loaderwrapper.style.display = 'none'; + index.style.display = 'block'; + } else { + window.setTimeout(loading, 1000); + } }; const tabs = () => { diff --git a/amd/src/table_handler.js b/amd/src/table_handler.js index d0cc3a3f..8005cd32 100644 --- a/amd/src/table_handler.js +++ b/amd/src/table_handler.js @@ -61,6 +61,9 @@ export default class TableHandler { * @param {int|string|null} page Page number. */ getTable = (page = 0) => { + + globalThis.reports++; + this.overridden = false; let search = document.getElementById(this.searchElement).value.trim(); @@ -85,10 +88,12 @@ export default class TableHandler { } spinner.classList.add('hide'); this.tableEventListeners(); // Re-add table event listeners. - - }).fail(() => { + globalThis.reports--; + }) + .fail(() => { + globalThis.reports--; Notification.exception(new Error('Failed to update table.')); - }); + }); }; /** diff --git a/classes/frequency.php b/classes/frequency.php index 47d1b885..9bcd2446 100644 --- a/classes/frequency.php +++ b/classes/frequency.php @@ -392,10 +392,11 @@ public function get_event_users(int $contextid, string $module, bool $cache = tr $users = $data->users; } else { // Not valid cache data. $sql = 'SELECT u.userid as id - FROM {local_assessfreq_user} u - INNER JOIN {local_assessfreq_site} s ON u.eventid = s.id - WHERE s.contextid = ? - AND s.module = ?'; + FROM {local_assessfreq_user} u + INNER JOIN {local_assessfreq_site} s ON u.eventid = s.id + WHERE s.contextid = ? + AND s.module = ? + GROUP BY u.userid'; $params = [$contextid, $module]; $users = $DB->get_records_sql($sql, $params); @@ -813,7 +814,7 @@ public function get_user_events(int $userid, string $module = 'all', int $from = public function get_user_events_all(int $courseid, string $module = 'all', int $from = 0, int $to = 0): iterable { global $DB; - $rowkey = $DB->sql_concat('s.id', "'_'", 'u.userid'); + $rowkey = $DB->sql_concat('s.id', "'_'", 'u.userid', "'_'", 'u.id'); $sql = "SELECT $rowkey as myrow, u.userid, s.* FROM {local_assessfreq_site} s INNER JOIN {local_assessfreq_user} u ON u.eventid = s.id diff --git a/classes/source_base.php b/classes/source_base.php index fc2f0ed0..6bfdccf5 100644 --- a/classes/source_base.php +++ b/classes/source_base.php @@ -32,6 +32,13 @@ abstract class source_base { private static array $instances = []; + /** + * Static array cache multiple calls to the same functions. + * + * @var array + */ + private static array $cache = []; + public function __construct() { $this->get_required_js(); $this->get_required_css(); @@ -127,20 +134,32 @@ protected function get_required_css() { protected function get_tracking(int $assessid, bool $limited = false) : array { global $DB; - $trendlimit = get_config('assessfreqreport_activity_dashboard', 'trendcount'); + $module = $this->get_module(); + + $cachekey = "$assessid-$module"; + + if (isset(self::$cache[$cachekey])) { + return self::$cache[$cachekey]; + } + + $trendcount = get_config('assessfreqreport_activity_dashboard', 'trendcount'); + $trendlimit = get_config('assessfreqreport_activity_dashboard', 'trendlimit'); $return = []; $trends = $DB->get_records( 'local_assessfreq_trend', - ['assessid' => $assessid, 'module' => $this->get_module()], - 'timecreated ASC' + ['assessid' => $assessid, 'module' => $module], + 'id DESC', + '*', + 0, + $trendlimit ); if (!$limited) { return $trends; } - $modulus = round(count($trends) / $trendlimit); + $modulus = round(count($trends) / $trendcount); $i = 0; - if (count($trends) < $trendlimit) { + if (count($trends) < $trendcount) { return $trends; } foreach ($trends as $trend) { @@ -150,6 +169,8 @@ protected function get_tracking(int $assessid, bool $limited = false) : array { $i++; } + self::$cache[$cachekey] = $return; + return $return; } diff --git a/report/activities_in_progress/classes/output/renderer.php b/report/activities_in_progress/classes/output/renderer.php index bd87bd81..21e3ea0e 100644 --- a/report/activities_in_progress/classes/output/renderer.php +++ b/report/activities_in_progress/classes/output/renderer.php @@ -152,7 +152,7 @@ public function render_report($data) { get_config('assessfreqreport_activities_in_progress', 'finishedcolor'), ]; - if ($participants) { + if ($participantseriesdata) { $chart = new chart_pie(); $chart->set_doughnut(true); $participants = new chart_series( diff --git a/report/activities_in_progress/lang/en/assessfreqreport_activities_in_progress.php b/report/activities_in_progress/lang/en/assessfreqreport_activities_in_progress.php index 775d996d..4c5857bb 100644 --- a/report/activities_in_progress/lang/en/assessfreqreport_activities_in_progress.php +++ b/report/activities_in_progress/lang/en/assessfreqreport_activities_in_progress.php @@ -39,10 +39,6 @@ $string['settings:inprogresscolor_desc'] = 'Select color to display for in progress users in charts'; $string['settings:finishedcolor'] = 'Finished color'; $string['settings:finishedcolor_desc'] = 'Select color to display for finished users in charts'; -$string['settings:trendcount'] = 'Trend chart limit'; -$string['settings:trendcount_desc'] = 'The trend data is run every minute and can contain a lot of data. -For example an assessment running for 5 days can have 7200 points that can be mapped which can overwhelm the chart. -This setting specifies the number of points that will be evenly plotted on the graph'; $string['settings:graphsheading'] = 'Graph settings'; $string['settings:graphsheading_desc'] = 'Specify the graph settings for each graph report'; diff --git a/report/activity_dashboard/lang/en/assessfreqreport_activity_dashboard.php b/report/activity_dashboard/lang/en/assessfreqreport_activity_dashboard.php index efae39d0..6bcb8f57 100644 --- a/report/activity_dashboard/lang/en/assessfreqreport_activity_dashboard.php +++ b/report/activity_dashboard/lang/en/assessfreqreport_activity_dashboard.php @@ -41,10 +41,14 @@ $string['settings:inprogresscolor_desc'] = 'Select color to display for in progress users in charts'; $string['settings:finishedcolor'] = 'Finished color'; $string['settings:finishedcolor_desc'] = 'Select color to display for finished users in charts'; -$string['settings:trendcount'] = 'Trend chart limit'; +$string['settings:trendcount'] = 'Trend chart count'; $string['settings:trendcount_desc'] = 'The trend data is run every minute and can contain a lot of data. For example an assessment running for 5 days can have 7200 points that can be mapped which can overwhelm the chart. This setting specifies the number of points that will be evenly plotted on the graph'; +$string['settings:trendlimit'] = 'Trend chart limit'; +$string['settings:trendlimit_desc'] = 'The trend data is run every minute and can contain a lot of data. +For example an assessment running for 5 days can have 7200 points that can be mapped which can overwhelm the chart. +This setting specifies the number of most recent points that will be used'; $string['form:activity'] = 'Activity'; $string['form:entercourse'] = 'Enter course name'; diff --git a/report/activity_dashboard/settings.php b/report/activity_dashboard/settings.php index 7ccfee24..a8e4f106 100644 --- a/report/activity_dashboard/settings.php +++ b/report/activity_dashboard/settings.php @@ -44,6 +44,13 @@ 300 )); +$settings->add(new admin_setting_configint( + 'assessfreqreport_activity_dashboard/trendlimit', + get_string('settings:trendlimit', 'assessfreqreport_activity_dashboard'), + get_string('settings:trendlimit_desc', 'assessfreqreport_activity_dashboard'), + 3000 +)); + $settings->add(new admin_setting_configcolourpicker( 'assessfreqreport_activity_dashboard/notloggedincolor', get_string('settings:notloggedincolor', 'assessfreqreport_activity_dashboard'), diff --git a/report/heatmap/templates/filters.mustache b/report/heatmap/templates/filters.mustache index a46a2bb2..a5ad206e 100644 --- a/report/heatmap/templates/filters.mustache +++ b/report/heatmap/templates/filters.mustache @@ -50,7 +50,7 @@
-
{{#str}}report:scales, assessfreqreport_heatmap {{/str}} {{{scales}}}
+
{{#str}}report:scales, assessfreqreport_heatmap {{/str}} {{{scales}}}
diff --git a/report/student_search/classes/output/user_table.php b/report/student_search/classes/output/user_table.php index f18ebcf8..9a540d9b 100644 --- a/report/student_search/classes/output/user_table.php +++ b/report/student_search/classes/output/user_table.php @@ -513,7 +513,7 @@ public function query_db($pagesize, $useinitialsbar = false) { WHERE timemodified >= :stm'; $joins .= ' LEFT JOIN {quiz_overrides} qo ON u.id = qo.userid AND qo.quiz = :qoquiz'; - $joins .= " LEFT JOIN ($attemptsql) qa ON u.id = qa.userid"; + $joins .= " INNER JOIN ($attemptsql) qa ON u.id = qa.userid"; $joins .= " LEFT JOIN ($sessionsql) us ON u.id = us.userid"; $params['qaquiz'] = $quizobj->id; diff --git a/source/assign/classes/output/user_table.php b/source/assign/classes/output/user_table.php index a92cde2e..6e5a6886 100644 --- a/source/assign/classes/output/user_table.php +++ b/source/assign/classes/output/user_table.php @@ -77,6 +77,10 @@ class user_table extends table_sql implements renderable { */ private $context; + /** + * @var array Cache of course modules to reduce call volume. + */ + private static $cmcache = []; /** * report_table constructor. @@ -280,7 +284,7 @@ public function query_db($pagesize, $useinitialsbar = false) { $joins .= " LEFT JOIN {assign_overrides} ao ON u.id = ao.userid AND ao.assignid = :aoassign - LEFT JOIN ( + INNER JOIN ( SELECT id, userid, status, assignment, timecreated as timestart FROM {assign_submission} WHERE assignment = :subassign @@ -305,6 +309,7 @@ public function query_db($pagesize, $useinitialsbar = false) { COALESCE(NULLIF(ao.cutoffdate, 0), $this->cutoffdate) AS cutoffdate, sub.status, sub.id AS submission, + sub.assignment, (CASE WHEN us.userid > 0 THEN 'loggedin' ELSE 'notloggedin' END) AS loggedinstatus FROM {user} u $finaljoin->joins @@ -429,13 +434,17 @@ protected function get_common_columns(): array { protected function get_common_column_actions(stdClass $row): string { global $OUTPUT; $actions = ''; + if (!isset(self::$cmcache[$row->assignment])) { + [,$cm] = get_course_and_cm_from_instance($row->assignment, 'assign'); + self::$cmcache[$row->assignment] = $cm; + } if ($row->status == 'submitted') { $classes = 'action-icon'; $attempturl = new moodle_url( '/mod/assign/view.php', [ 'action' => 'grader', - 'id' => $row->submission, + 'id' => self::$cmcache[$row->assignment]->id, 'userid' => $row->id, ] ); diff --git a/source/assign/classes/source.php b/source/assign/classes/source.php index 8e65f75d..635cebda 100644 --- a/source/assign/classes/source.php +++ b/source/assign/classes/source.php @@ -683,7 +683,7 @@ public function get_assign_data($assign) : stdClass { $assigndata->participantlink = $participantlink->out(false); $assigndata->dashboardlink = $dashboardlink->out(false); $assigndata->assessid = $assign->id; - $assigndata->context = $cm->context; + $assigndata->context = $context; $assigndata->timestampopen = $assignrecord->allowsubmissionsfromdate; $assigndata->timestampclose = $assignrecord->duedate; $assigndata->timestamplimit = $assignrecord->timelimit; diff --git a/styles.css b/styles.css index 68d3bb6c..e98b2772 100644 --- a/styles.css +++ b/styles.css @@ -1,5 +1,6 @@ body #local-assessfreq-index { font-family: Arial, serif; + display: none; } /* Style the tab */ @@ -105,4 +106,75 @@ body #local-assessfreq-index { outline: 0 !important; background-color: #0176be !important; color: #fff !important; +} + +/* Loading page and animation */ +#loader-wrapper { + text-align: center; + height: 300px; + color: #000; +} + +#loader, +#loader:before, +#loader:after { + border-radius: 50%; + width: 2.5em; + height: 2.5em; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + -webkit-animation: load7 1.8s infinite ease-in-out; + animation: load7 1.8s infinite ease-in-out; +} + +#loader { + color: #000; + font-size: 10px; + margin: 80px auto; + position: relative; + text-indent: -9999em; + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); + -webkit-animation-delay: -0.16s; + animation-delay: -0.16s; +} + +#loader:before, +#loader:after { + content: ''; + position: absolute; + top: 0; +} + +#loader:before { + left: -3.5em; + -webkit-animation-delay: -0.32s; + animation-delay: -0.32s; +} + +#loader:after { + left: 3.5em; +} + +@-webkit-keyframes load7 { + 0%, + 80%, + 100% { + box-shadow: 0 2.5em 0 -1.3em; + } + 40% { + box-shadow: 0 2.5em 0 0; + } +} + +@keyframes load7 { + 0%, + 80%, + 100% { + box-shadow: 0 2.5em 0 -1.3em; + } + 40% { + box-shadow: 0 2.5em 0 0; + } } \ No newline at end of file diff --git a/templates/index.mustache b/templates/index.mustache index 32e1f938..5d75fa8d 100644 --- a/templates/index.mustache +++ b/templates/index.mustache @@ -28,4 +28,7 @@ {{> local_assessfreq/tabs}} - +
+
+

Loading reports...

+
\ No newline at end of file