diff --git a/README.md b/README.md
index f6d5b867..faf1d83e 100644
--- a/README.md
+++ b/README.md
@@ -47,3 +47,36 @@ 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
this program. If not, see .
+
+## Version 2024040300 refactor ##
+
+Post version `2024040300` this plugin was completely refactored to support more reports and modules.
+
+Each report is now a subplugin within the `report` directory
+The subplugins report class should extend from the \local_assessfreq\report_base class
+
+Capability checks were reworked to be relative to the location that they are being loading from. The initial version
+has the following capabilities:
+
+- local/assessfreq:view
+- assessfreqreport/activity_dashboard:view
+- assessfreqreport/activities_in_progress:view
+- assessfreqreport/heatmap:view
+- 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 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
+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/calendar.min.js b/amd/build/calendar.min.js
deleted file mode 100644
index 59f5f57f..00000000
--- a/amd/build/calendar.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Javascript for heatmap calendar generation and display.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define("local_assessfreq/calendar",["core/str","core/notification","core/ajax"],(function(Str,Notification,Ajax){var Calendar={},eventArray=[];const stringArr=[{key:"sun",component:"calendar"},{key:"mon",component:"calendar"},{key:"tue",component:"calendar"},{key:"wed",component:"calendar"},{key:"thu",component:"calendar"},{key:"fri",component:"calendar"},{key:"sat",component:"calendar"},{key:"jan",component:"local_assessfreq"},{key:"feb",component:"local_assessfreq"},{key:"mar",component:"local_assessfreq"},{key:"apr",component:"local_assessfreq"},{key:"may",component:"local_assessfreq"},{key:"jun",component:"local_assessfreq"},{key:"jul",component:"local_assessfreq"},{key:"aug",component:"local_assessfreq"},{key:"sep",component:"local_assessfreq"},{key:"oct",component:"local_assessfreq"},{key:"nov",component:"local_assessfreq"},{key:"dec",component:"local_assessfreq"}];var stringResult,heatRangeMax,heatRangeMin,colorArray,processModules,heatRangeScale={1:0,2:0,3:0,4:0,5:0,6:0};const getContrast=function(hexcolor){return void 0===hexcolor?"#000000":("#"===hexcolor.slice(0,1)&&(hexcolor=hexcolor.slice(1)),(299*parseInt(hexcolor.substr(0,2),16)+587*parseInt(hexcolor.substr(2,2),16)+114*parseInt(hexcolor.substr(4,2),16))/1e3>=128?"#000000":"#FFFFFF")},daysInMonth=function(month,year){return 32-new Date(year,month,32).getDate()},getHeatColors=function(){return new Promise(((resolve,reject)=>{Ajax.call([{methodname:"local_assessfreq_get_heat_colors",args:{}}],!0,!1)[0].done((function(response){colorArray=JSON.parse(response),resolve(colorArray)})).fail((function(){reject(new Error("Failed to get heat colors"))}))}))},getProcessModules=function(){return new Promise(((resolve,reject)=>{Ajax.call([{methodname:"local_assessfreq_get_process_modules",args:{}}],!0,!1)[0].done((function(response){processModules=JSON.parse(response),resolve(processModules)})).fail((function(){reject(new Error("Failed to get process events"))}))}))},getHeat=function(eventCount){if(eventCount==heatRangeMin)return 1;const localPercent=(eventCount-heatRangeMin)/(heatRangeMax-heatRangeMin);let heat=Math.round(5*localPercent+1);return heat<1&&(heat=1),heat>6&&(heat=6),heat},getEvents=function(_ref){let{year:year,metric:metric,modules:modules}=_ref;return new Promise(((resolve,reject)=>{let args={year:year,metric:metric,modules:modules},jsonArgs=JSON.stringify(args);Ajax.call([{methodname:"local_assessfreq_get_frequency",args:{jsondata:jsonArgs}}])[0].done((response=>{eventArray=JSON.parse(response),resolve(eventArray)})).fail((()=>{reject(new Error("Failed to get events"))}))}))},createTables=function(_ref2){let{year:year,startMonth:startMonth,endMonth:endMonth}=_ref2;return new Promise(((resolve,reject)=>{let calendarContainer=document.createElement("div"),month=startMonth;for(let i=startMonth;i<=endMonth;i++){let container=document.createElement("div");container.classList.add("local-assessfreq-month");let table=document.createElement("table");table.classList.add("table-striped");let thead=document.createElement("thead"),tbody=document.createElement("tbody");tbody.id="calendar-body-"+i;let monthRow=document.createElement("tr"),dayrow=document.createElement("tr"),monthHeader=document.createElement("th");monthHeader.colSpan=7,monthHeader.innerHTML=stringResult[7+month];for(let j=0;j<7;j++){let dayHeader=document.createElement("th");dayHeader.innerHTML=stringResult[j],dayrow.appendChild(dayHeader)}monthRow.appendChild(monthHeader),thead.appendChild(monthRow),thead.appendChild(dayrow),table.appendChild(thead),table.appendChild(tbody),container.appendChild(table),calendarContainer.appendChild(container),month++}if(void 0===year||void 0===startMonth||void 0===endMonth)reject(Error("Failed to create calendar tables."));else{resolve({calendarContainer:calendarContainer,year:year,startMonth:startMonth})}}))},getTooltip=function(dayArray){let tipHTML="";for(let[key,value]of Object.entries(dayArray))tipHTML+=""+processModules[key]+": "+value+" ";return tipHTML},populateCalendarDays=function(table,year,month){let firstDay=new Date(year,month).getDay(),monthEvents=function(year,month){let monthevents;return void 0!==eventArray[year]&&void 0!==eventArray[year][month]&&(monthevents=eventArray[year][month]),monthevents}(year,month+1),date=1;for(let i=0;i<6;i++){let row=document.createElement("tr");for(let j=0;j<7;j++){if(0===i&&jdaysInMonth(month,year))break;if(cell=document.createElement("td"),cellText=document.createTextNode(date),void 0!==monthEvents&&monthEvents.hasOwnProperty(date)){let heat=getHeat(monthEvents[date].number);(0==heatRangeScale[heat]||heatRangeScale[heat]>monthEvents[date].number)&&(heatRangeScale[heat]=monthEvents[date].number),cell.style.backgroundColor=colorArray[heat],cell.style.color=getContrast(colorArray[heat]),cell.dataset.toggle="tooltip",cell.dataset.html="true",cell.dataset.event="true",cell.dataset.date=year+"-"+(month+1)+"-"+date,cell.title=getTooltip(monthEvents[date]),cell.style.cursor="pointer"}date++}cell.appendChild(cellText),row.appendChild(cell)}table.appendChild(row)}},populateCalendar=function(_ref3){let{calendarContainer:calendarContainer,year:year,startMonth:startMonth}=_ref3;return new Promise(((resolve,reject)=>{let tables=calendarContainer.getElementsByTagName("tbody"),month=startMonth;for(var i=0;i{let table=document.createElement("table"),tbody=document.createElement("tbody"),trow=document.createElement("tr");for(var i=1;i<7;i++)if(0!==heatRangeScale[i]){let cell=document.createElement("td"),cellText=document.createTextNode(heatRangeScale[i]+"+");cell.appendChild(cellText),cell.style.backgroundColor=colorArray[i],cell.style.color=getContrast(colorArray[i]),trow.appendChild(cell)}tbody.appendChild(trow),table.appendChild(tbody),heatRangeScale={1:0,2:0,3:0,4:0,5:0,6:0},resolve(table)}))},Calendar.generate=function(year,startMonth,endMonth,metric,modules){return new Promise(((resolve,reject)=>{const dateObj={year:year,startMonth:startMonth,endMonth:endMonth},eventObj={year:year,metric:metric,modules:modules};Str.get_strings(stringArr).catch((()=>{Notification.exception(new Error("Failed to load strings"))})).then((stringReturn=>(stringResult=stringReturn,eventObj))).then(getEvents).then((eventArray=>{!function(eventArray,dateObj){new Promise((resolve=>{if(void 0===eventArray&&(heatRangeMax=0,heatRangeMin=0,resolve(eventArray)),Object.keys(eventArray).length>0&&"undefined"!==eventArray[dateObj.year]){let eventcount=new Array,year=eventArray[dateObj.year];for(let i=0;i<12;i++)if(void 0!==year[i]){let month=year[i];for(let j=0;j<32;j++)void 0!==month[j]&&eventcount.push(month[j].number)}heatRangeMax=Math.max(...eventcount),heatRangeMin=Math.min(...eventcount)}else heatRangeMax=0,heatRangeMin=0;resolve(eventArray)}))}(eventArray,dateObj)})).then(getHeatColors).then(getProcessModules).then((()=>dateObj)).then(createTables).then(populateCalendar).then((calendarHTML=>{void 0!==calendarHTML?resolve(calendarHTML):reject(Error("Could not generate calendar"))}))}))},Calendar}));
-
-//# sourceMappingURL=calendar.min.js.map
\ No newline at end of file
diff --git a/amd/build/calendar.min.js.map b/amd/build/calendar.min.js.map
deleted file mode 100644
index 067b1a77..00000000
--- a/amd/build/calendar.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"calendar.min.js","sources":["../src/calendar.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 * Javascript for heatmap calendar generation and display.\n *\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['core/str', 'core/notification', 'core/ajax'], function (Str, Notification, Ajax) {\n\n /**\n * Module level variables.\n */\n var Calendar = {};\n var eventArray = [];\n const stringArr = [\n {key: 'sun', component: 'calendar'},\n {key: 'mon', component: 'calendar'},\n {key: 'tue', component: 'calendar'},\n {key: 'wed', component: 'calendar'},\n {key: 'thu', component: 'calendar'},\n {key: 'fri', component: 'calendar'},\n {key: 'sat', component: 'calendar'},\n {key: 'jan', component: 'local_assessfreq'},\n {key: 'feb', component: 'local_assessfreq'},\n {key: 'mar', component: 'local_assessfreq'},\n {key: 'apr', component: 'local_assessfreq'},\n {key: 'may', component: 'local_assessfreq'},\n {key: 'jun', component: 'local_assessfreq'},\n {key: 'jul', component: 'local_assessfreq'},\n {key: 'aug', component: 'local_assessfreq'},\n {key: 'sep', component: 'local_assessfreq'},\n {key: 'oct', component: 'local_assessfreq'},\n {key: 'nov', component: 'local_assessfreq'},\n {key: 'dec', component: 'local_assessfreq'},\n ];\n var stringResult;\n var heatRangeMax;\n var heatRangeMin;\n var colorArray;\n var processModules;\n var heatRangeScale = {'1': 0, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0};\n\n /**\n * Pick a contrasting text color based on the background color.\n *\n * @param {String} hexcolor A hexcolor value.\n * @return {String} The contrasting color (black or white).\n */\n const getContrast = function (hexcolor) {\n\n if (typeof (hexcolor) === \"undefined\") {\n return '#000000';\n }\n\n // If a leading # is provided, remove it.\n if (hexcolor.slice(0, 1) === '#') {\n hexcolor = hexcolor.slice(1);\n }\n\n // Convert to RGB value.\n var r = parseInt(hexcolor.substr(0,2),16);\n var g = parseInt(hexcolor.substr(2,2),16);\n var b = parseInt(hexcolor.substr(4,2),16);\n\n // Get YIQ ratio.\n var yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;\n\n // Check contrast.\n return (yiq >= 128) ? '#000000' : '#FFFFFF';\n };\n\n /**\n * Check how many days in a month code.\n * from https://dzone.com/articles/determining-number-days-month.\n *\n * @method daysInMonth\n * @param {Number} month The month to get the number of days for.\n * @param {Number} year The year to get the number of days for.\n */\n const daysInMonth = function (month, year) {\n return 32 - new Date(year, month, 32).getDate();\n };\n\n /**\n * Get the heat colors to use in the heat map via Ajax.\n *\n * @method getHeatColors\n */\n const getHeatColors = function () {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_assessfreq_get_heat_colors',\n args: {},\n }], true, false)[0].done(function (response) {\n colorArray = JSON.parse(response);\n resolve(colorArray);\n }).fail(function () {\n reject(new Error('Failed to get heat colors'));\n });\n });\n };\n\n /**\n * Get the event names that we are processing.\n *\n * @method getProcessEvents\n */\n const getProcessModules = function () {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_assessfreq_get_process_modules',\n args: {},\n }], true, false)[0].done(function (response) {\n processModules = JSON.parse(response);\n resolve(processModules);\n }).fail(function () {\n reject(new Error('Failed to get process events'));\n });\n });\n };\n\n /**\n * Calculate the min and max values to use in the heatmap.\n *\n * @method daysInMonth\n * @param {Object} eventArray All the event count for the heatmap.\n * @param {Object} dateObj Date details.\n */\n const calcHeatRange = function (eventArray, dateObj) {\n return new Promise((resolve) => {\n\n // Resolve early if there are no events.\n if (typeof (eventArray) === \"undefined\") {\n heatRangeMax = 0;\n heatRangeMin = 0;\n\n resolve(eventArray);\n }\n // If scheduled tasks have not run yet we may not have any data.\n let eventArrayLength = Object.keys(eventArray).length;\n if ((eventArrayLength > 0) && (eventArray[dateObj.year] !== \"undefined\")) {\n let eventcount = new Array;\n let year = eventArray[dateObj.year];\n\n // Iterate through all the event counts.\n // This code looks nasty but there is only 366 days in a year.\n for (let i = 0; i < 12; i++) {\n if (typeof year[i] !== \"undefined\") {\n let month = year[i];\n for (let j = 0; j < 32; j++) {\n if (typeof month[j] !== \"undefined\") {\n eventcount.push(month[j].number);\n }\n }\n }\n }\n\n // Get min and max values to calculate heat spread.\n heatRangeMax = Math.max(...eventcount);\n heatRangeMin = Math.min(...eventcount);\n } else {\n heatRangeMax = 0;\n heatRangeMin = 0;\n }\n\n resolve(eventArray);\n });\n };\n\n /**\n * Translate assessment frequency to a heat value.\n *\n * @method getHeat\n * @param {Number} eventCount The count to get the heat value.\n * @return {Number} heat The heat value.\n */\n const getHeat = function (eventCount) {\n let scaleMin = 1;\n\n if (eventCount == heatRangeMin) {\n return scaleMin;\n }\n\n const scaleRange = 5; // 0 - 5 steps.\n const localRange = heatRangeMax - heatRangeMin;\n const localPercent = (eventCount - heatRangeMin) / localRange;\n let heat = Math.round((localPercent * scaleRange) + 1);\n\n // Clamp values.\n if (heat < 1) {\n heat = 1;\n }\n\n if (heat > 6) {\n heat = 6;\n }\n\n return heat;\n };\n\n /**\n * Get the events to display in the calendar via ajax call.\n *\n * @method getEvents\n *\n * @param {Object} args The arguments to pass to the ajax call.\n * @param {Number} args.year The year to get the events for.\n * @param {String} args.metric The metric to get the events for.\n * @param {Array} args.modules The modules to get the events for.\n *\n * @return {Promise}\n */\n const getEvents = function ({year, metric, modules}) {\n return new Promise((resolve, reject) => {\n let args = {\n year: year,\n metric: metric,\n modules: modules\n };\n let jsonArgs = JSON.stringify(args);\n\n // Get the events to use in the mapping.\n Ajax.call([{\n methodname: 'local_assessfreq_get_frequency',\n args: {\n jsondata: jsonArgs\n },\n }])[0].done((response) => {\n eventArray = JSON.parse(response);\n resolve(eventArray);\n }).fail(() => {\n reject(new Error('Failed to get events'));\n });\n });\n };\n\n /**\n * Get the events for a particular month and year.\n *\n * @param {Number} year The year to get the number of days for.\n * @param {Number} month The month to get the number of days for.\n * @return {Array} monthevents The events for the supplied month.\n */\n const getMonthEvents = function (year, month) {\n let monthevents;\n\n if ((typeof eventArray[year] !== \"undefined\") && (typeof eventArray[year][month] !== \"undefined\")) {\n monthevents = eventArray[year][month];\n }\n\n return monthevents;\n };\n\n /**\n * Create the table structure for the calendar months.\n *\n * @param {Object} args The arguments to pass to the ajax call.\n * @param {Number} args.year The year to get the events for.\n * @param {Number} args.startMonth The month to start the calendar\n * @param {Number} args.endMonth The month to end the calendar\n *\n * @return {Promise}\n */\n const createTables = function ({year, startMonth, endMonth}) {\n return new Promise((resolve, reject) => {\n let calendarContainer = document.createElement('div');\n let month = startMonth;\n\n // Itterate through and build are tables.\n for (let i = startMonth; i <= endMonth; i++) {\n // Setup some elements.\n let container = document.createElement('div');\n container.classList.add('local-assessfreq-month');\n let table = document.createElement('table');\n table.classList.add('table-striped');\n let thead = document.createElement('thead');\n let tbody = document.createElement('tbody');\n tbody.id = 'calendar-body-' + i;\n let monthRow = document.createElement('tr');\n let dayrow = document.createElement('tr');\n let monthHeader = document.createElement('th');\n monthHeader.colSpan = 7;\n monthHeader.innerHTML = stringResult[(7 + month)];\n\n for (let j = 0; j < 7; j++) {\n let dayHeader = document.createElement('th');\n dayHeader.innerHTML = stringResult[j];\n dayrow.appendChild(dayHeader);\n }\n\n // Construct the table.\n monthRow.appendChild(monthHeader);\n\n thead.appendChild(monthRow);\n thead.appendChild(dayrow);\n\n table.appendChild(thead);\n table.appendChild(tbody);\n\n container.appendChild(table);\n\n // Add to parent.\n calendarContainer.appendChild(container);\n\n // Increment variables.\n month++;\n }\n\n if ((typeof year === 'undefined') || (typeof startMonth === 'undefined') || (typeof endMonth === 'undefined')) {\n reject(Error('Failed to create calendar tables.'));\n } else {\n const resultObj = {\n calendarContainer : calendarContainer,\n year : year,\n startMonth : startMonth\n };\n resolve(resultObj);\n }\n });\n };\n\n /**\n * Generate the tooltip HTML.\n *\n * @param {Object} dayArray The details of the events for that day/\n * @return {String} tipHTML The HTML for the tooltip.\n */\n const getTooltip = function (dayArray) {\n let tipHTML = '';\n\n for (let [key, value] of Object.entries(dayArray)) {\n tipHTML += '' + processModules[key] + ': ' + value + ' ';\n }\n\n return tipHTML;\n };\n\n /**\n * Generate calendar markup for the month.\n *\n * @param {Object} table The base table to populate.\n * @param {Number} year The year to generate calendar for.\n * @param {Number} month The monthe to generate calendar for.\n */\n const populateCalendarDays = function (table, year, month) {\n let firstDay = (new Date(year, month)).getDay(); // Get the starting day of the month.\n let monthEvents = getMonthEvents(year, (month + 1)); // We add one due to month diferences between PHP and JS.\n let date = 1; // Creating all cells.\n\n for (let i = 0; i < 6; i++) {\n let row = document.createElement(\"tr\"); // Creates a table row.\n\n // Creating individual cells, filing them up with data.\n for (let j = 0; j < 7; j++) {\n if (i === 0 && j < firstDay) {\n var cell = document.createElement(\"td\");\n var cellText = document.createTextNode(\"\");\n cell.dataset.event = 'false';\n } else if (date > daysInMonth(month, year)) { // Break if we have generated all the days for this month.\n break;\n } else {\n cell = document.createElement(\"td\");\n cellText = document.createTextNode(date);\n if ((typeof monthEvents !== \"undefined\") && (monthEvents.hasOwnProperty(date))) {\n let heat = getHeat(monthEvents[date]['number']);\n\n if (heatRangeScale[heat] == 0 || heatRangeScale[heat] > monthEvents[date]['number']) {\n heatRangeScale[heat] = monthEvents[date]['number'];\n }\n\n cell.style.backgroundColor = colorArray[heat];\n cell.style.color = getContrast(colorArray[heat]);\n\n // Add tooltip to cell.\n cell.dataset.toggle = 'tooltip';\n cell.dataset.html = 'true';\n cell.dataset.event = 'true';\n cell.dataset.date = year + '-' + (month + 1) + '-' + date;\n cell.title = getTooltip(monthEvents[date]);\n cell.style.cursor = \"pointer\";\n }\n date++;\n }\n\n cell.appendChild(cellText);\n row.appendChild(cell);\n }\n table.appendChild(row); // Appending each row into calendar body.\n }\n };\n\n /**\n * Controls the population of the calendar in to the base tables.\n *\n * @param {Object} args The arguments to pass to the ajax call.\n * @param {Object} args.calendarContainer The container to populate the calendar into.\n * @param {Number} args.year The year to get the events for.\n * @param {Number} args.startMonth The month to start the calendar\n *\n * @return {Promise}\n */\n const populateCalendar = function ({calendarContainer, year, startMonth}) {\n return new Promise((resolve, reject) => {\n // Get the table boodies.\n let tables = calendarContainer.getElementsByTagName(\"tbody\");\n let month = startMonth;\n\n // For each table body populate with calendar.\n for (var i = 0; i < tables.length; i++) {\n let table = tables[i];\n populateCalendarDays(table, year, month);\n month++;\n }\n\n if (typeof calendarContainer === 'undefined') {\n reject(Error('Failed to populate calendar tables.'));\n } else {\n resolve(calendarContainer);\n }\n });\n };\n\n /**\n * Create the heatmap scale for the calendar.\n *\n * @method createHeatScale\n */\n Calendar.createHeatScale = function () {\n return new Promise((resolve) => {\n let table = document.createElement('table');\n let tbody = document.createElement('tbody');\n let trow = document.createElement('tr');\n\n for (var i = 1; i < 7; i++) {\n if (heatRangeScale[i] !== 0) {\n let cell = document.createElement('td');\n let cellText = document.createTextNode(heatRangeScale[i] + '+');\n\n cell.appendChild(cellText);\n cell.style.backgroundColor = colorArray[i];\n cell.style.color = getContrast(colorArray[i]);\n\n trow.appendChild(cell);\n }\n }\n\n tbody.appendChild(trow);\n table.appendChild(tbody);\n\n // Reset heat range scale.\n heatRangeScale = {'1': 0, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0};\n\n resolve(table);\n });\n };\n\n /**\n * Initialise method for report calendar heatmap creation.\n *\n * @param {Number} year The year to generate the heatmap for.\n * @param {Number} startMonth The month to start with for the heatmap calendar.\n * @param {Number} endMonth The month to end with for the heatmap calendar.\n * @param {String} metric The type of metric to display, 'students' or 'aseess'.\n * @param {Array} modules The modules to display in the heatamp.\n * @return {Promise}\n */\n Calendar.generate = function (year, startMonth, endMonth, metric, modules) {\n return new Promise((resolve, reject) => {\n const dateObj = {\n year : year,\n startMonth : startMonth,\n endMonth : endMonth\n };\n\n const eventObj = {\n year : year,\n metric : metric,\n modules : modules\n };\n\n Str.get_strings(stringArr).catch(() => { // Get required strings.\n Notification.exception(new Error('Failed to load strings'));\n return;\n }).then(stringReturn => { // Save string to global to be used later.\n stringResult = stringReturn;\n return eventObj;\n })\n .then(getEvents)\n .then((eventArray) => {\n calcHeatRange(eventArray, dateObj);\n })\n .then(getHeatColors)\n .then(getProcessModules)\n .then(() => {\n return dateObj;\n })\n .then(createTables) // Create tables for calendar.\n .then(populateCalendar)\n .then((calendarHTML) => { // Return the result of the generate function.\n if (typeof calendarHTML !== 'undefined') {\n resolve(calendarHTML);\n } else {\n reject(Error('Could not generate calendar'));\n }\n });\n });\n\n };\n\n return Calendar;\n});\n"],"names":["define","Str","Notification","Ajax","Calendar","eventArray","stringArr","key","component","stringResult","heatRangeMax","heatRangeMin","colorArray","processModules","heatRangeScale","getContrast","hexcolor","slice","parseInt","substr","daysInMonth","month","year","Date","getDate","getHeatColors","Promise","resolve","reject","call","methodname","args","done","response","JSON","parse","fail","Error","getProcessModules","getHeat","eventCount","localPercent","heat","Math","round","getEvents","metric","modules","jsonArgs","stringify","jsondata","createTables","startMonth","endMonth","calendarContainer","document","createElement","i","container","classList","add","table","thead","tbody","id","monthRow","dayrow","monthHeader","colSpan","innerHTML","j","dayHeader","appendChild","getTooltip","dayArray","tipHTML","value","Object","entries","populateCalendarDays","firstDay","getDay","monthEvents","monthevents","getMonthEvents","date","row","cell","cellText","createTextNode","dataset","event","hasOwnProperty","style","backgroundColor","color","toggle","html","title","cursor","populateCalendar","tables","getElementsByTagName","length","createHeatScale","trow","generate","dateObj","eventObj","get_strings","catch","exception","then","stringReturn","keys","eventcount","Array","push","number","max","min","calcHeatRange","calendarHTML"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/chart_data.min.js b/amd/build/chart_data.min.js
deleted file mode 100644
index 19594e64..00000000
--- a/amd/build/chart_data.min.js
+++ /dev/null
@@ -1,10 +0,0 @@
-define("local_assessfreq/chart_data",["exports","core/fragment","core/notification","core/str","core/templates"],(function(_exports,_fragment,_notification,Str,_templates){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}}
-/**
- * Chart data JS module.
- *
- * @module local_assessfreq/char_data
- * @copyright 2020 Guillermo Gomez
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */let cards,contextId,fragment,template;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.getCardCharts=void 0,_fragment=_interopRequireDefault(_fragment),_notification=_interopRequireDefault(_notification),Str=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}(Str),_templates=_interopRequireDefault(_templates);_exports.getCardCharts=(quizId,hoursFilter,yearSelect)=>{cards.forEach((cardData=>{let cardElement=document.getElementById(cardData.cardId),spinner=cardElement.getElementsByClassName("overlay-icon-container")[0],chartBody=cardElement.getElementsByClassName("chart-body")[0],values={call:cardData.call};hoursFilter&&(values.hoursahead=hoursFilter[0],values.hoursbehind=hoursFilter[1]),quizId&&(values.quiz=quizId),yearSelect&&(values.year=yearSelect);let params={data:JSON.stringify(values)};spinner.classList.remove("hide"),_fragment.default.loadFragment("local_assessfreq",fragment,contextId,params).done((response=>{let resObj=JSON.parse(response);if(!0===resObj.hasdata){let context={withtable:!0,chartdata:JSON.stringify(resObj.chart)};return void 0!==cardData.aspect&&(context.aspect=cardData.aspect),void _templates.default.render(template,context).done(((html,js)=>{spinner.classList.add("hide"),_templates.default.replaceNodeContents(chartBody,html,js)})).fail((()=>{_notification.default.exception(new Error("Failed to load chart template."))}))}Str.get_string("nodata","local_assessfreq").then((str=>{const noDatastr=document.createElement("h3");noDatastr.innerHTML=str,chartBody.innerHTML=noDatastr.outerHTML,spinner.classList.add("hide")})).catch((()=>{_notification.default.exception(new Error("Failed to load string: nodata"))}))})).fail((()=>{_notification.default.exception(new Error("Failed to load card."))}))}))};_exports.init=(cardsArray,contextIdChart,fragmentChart,templateChart)=>{cards=cardsArray,contextId=contextIdChart,fragment=fragmentChart,template=templateChart}}));
-
-//# sourceMappingURL=chart_data.min.js.map
\ No newline at end of file
diff --git a/amd/build/chart_data.min.js.map b/amd/build/chart_data.min.js.map
deleted file mode 100644
index 2dbb84b3..00000000
--- a/amd/build/chart_data.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"chart_data.min.js","sources":["../src/chart_data.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/char_data\n * @copyright 2020 Guillermo Gomez \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Fragment from 'core/fragment';\nimport Notification from 'core/notification';\nimport * as Str from 'core/str';\nimport Templates from 'core/templates';\n\n/**\n * Module level variables.\n */\nlet cards;\nlet contextId;\nlet fragment;\nlet template;\n\n/**\n * For each of the cards on the dashboard get their corresponding chart data.\n * Data is based on the year variable from the corresponding dropdown.\n * Chart data is loaded via ajax.\n *\n * @param {int|null} quizId The quiz Id.\n * @param {array|null} hoursFilter Array with hour ahead or behind preference.\n * @param {int|null} yearSelect Year selected.\n */\nexport const getCardCharts = (quizId, hoursFilter, yearSelect) => {\n cards.forEach((cardData) => {\n let cardElement = document.getElementById(cardData.cardId);\n let spinner = cardElement.getElementsByClassName('overlay-icon-container')[0];\n let chartBody = cardElement.getElementsByClassName('chart-body')[0];\n let values = {'call': cardData.call};\n // Add values to Object depending on dashboard type.\n if (hoursFilter) {\n values.hoursahead = hoursFilter[0];\n values.hoursbehind = hoursFilter[1];\n }\n if (quizId) {\n values.quiz = quizId;\n }\n if (yearSelect) {\n values.year = yearSelect;\n }\n let params = {'data': JSON.stringify(values)};\n\n spinner.classList.remove('hide'); // Show sinner if not already shown.\n Fragment.loadFragment('local_assessfreq', fragment, contextId, params)\n .done((response) => {\n let resObj = JSON.parse(response);\n if (resObj.hasdata === true) {\n let context = {\n 'withtable': true, 'chartdata': JSON.stringify(resObj.chart)\n };\n if (typeof cardData.aspect !== 'undefined') {\n context.aspect = cardData.aspect;\n }\n Templates.render(template, context).done((html, js) => {\n spinner.classList.add('hide'); // Hide spinner if not already hidden.\n // Load card body.\n Templates.replaceNodeContents(chartBody, html, js);\n }).fail(() => {\n Notification.exception(new Error('Failed to load chart template.'));\n return;\n });\n return;\n } else {\n Str.get_string('nodata', 'local_assessfreq').then((str) => {\n const noDatastr = document.createElement('h3');\n noDatastr.innerHTML = str;\n chartBody.innerHTML = noDatastr.outerHTML;\n spinner.classList.add('hide'); // Hide spinner if not already hidden.\n return;\n }).catch(() => {\n Notification.exception(new Error('Failed to load string: nodata'));\n });\n }\n }).fail(() => {\n Notification.exception(new Error('Failed to load card.'));\n return;\n });\n });\n};\n\n/**\n * Initialise method for table handler.\n *\n * @param {array} cardsArray Cards array.\n * @param {int} contextIdChart The context id.\n * @param {string} fragmentChart Fragment name.\n * @param {string} templateChart Template name.\n */\nexport const init = (cardsArray, contextIdChart, fragmentChart, templateChart) => {\n cards = cardsArray;\n contextId = contextIdChart;\n fragment = fragmentChart;\n template = templateChart;\n};\n"],"names":["cards","contextId","fragment","template","quizId","hoursFilter","yearSelect","forEach","cardData","cardElement","document","getElementById","cardId","spinner","getElementsByClassName","chartBody","values","call","hoursahead","hoursbehind","quiz","year","params","JSON","stringify","classList","remove","loadFragment","done","response","resObj","parse","hasdata","context","chart","aspect","render","html","js","add","replaceNodeContents","fail","exception","Error","Str","get_string","then","str","noDatastr","createElement","innerHTML","outerHTML","catch","cardsArray","contextIdChart","fragmentChart","templateChart"],"mappings":";;;;;;;SA+BIA,MACAC,UACAC,SACAC,w6BAWyB,CAACC,OAAQC,YAAaC,cAC/CN,MAAMO,SAASC,eACPC,YAAcC,SAASC,eAAeH,SAASI,QAC/CC,QAAUJ,YAAYK,uBAAuB,0BAA0B,GACvEC,UAAYN,YAAYK,uBAAuB,cAAc,GAC7DE,OAAS,MAASR,SAASS,MAE3BZ,cACAW,OAAOE,WAAab,YAAY,GAChCW,OAAOG,YAAcd,YAAY,IAEjCD,SACAY,OAAOI,KAAOhB,QAEdE,aACAU,OAAOK,KAAOf,gBAEdgB,OAAS,MAASC,KAAKC,UAAUR,SAErCH,QAAQY,UAAUC,OAAO,0BAChBC,aAAa,mBAAoBzB,SAAUD,UAAWqB,QAC1DM,MAAMC,eACCC,OAASP,KAAKQ,MAAMF,cACD,IAAnBC,OAAOE,QAAkB,KACrBC,QAAU,YACG,YAAmBV,KAAKC,UAAUM,OAAOI,oBAE3B,IAApB1B,SAAS2B,SAChBF,QAAQE,OAAS3B,SAAS2B,gCAEpBC,OAAOjC,SAAU8B,SAASL,MAAK,CAACS,KAAMC,MAC5CzB,QAAQY,UAAUc,IAAI,2BAEZC,oBAAoBzB,UAAWsB,KAAMC,OAChDG,MAAK,2BACSC,UAAU,IAAIC,MAAM,sCAKrCC,IAAIC,WAAW,SAAU,oBAAoBC,MAAMC,YACzCC,UAAYtC,SAASuC,cAAc,MACzCD,UAAUE,UAAYH,IACtBhC,UAAUmC,UAAYF,UAAUG,UAChCtC,QAAQY,UAAUc,IAAI,WAEvBa,OAAM,2BACQV,UAAU,IAAIC,MAAM,wCAG1CF,MAAK,2BACSC,UAAU,IAAIC,MAAM,8CAc7B,CAACU,WAAYC,eAAgBC,cAAeC,iBAC5DxD,MAAQqD,WACRpD,UAAYqD,eACZpD,SAAWqD,cACXpD,SAAWqD"}
\ No newline at end of file
diff --git a/amd/build/chart_output_chartjs.min.js b/amd/build/chart_output_chartjs.min.js
deleted file mode 100644
index f2ae8904..00000000
--- a/amd/build/chart_output_chartjs.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Chart output for chart.js with custom override for aspect config.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define("local_assessfreq/chart_output_chartjs",["core/chart_output_chartjs"],(function(Output){var ChartOutput={},aspectRatio=!1,rtLegendoptions=!1;return Output.prototype._makeConfig=function(){var config={type:this._getChartType(),data:{labels:this._cleanData(this._chart.getLabels()),datasets:this._makeDatasetsConfig()},options:{title:{display:null!==this._chart.getTitle(),text:this._cleanData(this._chart.getTitle())}}},legendOptions=this._chart.getLegendOptions();return legendOptions&&(config.options.legend=legendOptions),rtLegendoptions&&(config.options.legend=rtLegendoptions),this._chart.getXAxes().forEach(function(axis,i){var axisLabels=axis.getLabels();config.options.scales=config.options.scales||{},config.options.scales.xAxes=config.options.scales.xAxes||[],config.options.scales.xAxes[i]=this._makeAxisConfig(axis,"x",i),null!==axisLabels&&(config.options.scales.xAxes[i].ticks.callback=function(value,index){return axisLabels[index]||""}),config.options.scales.xAxes[i].stacked=this._isStacked()}.bind(this)),this._chart.getYAxes().forEach(function(axis,i){var axisLabels=axis.getLabels();config.options.scales=config.options.scales||{},config.options.scales.yAxes=config.options.scales.yAxes||[],config.options.scales.yAxes[i]=this._makeAxisConfig(axis,"y",i),null!==axisLabels&&(config.options.scales.yAxes[i].ticks.callback=function(value){return axisLabels[parseInt(value,10)]||""}),config.options.scales.yAxes[i].stacked=this._isStacked()}.bind(this)),config.options.tooltips={callbacks:{label:this._makeTooltip.bind(this)}},config.options.maintainAspectRatio=aspectRatio,config},ChartOutput.init=function(chartImage,ChartInst,aspect,legend){aspectRatio=aspect,rtLegendoptions=legend,new Output(chartImage,ChartInst)},ChartOutput}));
-
-//# sourceMappingURL=chart_output_chartjs.min.js.map
\ No newline at end of file
diff --git a/amd/build/chart_output_chartjs.min.js.map b/amd/build/chart_output_chartjs.min.js.map
deleted file mode 100644
index 65756f59..00000000
--- a/amd/build/chart_output_chartjs.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"chart_output_chartjs.min.js","sources":["../src/chart_output_chartjs.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 output for chart.js with custom override for aspect config.\n *\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['core/chart_output_chartjs'], function (Output) {\n\n /**\n * Module level variables.\n */\n var ChartOutput = {};\n var aspectRatio = false;\n var rtLegendoptions = false;\n\n /**\n * Overrride the config.\n *\n * @protected\n * @return {Object} The axis config.\n */\n Output.prototype._makeConfig = function () {\n var config = {\n type: this._getChartType(),\n data: {\n labels: this._cleanData(this._chart.getLabels()),\n datasets: this._makeDatasetsConfig()\n },\n options: {\n title: {\n display: this._chart.getTitle() !== null,\n text: this._cleanData(this._chart.getTitle())\n }\n }\n };\n var legendOptions = this._chart.getLegendOptions();\n if (legendOptions) {\n config.options.legend = legendOptions;\n }\n\n // Override legend options with those provided at run time.\n if (rtLegendoptions) {\n config.options.legend = rtLegendoptions;\n }\n\n this._chart.getXAxes().forEach(function (axis, i) {\n var axisLabels = axis.getLabels();\n\n config.options.scales = config.options.scales || {};\n config.options.scales.xAxes = config.options.scales.xAxes || [];\n config.options.scales.xAxes[i] = this._makeAxisConfig(axis, 'x', i);\n\n if (axisLabels !== null) {\n config.options.scales.xAxes[i].ticks.callback = function (value, index) {\n return axisLabels[index] || '';\n };\n }\n config.options.scales.xAxes[i].stacked = this._isStacked();\n }.bind(this));\n\n this._chart.getYAxes().forEach(function (axis, i) {\n var axisLabels = axis.getLabels();\n\n config.options.scales = config.options.scales || {};\n config.options.scales.yAxes = config.options.scales.yAxes || [];\n config.options.scales.yAxes[i] = this._makeAxisConfig(axis, 'y', i);\n\n if (axisLabels !== null) {\n config.options.scales.yAxes[i].ticks.callback = function (value) {\n return axisLabels[parseInt(value, 10)] || '';\n };\n }\n config.options.scales.yAxes[i].stacked = this._isStacked();\n }.bind(this));\n\n config.options.tooltips = {\n callbacks: {\n label: this._makeTooltip.bind(this)\n }\n };\n\n config.options.maintainAspectRatio = aspectRatio;\n\n return config;\n };\n\n /**\n * Get the aspect ratio setting and initialise the chart.\n *\n * @param {string} chartImage The image to replace.\n * @param {object} ChartInst The chart instance.\n * @param {boolean} aspect The aspect ratio.\n * @param {object} legend The legend options.\n */\n ChartOutput.init = function (chartImage, ChartInst, aspect, legend) {\n aspectRatio = aspect;\n rtLegendoptions = legend;\n new Output(chartImage, ChartInst);\n };\n\n return ChartOutput;\n\n});\n"],"names":["define","Output","ChartOutput","aspectRatio","rtLegendoptions","prototype","_makeConfig","config","type","this","_getChartType","data","labels","_cleanData","_chart","getLabels","datasets","_makeDatasetsConfig","options","title","display","getTitle","text","legendOptions","getLegendOptions","legend","getXAxes","forEach","axis","i","axisLabels","scales","xAxes","_makeAxisConfig","ticks","callback","value","index","stacked","_isStacked","bind","getYAxes","yAxes","parseInt","tooltips","callbacks","label","_makeTooltip","maintainAspectRatio","init","chartImage","ChartInst","aspect"],"mappings":";;;;;;AAqBAA,+CAAO,CAAC,8BAA8B,SAAUC,YAKxCC,YAAc,GACdC,aAAc,EACdC,iBAAkB,SAQtBH,OAAOI,UAAUC,YAAc,eACvBC,OAAS,CACTC,KAAMC,KAAKC,gBACXC,KAAM,CACFC,OAAQH,KAAKI,WAAWJ,KAAKK,OAAOC,aACpCC,SAAUP,KAAKQ,uBAEnBC,QAAS,CACLC,MAAO,CACHC,QAAoC,OAA3BX,KAAKK,OAAOO,WACrBC,KAAMb,KAAKI,WAAWJ,KAAKK,OAAOO,eAI1CE,cAAgBd,KAAKK,OAAOU,0BAC5BD,gBACAhB,OAAOW,QAAQO,OAASF,eAIxBnB,kBACAG,OAAOW,QAAQO,OAASrB,sBAGvBU,OAAOY,WAAWC,QAAQ,SAAUC,KAAMC,OACvCC,WAAaF,KAAKb,YAEtBR,OAAOW,QAAQa,OAASxB,OAAOW,QAAQa,QAAU,GACjDxB,OAAOW,QAAQa,OAAOC,MAAQzB,OAAOW,QAAQa,OAAOC,OAAS,GAC7DzB,OAAOW,QAAQa,OAAOC,MAAMH,GAAKpB,KAAKwB,gBAAgBL,KAAM,IAAKC,GAE9C,OAAfC,aACAvB,OAAOW,QAAQa,OAAOC,MAAMH,GAAGK,MAAMC,SAAW,SAAUC,MAAOC,cACtDP,WAAWO,QAAU,KAGpC9B,OAAOW,QAAQa,OAAOC,MAAMH,GAAGS,QAAU7B,KAAK8B,cAChDC,KAAK/B,YAEFK,OAAO2B,WAAWd,QAAQ,SAAUC,KAAMC,OACvCC,WAAaF,KAAKb,YAEtBR,OAAOW,QAAQa,OAASxB,OAAOW,QAAQa,QAAU,GACjDxB,OAAOW,QAAQa,OAAOW,MAAQnC,OAAOW,QAAQa,OAAOW,OAAS,GAC7DnC,OAAOW,QAAQa,OAAOW,MAAMb,GAAKpB,KAAKwB,gBAAgBL,KAAM,IAAKC,GAE9C,OAAfC,aACAvB,OAAOW,QAAQa,OAAOW,MAAMb,GAAGK,MAAMC,SAAW,SAAUC,cAC/CN,WAAWa,SAASP,MAAO,MAAQ,KAGlD7B,OAAOW,QAAQa,OAAOW,MAAMb,GAAGS,QAAU7B,KAAK8B,cAChDC,KAAK/B,OAEPF,OAAOW,QAAQ0B,SAAW,CACtBC,UAAW,CACPC,MAAOrC,KAAKsC,aAAaP,KAAK/B,QAItCF,OAAOW,QAAQ8B,oBAAsB7C,YAE9BI,QAWXL,YAAY+C,KAAO,SAAUC,WAAYC,UAAWC,OAAQ3B,QACxDtB,YAAciD,OACdhD,gBAAkBqB,WACdxB,OAAOiD,WAAYC,YAGpBjD"}
\ No newline at end of file
diff --git a/amd/build/course_selector.min.js b/amd/build/course_selector.min.js
index b3a9f75b..ea4541ef 100644
--- a/amd/build/course_selector.min.js
+++ b/amd/build/course_selector.min.js
@@ -7,6 +7,6 @@
* @copyright2016 Frédéric Massart - FMCorz.net
* @licensehttp://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("local_assessfreq/course_selector",["core/ajax","core/notification"],(function(Ajax,Notification){var CourseSelector={transport:function(selector,query,callback){Ajax.call([{methodname:"local_assessfreq_get_courses",args:{query:query}}])[0].then((response=>{let courseArray=JSON.parse(response);callback(courseArray)})).fail((()=>{Notification.exception(new Error("Failed to get events"))}))},processResults:function(selector,results){let options=[];return results.forEach((element=>{options.push({value:element.id,label:element.fullname})})),options}};return CourseSelector}));
+define("local_assessfreq/course_selector",["core/ajax","core/notification"],(function(Ajax,Notification){let CourseSelector={transport:function(selector,query,callback){Ajax.call([{methodname:"local_assessfreq_get_courses",args:{query:query}}])[0].then((response=>{let courseArray=JSON.parse(response);callback(courseArray)}))},processResults:function(selector,results){let options=[];return results.forEach((element=>{options.push({value:element.id,label:element.fullname})})),options}};return CourseSelector}));
//# sourceMappingURL=course_selector.min.js.map
\ No newline at end of file
diff --git a/amd/build/course_selector.min.js.map b/amd/build/course_selector.min.js.map
index 21d71eb6..fe26f533 100644
--- a/amd/build/course_selector.min.js.map
+++ b/amd/build/course_selector.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"course_selector.min.js","sources":["../src/course_selector.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 * Frameworks datasource.\n *\n * This module is compatible with core/form-autocomplete.\n *\n * @packagetool_lpmigrate\n * @copyright2016 Frédéric Massart - FMCorz.net\n * @licensehttp://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['core/ajax', 'core/notification'], function (Ajax, Notification) {\n\n /**\n * Module level variables.\n */\n var CourseSelector = {};\n\n /**\n * Source of data for Ajax element.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {String} query The query string.\n * @param {Function} callback A callback function receiving an array of results.\n * @return {Void}\n */\n CourseSelector.transport = function (selector, query, callback) {\n Ajax.call([{\n methodname: 'local_assessfreq_get_courses',\n args: {\n query: query\n },\n }])[0].then((response) => {\n let courseArray = JSON.parse(response);\n callback(courseArray);\n }).fail(() => {\n Notification.exception(new Error('Failed to get events'));\n });\n };\n\n /**\n * Process the results for auto complete elements.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {Array} results An array or results.\n * @return {Array} New array of results.\n */\n CourseSelector.processResults = function (selector, results) {\n let options = [];\n results.forEach((element) => {\n options.push({\n value: element.id,\n label: element.fullname\n });\n });\n\n return options;\n };\n\n return CourseSelector;\n});\n"],"names":["define","Ajax","Notification","CourseSelector","selector","query","callback","call","methodname","args","then","response","courseArray","JSON","parse","fail","exception","Error","results","options","forEach","element","push","value","id","label","fullname"],"mappings":";;;;;;;;;AAyBAA,0CAAO,CAAC,YAAa,sBAAsB,SAAUC,KAAMC,kBAKnDC,eAAiB,CAUrBA,UAA2B,SAAUC,SAAUC,MAAOC,UAClDL,KAAKM,KAAK,CAAC,CACPC,WAAY,+BACZC,KAAM,CACFJ,MAAOA,UAEX,GAAGK,MAAMC,eACLC,YAAcC,KAAKC,MAAMH,UAC7BL,SAASM,gBACVG,MAAK,KACJb,aAAac,UAAU,IAAIC,MAAM,6BAWzCd,eAAgC,SAAUC,SAAUc,aAC5CC,QAAU,UACdD,QAAQE,SAASC,UACbF,QAAQG,KAAK,CACTC,MAAOF,QAAQG,GACfC,MAAOJ,QAAQK,cAIhBP,iBAGJhB"}
\ No newline at end of file
+{"version":3,"file":"course_selector.min.js","sources":["../src/course_selector.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 * Frameworks datasource.\n *\n * This module is compatible with core/form-autocomplete.\n *\n * @packagetool_lpmigrate\n * @copyright2016 Frédéric Massart - FMCorz.net\n * @licensehttp://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['core/ajax', 'core/notification'], function (Ajax, Notification) {\n\n /**\n * Module level variables.\n */\n let CourseSelector = {};\n\n /**\n * Source of data for Ajax element.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {String} query The query string.\n * @param {Function} callback A callback function receiving an array of results.\n */\n CourseSelector.transport = function(selector, query, callback) {\n Ajax.call([{\n methodname: 'local_assessfreq_get_courses',\n args: {\n query: query\n },\n }])[0].then((response) => {\n let courseArray = JSON.parse(response);\n // eslint-disable-next-line promise/no-callback-in-promise\n callback(courseArray);\n });\n };\n\n /**\n * Process the results for auto complete elements.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {Array} results An array or results.\n * @return {Array} New array of results.\n */\n CourseSelector.processResults = function (selector, results) {\n let options = [];\n results.forEach((element) => {\n options.push({\n value: element.id,\n label: element.fullname\n });\n });\n\n return options;\n };\n\n return CourseSelector;\n});\n"],"names":["define","Ajax","Notification","CourseSelector","selector","query","callback","call","methodname","args","then","response","courseArray","JSON","parse","results","options","forEach","element","push","value","id","label","fullname"],"mappings":";;;;;;;;;AAyBAA,0CAAO,CAAC,YAAa,sBAAsB,SAAUC,KAAMC,kBAKnDC,eAAiB,CASrBA,UAA2B,SAASC,SAAUC,MAAOC,UACjDL,KAAKM,KAAK,CAAC,CACPC,WAAY,+BACZC,KAAM,CACFJ,MAAOA,UAEX,GAAGK,MAAMC,eACLC,YAAcC,KAAKC,MAAMH,UAE7BL,SAASM,iBAWjBT,eAAgC,SAAUC,SAAUW,aAC5CC,QAAU,UACdD,QAAQE,SAASC,UACbF,QAAQG,KAAK,CACTC,MAAOF,QAAQG,GACfC,MAAOJ,QAAQK,cAIhBP,iBAGJb"}
\ No newline at end of file
diff --git a/amd/build/dashboard.min.js b/amd/build/dashboard.min.js
new file mode 100644
index 00000000..eb2054c7
--- /dev/null
+++ b/amd/build/dashboard.min.js
@@ -0,0 +1,12 @@
+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
new file mode 100644
index 00000000..e15b07a5
--- /dev/null
+++ b/amd/build/dashboard.min.js.map
@@ -0,0 +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\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/dashboard_assessment.min.js b/amd/build/dashboard_assessment.min.js
deleted file mode 100644
index 913662d3..00000000
--- a/amd/build/dashboard_assessment.min.js
+++ /dev/null
@@ -1,10 +0,0 @@
-define("local_assessfreq/dashboard_assessment",["exports","core/notification","local_assessfreq/calendar","local_assessfreq/chart_data","local_assessfreq/dayview","local_assessfreq/user_preferences","local_assessfreq/zoom_modal"],(function(_exports,_notification,_calendar,ChartData,_dayview,UserPreference,_zoom_modal){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 _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
-/**
- * Javascript for report card display and processing.
- *
- * @module local_assessfreq/dashboard_assessment
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */var contextid,yearselect,yearselectheatmap,metricselectheatmap,timeout;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_calendar=_interopRequireDefault(_calendar),ChartData=_interopRequireWildcard(ChartData),_dayview=_interopRequireDefault(_dayview),UserPreference=_interopRequireWildcard(UserPreference),_zoom_modal=_interopRequireDefault(_zoom_modal);var modulesJson="",heatmapOptionsJson="";const cards=[{cardId:"local-assessfreq-assess-due-month",call:"assess_by_month"},{cardId:"local-assessfreq-assess-by-activity",call:"assess_by_activity"},{cardId:"local-assessfreq-assess-due-month-student",call:"assess_by_month_student"}],yearButtonAction=event=>{event.preventDefault();var element=event.target;"a"===element.tagName.toLowerCase()&&element.dataset.year!==yearselect&&(yearselect=element.dataset.year,UserPreference.setUserPreference("local_assessfreq_overview_year_preference",yearselect),document.getElementById("local-assessfreq-report-overview").getElementsByClassName("local-assessfreq-year")[0].innerHTML=yearselect,ChartData.getCardCharts(0,null,yearselect))},updateHeatmapDebounce=()=>{clearTimeout(timeout),timeout=setTimeout(updateHeatmap(),750)},detailView=event=>{let element=event.target;"td"===element.tagName.toLowerCase()&&"true"===element.dataset.event&&_dayview.default.display(element.dataset.date)},updateHeatmap=()=>{for(var links=document.getElementById("local-assessfreq-heatmap-modules").getElementsByTagName("a"),modules=[],i=0;i{let heatmapOptions=JSON.parse(heatmapOptionsJson),year=parseInt(heatmapOptions.year),metric=heatmapOptions.metric,modules=heatmapOptions.modules,spinner=document.getElementById("local-assessfreq-report-heatmap").getElementsByClassName("overlay-icon-container")[0];spinner.classList.remove("hide"),_calendar.default.generate(year,0,11,metric,modules).then((calendar=>{let calendarContainer=document.getElementById("local-assessfreq-report-heatmap-months");calendarContainer.innerHTML=calendar.innerHTML,calendarContainer.addEventListener("click",detailView)})).then(_calendar.default.createHeatScale).then((heatScale=>{document.getElementById("local-assessfreq-report-heatmap-scale").innerHTML=heatScale.outerHTML,spinner.classList.add("hide")})).catch((()=>{_notification.default.exception(new Error("Failed to calendar."))}))})(),(_ref=>{let{year:year,metric:metric,modules:modules}=_ref,downloadForm=document.getElementById("local-assessfreq-heatmap-form"),formElements=downloadForm.elements,toRemove=new Array;0===modules.length&&(modules=["all"]);for(let i=0;i{event.preventDefault();var element=event.target;"a"===element.tagName.toLowerCase()&&element.dataset.year!==yearselectheatmap&&(yearselectheatmap=element.dataset.year,UserPreference.setUserPreference("local_assessfreq_heatmap_year_preference",yearselectheatmap),document.getElementById("local-assessfreq-report-heatmap").getElementsByClassName("local-assessfreq-year")[0].innerHTML=yearselectheatmap,updateHeatmapDebounce())},metricHeatmapButtonAction=event=>{event.preventDefault();var element=event.target;"a"===element.tagName.toLowerCase()&&element.dataset.metric!==metricselectheatmap&&(metricselectheatmap=element.dataset.metric,UserPreference.setUserPreference("local_assessfreq_heatmap_metric_preference",metricselectheatmap),updateHeatmapDebounce())},triggerZoomGraph=event=>{let call=event.target.closest("div").dataset.call,params={data:JSON.stringify({year:yearselect,call:call})};_zoom_modal.default.zoomGraph(event,params,"get_chart")};_exports.init=context=>{contextid=context;let cardsYearSelectElement=document.getElementById("local-assessfreq-cards-year");yearselect=cardsYearSelectElement.getElementsByClassName("active")[0].dataset.year,cardsYearSelectElement.addEventListener("click",yearButtonAction);let cardsYearSelectHeatmapElement=document.getElementById("local-assessfreq-heatmap-year");yearselectheatmap=cardsYearSelectHeatmapElement.getElementsByClassName("active")[0].dataset.year,cardsYearSelectHeatmapElement.addEventListener("click",yearHeatmapButtonAction);let cardsMetricSelectHeatmapElement=document.getElementById("local-assessfreq-heatmap-metrics");metricselectheatmap=cardsMetricSelectHeatmapElement.getElementsByClassName("active")[0].dataset.metric,cardsMetricSelectHeatmapElement.addEventListener("click",metricHeatmapButtonAction),(element=>{for(var links=element.getElementsByTagName("a"),all=links[0],i=0;i.\n\n/**\n * Javascript for report card display and processing.\n *\n * @module local_assessfreq/dashboard_assessment\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport Calendar from 'local_assessfreq/calendar';\nimport * as ChartData from 'local_assessfreq/chart_data';\nimport Dayview from 'local_assessfreq/dayview';\nimport * as UserPreference from 'local_assessfreq/user_preferences';\nimport ZoomModal from 'local_assessfreq/zoom_modal';\n\n/**\n * Module level variables.\n */\nvar contextid;\nvar yearselect;\nvar yearselectheatmap;\nvar metricselectheatmap;\nvar timeout;\nvar modulesJson = '';\nvar heatmapOptionsJson = '';\n\nconst cards = [\n {cardId: 'local-assessfreq-assess-due-month', call: 'assess_by_month'},\n {cardId: 'local-assessfreq-assess-by-activity', call: 'assess_by_activity'},\n {cardId: 'local-assessfreq-assess-due-month-student', call: 'assess_by_month_student'}\n];\n\n/**\n * Get and process the selected year from the dropdown,\n * and update the corresponding user perference.\n *\n * @param {event} event The triggered event for the element.\n */\nconst yearButtonAction = (event) => {\n event.preventDefault();\n var element = event.target;\n\n if (element.tagName.toLowerCase() === 'a' && element.dataset.year !== yearselect) { // Only act on certain elements.\n yearselect = element.dataset.year;\n\n // Save selection as a user preference.\n UserPreference.setUserPreference('local_assessfreq_overview_year_preference', yearselect);\n\n // Update card data based on selected year.\n var yeartitle = document.getElementById('local-assessfreq-report-overview')\n .getElementsByClassName('local-assessfreq-year')[0];\n yeartitle.innerHTML = yearselect;\n\n ChartData.getCardCharts(0, null, yearselect); // Process loading for the assessment cards.\n }\n};\n\n/**\n * Quick and dirty debounce method for the heatmap settings menu.\n * This stops the ajax method that updates the heatmap from being updated\n * while the user is still checking options.\n *\n */\nconst updateHeatmapDebounce = () => {\n clearTimeout(timeout);\n timeout = setTimeout(updateHeatmap(), 750);\n};\n\n/**\n * Display heatmap calendar.\n *\n * @param {event} event The triggered event for the element.\n */\nconst detailView = (event) => {\n let element = event.target;\n if (element.tagName.toLowerCase() === 'td' && element.dataset.event === 'true') { // Only act on certain elements.\n Dayview.display(element.dataset.date);\n }\n};\n\n/**\n * Start heatmap generation.\n *\n */\nconst generateHeatmap = () => {\n let heatmapOptions = JSON.parse(heatmapOptionsJson);\n let year = parseInt(heatmapOptions.year);\n let metric = heatmapOptions.metric;\n let modules = heatmapOptions.modules;\n let heatmapContainer = document.getElementById('local-assessfreq-report-heatmap');\n let spinner = heatmapContainer.getElementsByClassName('overlay-icon-container')[0];\n\n spinner.classList.remove('hide'); // Show spinner if not already shown.\n\n Calendar.generate(year, 0, 11, metric, modules)\n .then(calendar => {\n let calendarContainer = document.getElementById('local-assessfreq-report-heatmap-months');\n calendarContainer.innerHTML = calendar.innerHTML;\n calendarContainer.addEventListener('click', detailView);\n })\n .then(Calendar.createHeatScale)\n .then((heatScale) => {\n let heatScaleContainer = document.getElementById('local-assessfreq-report-heatmap-scale');\n heatScaleContainer.innerHTML = heatScale.outerHTML;\n spinner.classList.add('hide'); // Hide sinner if not already hidden.\n })\n .catch(() => {\n Notification.exception(new Error('Failed to calendar.'));\n return;\n });\n};\n\nconst updateDownload = ({year, metric, modules}) => {\n let downloadForm = document.getElementById('local-assessfreq-heatmap-form');\n let formElements = downloadForm.elements;\n let toRemove = new Array();\n\n if (modules.length === 0) {\n modules = ['all'];\n }\n\n for (let i = 0; i < formElements.length; i++) {\n if (formElements[i] === undefined) {\n continue;\n }\n // Update year field.\n if ((formElements[i].type === 'hidden') && (formElements[i].name === 'year')) {\n formElements[i].value = year;\n continue;\n }\n\n // Update metric field.\n if ((formElements[i].type === 'hidden') && (formElements[i].name === 'metric')) {\n formElements[i].value = metric;\n continue;\n }\n\n // Update module fields.\n if ((formElements[i].type === 'hidden') && (formElements[i].name.startsWith('modules'))) {\n toRemove.push(formElements[i]);\n continue;\n }\n }\n\n for (const element of toRemove) {\n element.remove();\n }\n\n for (let i = 0; i < modules.length; i++) {\n let input = document.createElement('input');\n input.type = 'hidden';\n input.name = 'modules[' + modules[i] + ']';\n input.value = modules[i];\n\n downloadForm.appendChild(input);\n }\n};\n\n/**\n * Update the heatmap based on the current filter settings.\n *\n */\nconst updateHeatmap = () => {\n // Get current state of select menu items.\n var cardsModulesSelectHeatmapElement = document.getElementById('local-assessfreq-heatmap-modules');\n var links = cardsModulesSelectHeatmapElement.getElementsByTagName('a');\n var modules = [];\n\n for (var i = 0; i < links.length; i++) {\n if (links[i].classList.contains('active')) {\n let module = links[i].dataset.module;\n modules.push(module);\n }\n }\n\n // Save selection as a user preference.\n if (modulesJson !== JSON.stringify(modules)) {\n modulesJson = JSON.stringify(modules);\n UserPreference.setUserPreference('local_assessfreq_heatmap_modules_preference', modulesJson);\n }\n\n // Build settings object.\n var optionsObj = {\n 'year': yearselectheatmap,\n 'metric': metricselectheatmap,\n 'modules': modules\n };\n\n var optionsJson = JSON.stringify(optionsObj);\n\n if (optionsJson !== heatmapOptionsJson) { // Compare to global to see if there are any changes.\n // If list has changed fetch heatmap and update user preference.\n heatmapOptionsJson = optionsJson;\n generateHeatmap();\n\n // Update the download options.\n updateDownload(optionsObj);\n }\n};\n\n/**\n * Get and process the selected year 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 */\nconst yearHeatmapButtonAction = (event) => {\n event.preventDefault();\n var element = event.target;\n\n if (element.tagName.toLowerCase() === 'a' && element.dataset.year !== yearselectheatmap) { // Only act on certain elements.\n yearselectheatmap = element.dataset.year;\n\n // Save selection as a user preference.\n UserPreference.setUserPreference('local_assessfreq_heatmap_year_preference', yearselectheatmap);\n\n // Update card data based on selected year.\n var yeartitle = document.getElementById('local-assessfreq-report-heatmap')\n .getElementsByClassName('local-assessfreq-year')[0];\n yeartitle.innerHTML = yearselectheatmap;\n\n updateHeatmapDebounce(); // Call function to update heatmap.\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 */\nconst metricHeatmapButtonAction = (event) => {\n event.preventDefault();\n var element = event.target;\n\n if (element.tagName.toLowerCase() === 'a' && element.dataset.metric !== metricselectheatmap) {\n metricselectheatmap = element.dataset.metric;\n\n // Save selection as a user preference.\n UserPreference.setUserPreference('local_assessfreq_heatmap_metric_preference', metricselectheatmap);\n\n updateHeatmapDebounce(); // Call function to update heatmap.\n }\n};\n\n/**\n * Add the event listeners to the modules in the module select dropdown.\n *\n * @param {Object} element The dropdown HTML element that contains the list of modules as links.\n */\nconst moduleListChildrenEvents = (element) => {\n var links = element.getElementsByTagName('a');\n var all = links[0];\n\n for (var i = 0; i < links.length; i++) {\n let module = links[i].dataset.module;\n\n if (module.toLowerCase() === 'all') {\n links[i].addEventListener('click', function (event) {\n event.preventDefault();\n // Remove active class from all other links.\n for (var j = 0; j < links.length; j++) {\n links[j].classList.remove('active');\n }\n updateHeatmapDebounce(); // Call function to update heatmap.\n });\n } else if (module.toLowerCase() === 'close') {\n links[i].addEventListener('click', function (event) {\n event.preventDefault();\n event.stopPropagation();\n\n var dropdownmenu = document.getElementById('local-assessfreq-heatmap-modules-filter');\n dropdownmenu.classList.remove('show');\n\n updateHeatmapDebounce(); // Call function to update heatmap.\n });\n } else {\n links[i].addEventListener('click', function (event) {\n event.preventDefault();\n event.stopPropagation();\n\n all.classList.remove('active');\n\n event.target.classList.toggle('active');\n updateHeatmapDebounce();\n });\n }\n }\n};\n\n/**\n * Thin wrapper to add extra data to click event.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst triggerZoomGraph = (event) => {\n let call = event.target.closest('div').dataset.call;\n let params = {'data': JSON.stringify({'year': yearselect, 'call': call})};\n let method = 'get_chart';\n\n ZoomModal.zoomGraph(event, params, method);\n};\n\n/**\n * Initialise method for report card rendering.\n *\n * @param {integer} context The current context id.\n */\nexport const init = (context) => {\n contextid = context;\n\n // Set up event listener and related actions for year dropdown on report cards.\n let cardsYearSelectElement = document.getElementById('local-assessfreq-cards-year');\n yearselect = cardsYearSelectElement.getElementsByClassName('active')[0].dataset.year;\n cardsYearSelectElement.addEventListener('click', yearButtonAction);\n\n // Set up event listener and related actions for year dropdown on heatmp.\n let cardsYearSelectHeatmapElement = document.getElementById('local-assessfreq-heatmap-year');\n yearselectheatmap = cardsYearSelectHeatmapElement.getElementsByClassName('active')[0].dataset.year;\n cardsYearSelectHeatmapElement.addEventListener('click', yearHeatmapButtonAction);\n\n // Set up event listener and related actions for metric dropdown on heatmp.\n let cardsMetricSelectHeatmapElement = document.getElementById('local-assessfreq-heatmap-metrics');\n metricselectheatmap = cardsMetricSelectHeatmapElement.getElementsByClassName('active')[0].dataset.metric;\n cardsMetricSelectHeatmapElement.addEventListener('click', metricHeatmapButtonAction);\n\n // Set up event listener and related actions for module dropdown on heatmp.\n let cardsModulesSelectHeatmapElement = document.getElementById('local-assessfreq-heatmap-modules');\n moduleListChildrenEvents(cardsModulesSelectHeatmapElement);\n\n // Set up zoom event listeners.\n let dueMonthZoom = document.getElementById('local-assessfreq-assess-due-month-zoom');\n dueMonthZoom.addEventListener('click', triggerZoomGraph);\n\n let dueActivityZoom = document.getElementById('local-assessfreq-assess-by-activity-zoom');\n dueActivityZoom.addEventListener('click', triggerZoomGraph);\n\n let dueStudentZoom = document.getElementById('local-assessfreq-assess-due-month-student-zoom');\n dueStudentZoom.addEventListener('click', triggerZoomGraph);\n\n // Create the zoom modal.\n ZoomModal.init(context);\n\n // Setup the dayview modal.\n Dayview.init();\n\n // Setup the chart data for each card.\n ChartData.init(cards, contextid, 'get_chart', 'core/chart');\n\n // Process loading for the assessment cards.\n ChartData.getCardCharts(0, null, yearselect);\n\n // Get the data for the heatmap.\n updateHeatmap();\n\n};\n"],"names":["contextid","yearselect","yearselectheatmap","metricselectheatmap","timeout","modulesJson","heatmapOptionsJson","cards","cardId","call","yearButtonAction","event","preventDefault","element","target","tagName","toLowerCase","dataset","year","UserPreference","setUserPreference","document","getElementById","getElementsByClassName","innerHTML","ChartData","getCardCharts","updateHeatmapDebounce","clearTimeout","setTimeout","updateHeatmap","detailView","display","date","links","getElementsByTagName","modules","i","length","classList","contains","module","push","JSON","stringify","optionsObj","optionsJson","heatmapOptions","parse","parseInt","metric","spinner","remove","generate","then","calendar","calendarContainer","addEventListener","Calendar","createHeatScale","heatScale","outerHTML","add","catch","exception","Error","generateHeatmap","_ref","downloadForm","formElements","elements","toRemove","Array","undefined","type","name","startsWith","value","input","createElement","appendChild","updateDownload","yearHeatmapButtonAction","metricHeatmapButtonAction","triggerZoomGraph","closest","params","zoomGraph","context","cardsYearSelectElement","cardsYearSelectHeatmapElement","cardsMetricSelectHeatmapElement","all","j","stopPropagation","toggle","moduleListChildrenEvents","init"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/dashboard_quiz.min.js b/amd/build/dashboard_quiz.min.js
deleted file mode 100644
index 2682290f..00000000
--- a/amd/build/dashboard_quiz.min.js
+++ /dev/null
@@ -1,10 +0,0 @@
-define("local_assessfreq/dashboard_quiz",["exports","core/ajax","core/notification","core/str","core/templates","local_assessfreq/chart_data","local_assessfreq/form_modal","local_assessfreq/override_modal","local_assessfreq/table_handler","local_assessfreq/user_preferences","local_assessfreq/zoom_modal"],(function(_exports,_ajax,_notification,Str,_templates,ChartData,FormModal,_override_modal,TableHandler,UserPreference,ZoomModal){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 _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
-/**
- * Javascript for report card display and processing.
- *
- * @module local_assessfreq/dashboard_quiz
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),Str=_interopRequireWildcard(Str),_templates=_interopRequireDefault(_templates),ChartData=_interopRequireWildcard(ChartData),FormModal=_interopRequireWildcard(FormModal),_override_modal=_interopRequireDefault(_override_modal),TableHandler=_interopRequireWildcard(TableHandler),UserPreference=_interopRequireWildcard(UserPreference),ZoomModal=_interopRequireWildcard(ZoomModal);var contextid,counterid,selectQuizStr="",quizId=0,refreshPeriod=60;const cards=[{cardId:"local-assessfreq-quiz-summary-graph",call:"participant_summary",aspect:!0},{cardId:"local-assessfreq-quiz-summary-trend",call:"participant_trend",aspect:!1}],refreshCounter=function(){let reset=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],progressElement=document.getElementById("local-assessfreq-period-progress");!0===reset&&(clearInterval(counterid),counterid=null,progressElement.setAttribute("style","width: 100%"),progressElement.setAttribute("aria-valuenow",100)),counterid||(counterid=setInterval((()=>{let progressWidthAria=progressElement.getAttribute("aria-valuenow");const progressStep=100/refreshPeriod;progressWidthAria-progressStep>0?(progressElement.setAttribute("style","width: "+(progressWidthAria-progressStep)+"%"),progressElement.setAttribute("aria-valuenow",progressWidthAria-progressStep)):(clearInterval(counterid),counterid=null,progressElement.setAttribute("style","width: 100%"),progressElement.setAttribute("aria-valuenow",100),processDashboard(quizId),refreshCounter())}),1e3))},processDashboard=quiz=>{quizId=quiz;let titleElement=document.getElementById("local-assessfreq-quiz-title");titleElement.innerHTML=selectQuizStr,_ajax.default.call([{methodname:"local_assessfreq_get_quiz_data",args:{quizid:quiz}}])[0].then((response=>{let quizArray=JSON.parse(response),cardsElement=document.getElementById("local-assessfreq-quiz-dashboard-cards-deck"),trendElement=document.getElementById("local-assessfreq-quiz-dashboard-participant-trend-deck"),summarySpinner=document.getElementById("local-assessfreq-quiz-summary-card").getElementsByClassName("overlay-icon-container")[0],tableElement=document.getElementById("local-assessfreq-quiz-table"),periodElement=document.getElementById("local-assessfreq-period-container"),tableSearchInputElement=document.getElementById("local-assessfreq-quiz-student-table-search"),tableSearchResetElement=document.getElementById("local-assessfreq-quiz-student-table-search-reset"),tableSearchRowsElement=document.getElementById("local-assessfreq-quiz-student-table-rows"),quizLink=document.createElement("a");quizLink.href=quizArray.url,quizLink.innerHTML='',titleElement.innerHTML=quizArray.name+" ",titleElement.appendChild(quizLink);const currentdUrl=new URL(window.location.href),newUrl=currentdUrl.origin+currentdUrl.pathname+"?id="+quizId;history.pushState({},"",newUrl),Str.get_string("dashboard:quiztitle","local_assessfreq",{quiz:quizArray.name,course:quizArray.courseshortname}).then((str=>{document.title=str})).catch((()=>{_notification.default.exception(new Error("Failed to load string: dashboard:quiztitle"))})),_templates.default.render("local_assessfreq/quiz-summary-card-content",quizArray).done((html=>{summarySpinner.classList.add("hide");let contentcontainer=document.getElementById("local-assessfreq-quiz-summary-card-content");_templates.default.replaceNodeContents(contentcontainer,html,"")})).fail((()=>{_notification.default.exception(new Error("Failed to load quiz summary template."))})),cardsElement.classList.remove("hide"),trendElement.classList.remove("hide"),tableElement.classList.remove("hide"),periodElement.classList.remove("hide"),ChartData.getCardCharts(quizId),TableHandler.getTable(quizId),refreshCounter(),tableSearchInputElement.addEventListener("keyup",TableHandler.tableSearch),tableSearchInputElement.addEventListener("paste",TableHandler.tableSearch),tableSearchResetElement.addEventListener("click",TableHandler.tableSearchReset),tableSearchRowsElement.addEventListener("click",TableHandler.tableSearchRowSet)})).fail((()=>{_notification.default.exception(new Error("Failed to get quiz data"))}))},refreshAction=event=>{event.preventDefault();var element=event.target;null!==element.closest("button")&&"local-assessfreq-refresh-quiz-dashboard"===element.closest("button").id?(refreshCounter(!0),processDashboard(quizId)):"a"===element.tagName.toLowerCase()&&(refreshPeriod=element.dataset.period,refreshCounter(!0),UserPreference.setUserPreference("local_assessfreq_quiz_refresh_preference",refreshPeriod))},triggerZoomGraph=event=>{let call=event.target.closest("div").dataset.call,params={data:JSON.stringify({quiz:quizId,call:call})};ZoomModal.zoomGraph(event,params,"get_quiz_chart")};_exports.init=(context,quiz)=>{contextid=context,FormModal.init(context,processDashboard),ZoomModal.init(context),_override_modal.default.init(context,processDashboard),TableHandler.init(quizId,contextid,"local-assessfreq-quiz-student-table","local-assessfreq-quiz-table","get_student_table","local_assessfreq_quiz_table_rows_preference","local-assessfreq-quiz-student-table-search","local_assessfreq_student_table","local_assessfreq_set_table_preference"),ChartData.init(cards,context,"get_quiz_chart","local_assessfreq/chart"),Str.get_string("loadingquiztitle","local_assessfreq").then((str=>{selectQuizStr=str})).catch((()=>{_notification.default.exception(new Error("Failed to load string: loadingquiz"))})).then((()=>{quiz>0&&(quizId=quiz,processDashboard(quiz))})),UserPreference.getUserPreference("local_assessfreq_quiz_refresh_preference").then((response=>{refreshPeriod=response.preferences[0].value?response.preferences[0].value:60})).fail((()=>{_notification.default.exception(new Error("Failed to get use preference: refresh"))})),document.getElementById("local-assessfreq-period-container").addEventListener("click",refreshAction),document.getElementById("local-assessfreq-quiz-summary-graph-zoom").addEventListener("click",triggerZoomGraph),document.getElementById("local-assessfreq-quiz-summary-trend-zoom").addEventListener("click",triggerZoomGraph)}}));
-
-//# sourceMappingURL=dashboard_quiz.min.js.map
\ No newline at end of file
diff --git a/amd/build/dashboard_quiz.min.js.map b/amd/build/dashboard_quiz.min.js.map
deleted file mode 100644
index 9a8a67a5..00000000
--- a/amd/build/dashboard_quiz.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"dashboard_quiz.min.js","sources":["../src/dashboard_quiz.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 * Javascript for report card display and processing.\n *\n * @module local_assessfreq/dashboard_quiz\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\nimport * as Str from 'core/str';\nimport Templates from 'core/templates';\nimport * as ChartData from 'local_assessfreq/chart_data';\nimport * as FormModal from 'local_assessfreq/form_modal';\nimport OverrideModal from 'local_assessfreq/override_modal';\nimport * as TableHandler from 'local_assessfreq/table_handler';\nimport * as UserPreference from 'local_assessfreq/user_preferences';\nimport * as ZoomModal from 'local_assessfreq/zoom_modal';\n\n// Module level variables.\n\nvar selectQuizStr = '';\nvar contextid;\nvar quizId = 0;\nvar refreshPeriod = 60;\nvar counterid;\n\nconst cards = [\n {cardId: 'local-assessfreq-quiz-summary-graph', call: 'participant_summary', aspect: true},\n {cardId: 'local-assessfreq-quiz-summary-trend', call: 'participant_trend', aspect: false}\n];\n\n/**\n * Function for refreshing the counter.\n *\n * @param {boolean} reset the current count process.\n */\nconst refreshCounter = (reset = true) => {\n let progressElement = document.getElementById('local-assessfreq-period-progress');\n\n // Reset the current count process.\n if (reset === true) {\n clearInterval(counterid);\n counterid = null;\n progressElement.setAttribute('style', 'width: 100%');\n progressElement.setAttribute('aria-valuenow', 100);\n }\n\n // Exit early if there is already a counter running.\n if (counterid) {\n return;\n }\n\n counterid = setInterval(() => {\n let progressWidthAria = progressElement.getAttribute('aria-valuenow');\n const progressStep = 100 / refreshPeriod;\n\n if ((progressWidthAria - progressStep) > 0) {\n progressElement.setAttribute('style', 'width: ' + (progressWidthAria - progressStep) + '%');\n progressElement.setAttribute('aria-valuenow', (progressWidthAria - progressStep));\n } else {\n clearInterval(counterid);\n counterid = null;\n progressElement.setAttribute('style', 'width: 100%');\n progressElement.setAttribute('aria-valuenow', 100);\n processDashboard(quizId);\n refreshCounter();\n }\n }, (1000));\n};\n\n/**\n * Callback function that is called when a quiz is selected from the form.\n * Starts the processing of the dashboard.\n *\n * @param {int} quiz The quiz Id.\n */\nconst processDashboard = (quiz) => {\n quizId = quiz;\n let titleElement = document.getElementById('local-assessfreq-quiz-title');\n titleElement.innerHTML = selectQuizStr;\n // Get quiz data.\n Ajax.call([{\n methodname: 'local_assessfreq_get_quiz_data',\n args: {\n quizid: quiz\n },\n }])[0].then((response) => {\n\n let quizArray = JSON.parse(response);\n let cardsElement = document.getElementById('local-assessfreq-quiz-dashboard-cards-deck');\n let trendElement = document.getElementById('local-assessfreq-quiz-dashboard-participant-trend-deck');\n let summaryElement = document.getElementById('local-assessfreq-quiz-summary-card');\n let summarySpinner = summaryElement.getElementsByClassName('overlay-icon-container')[0];\n let tableElement = document.getElementById('local-assessfreq-quiz-table');\n let periodElement = document.getElementById('local-assessfreq-period-container');\n let tableSearchInputElement = document.getElementById('local-assessfreq-quiz-student-table-search');\n let tableSearchResetElement = document.getElementById('local-assessfreq-quiz-student-table-search-reset');\n let tableSearchRowsElement = document.getElementById('local-assessfreq-quiz-student-table-rows');\n\n let quizLink = document.createElement('a');\n quizLink.href = quizArray.url;\n quizLink.innerHTML = '';\n titleElement.innerHTML = quizArray.name + ' ';\n titleElement.appendChild(quizLink);\n\n // Update page URL with quiz ID, without reloading page so that page navigation and bookmarking works.\n const currentdUrl = new URL(window.location.href);\n const newUrl = currentdUrl.origin + currentdUrl.pathname + '?id=' + quizId;\n history.pushState({}, '', newUrl);\n\n // Update page title with quiz name.\n Str.get_string('dashboard:quiztitle', 'local_assessfreq', {'quiz': quizArray.name, 'course': quizArray.courseshortname})\n .then((str) => {\n document.title = str;\n }).catch(() => {\n Notification.exception(new Error('Failed to load string: dashboard:quiztitle'));\n });\n\n // Populate quiz summary card with details.\n Templates.render('local_assessfreq/quiz-summary-card-content', quizArray).done((html) => {\n summarySpinner.classList.add('hide');\n let contentcontainer = document.getElementById('local-assessfreq-quiz-summary-card-content');\n Templates.replaceNodeContents(contentcontainer, html, '');\n }).fail(() => {\n Notification.exception(new Error('Failed to load quiz summary template.'));\n return;\n });\n\n // Show the cards.\n cardsElement.classList.remove('hide');\n trendElement.classList.remove('hide');\n tableElement.classList.remove('hide');\n periodElement.classList.remove('hide');\n\n ChartData.getCardCharts(quizId);\n TableHandler.getTable(quizId);\n refreshCounter();\n\n tableSearchInputElement.addEventListener('keyup', TableHandler.tableSearch);\n tableSearchInputElement.addEventListener('paste', TableHandler.tableSearch);\n tableSearchResetElement.addEventListener('click', TableHandler.tableSearchReset);\n tableSearchRowsElement.addEventListener('click', TableHandler.tableSearchRowSet);\n\n return;\n }).fail(() => {\n Notification.exception(new Error('Failed to get quiz data'));\n });\n};\n\n/**\n * Handle processing of refresh and period button actions.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst refreshAction = (event) => {\n event.preventDefault();\n var element = event.target;\n\n if (element.closest('button') !== null && element.closest('button').id === 'local-assessfreq-refresh-quiz-dashboard') {\n refreshCounter(true);\n processDashboard(quizId);\n } else if (element.tagName.toLowerCase() === 'a') {\n refreshPeriod = element.dataset.period;\n refreshCounter(true);\n UserPreference.setUserPreference('local_assessfreq_quiz_refresh_preference', refreshPeriod);\n }\n};\n\n/**\n * Trigger the zoom graph. Thin wrapper to add extra data to click event.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst triggerZoomGraph = (event) => {\n let call = event.target.closest('div').dataset.call;\n let params = {'data': JSON.stringify({'quiz': quizId, 'call': call})};\n let method = 'get_quiz_chart';\n\n ZoomModal.zoomGraph(event, params, method);\n};\n\n/**\n * Initialise method for quiz dashboard rendering.\n *\n * @param {int} context The context id.\n * @param {int} quiz The quiz id.\n */\nexport const init = (context, quiz) => {\n contextid = context;\n FormModal.init(context, processDashboard); // Create modal for quiz selection modal.\n ZoomModal.init(context); // Create the zoom modal.\n OverrideModal.init(context, processDashboard);\n TableHandler.init(\n quizId,\n contextid,\n 'local-assessfreq-quiz-student-table',\n 'local-assessfreq-quiz-table',\n 'get_student_table',\n 'local_assessfreq_quiz_table_rows_preference',\n 'local-assessfreq-quiz-student-table-search',\n 'local_assessfreq_student_table',\n 'local_assessfreq_set_table_preference'\n );\n ChartData.init(cards, context, 'get_quiz_chart', 'local_assessfreq/chart');\n Str.get_string('loadingquiztitle', 'local_assessfreq').then((str) => {\n selectQuizStr = str;\n }).catch(() => {\n Notification.exception(new Error('Failed to load string: loadingquiz'));\n }).then(() => {\n if (quiz > 0) {\n quizId = quiz;\n processDashboard(quiz);\n }\n });\n\n UserPreference.getUserPreference('local_assessfreq_quiz_refresh_preference')\n .then((response) => {\n refreshPeriod = response.preferences[0].value ? response.preferences[0].value : 60;\n })\n .fail(() => {\n Notification.exception(new Error('Failed to get use preference: refresh'));\n });\n\n // Event handling for refresh and period buttons.\n let refreshElement = document.getElementById('local-assessfreq-period-container');\n refreshElement.addEventListener('click', refreshAction);\n\n // Set up zoom event listeners.\n let summaryZoom = document.getElementById('local-assessfreq-quiz-summary-graph-zoom');\n summaryZoom.addEventListener('click', triggerZoomGraph);\n\n let trendZoom = document.getElementById('local-assessfreq-quiz-summary-trend-zoom');\n trendZoom.addEventListener('click', triggerZoomGraph);\n\n};\n"],"names":["contextid","counterid","selectQuizStr","quizId","refreshPeriod","cards","cardId","call","aspect","refreshCounter","reset","progressElement","document","getElementById","clearInterval","setAttribute","setInterval","progressWidthAria","getAttribute","progressStep","processDashboard","quiz","titleElement","innerHTML","methodname","args","quizid","then","response","quizArray","JSON","parse","cardsElement","trendElement","summarySpinner","getElementsByClassName","tableElement","periodElement","tableSearchInputElement","tableSearchResetElement","tableSearchRowsElement","quizLink","createElement","href","url","name","appendChild","currentdUrl","URL","window","location","newUrl","origin","pathname","history","pushState","Str","get_string","courseshortname","str","title","catch","exception","Error","render","done","html","classList","add","contentcontainer","replaceNodeContents","fail","remove","ChartData","getCardCharts","TableHandler","getTable","addEventListener","tableSearch","tableSearchReset","tableSearchRowSet","refreshAction","event","preventDefault","element","target","closest","id","tagName","toLowerCase","dataset","period","UserPreference","setUserPreference","triggerZoomGraph","params","stringify","ZoomModal","zoomGraph","context","FormModal","init","getUserPreference","preferences","value"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/dashboard_quiz_inprogress.min.js b/amd/build/dashboard_quiz_inprogress.min.js
deleted file mode 100644
index 7554495d..00000000
--- a/amd/build/dashboard_quiz_inprogress.min.js
+++ /dev/null
@@ -1,10 +0,0 @@
-define("local_assessfreq/dashboard_quiz_inprogress",["exports","core/ajax","core/notification","core/templates","local_assessfreq/chart_data","local_assessfreq/table_handler","local_assessfreq/user_preferences","local_assessfreq/zoom_modal"],(function(_exports,_ajax,_notification,_templates,ChartData,TableHandler,UserPreference,ZoomModal){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 _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
-/**
- * Javascript for quizzes in progress display and processing.
- *
- * @module local_assessfreq/dashboard_quiz_inprogress
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */var contextid;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates),ChartData=_interopRequireWildcard(ChartData),TableHandler=_interopRequireWildcard(TableHandler),UserPreference=_interopRequireWildcard(UserPreference),ZoomModal=_interopRequireWildcard(ZoomModal);var counterid,hoursFilter,refreshPeriod=60,tablesort="name_asc",hoursAhead=0,hoursBehind=0;const cards=[{cardId:"local-assessfreq-quiz-summary-upcomming-graph",call:"upcomming_quizzes",aspect:!0},{cardId:"local-assessfreq-quiz-summary-inprogress-graph",call:"all_participants_inprogress",aspect:!0}],refreshCounter=function(){let reset=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],progressElement=document.getElementById("local-assessfreq-period-progress");!0===reset&&(clearInterval(counterid),counterid=null,progressElement.setAttribute("style","width: 100%"),progressElement.setAttribute("aria-valuenow",100)),counterid||(counterid=setInterval((()=>{let progressWidthAria=progressElement.getAttribute("aria-valuenow");const progressStep=100/refreshPeriod;progressWidthAria-progressStep>0?(progressElement.setAttribute("style","width: "+(progressWidthAria-progressStep)+"%"),progressElement.setAttribute("aria-valuenow",progressWidthAria-progressStep)):(clearInterval(counterid),counterid=null,progressElement.setAttribute("style","width: 100%"),progressElement.setAttribute("aria-valuenow",100),processDashboard(),refreshCounter())}),1e3))},processDashboard=()=>{_ajax.default.call([{methodname:"local_assessfreq_get_inprogress_counts",args:{}}])[0].then((response=>{let quizSummary=JSON.parse(response),summaryElement=document.getElementById("local-assessfreq-quiz-dashboard-inprogress-summary-card"),summarySpinner=summaryElement.getElementsByClassName("overlay-icon-container")[0],tableSearchInputElement=document.getElementById("local-assessfreq-quiz-inprogress-table-search"),tableSearchResetElement=document.getElementById("local-assessfreq-quiz-inprogress-table-search-reset"),tableSearchRowsElement=document.getElementById("local-assessfreq-quiz-inprogress-table-rows"),tableSortElement=document.getElementById("local-assessfreq-inprogress-table-sort");summaryElement.classList.remove("hide"),_templates.default.render("local_assessfreq/quiz-dashboard-inprogress-summary-card-content",quizSummary).done((html=>{summarySpinner.classList.add("hide");let contentcontainer=document.getElementById("local-assessfreq-quiz-dashboard-inprogress-summary-card-content");_templates.default.replaceNodeContents(contentcontainer,html,"")})).fail((()=>{_notification.default.exception(new Error("Failed to load quiz counts template."))})),hoursFilter=[hoursAhead,hoursBehind],ChartData.getCardCharts(0,hoursFilter),TableHandler.getTable(0,hoursFilter,tablesort),refreshCounter(),tableSearchInputElement.addEventListener("keyup",TableHandler.tableSearch),tableSearchInputElement.addEventListener("paste",TableHandler.tableSearch),tableSearchResetElement.addEventListener("click",TableHandler.tableSearchReset),tableSearchRowsElement.addEventListener("click",TableHandler.tableSearchRowSet),tableSortElement.addEventListener("click",TableHandler.tableSortButtonAction)})).fail((()=>{_notification.default.exception(new Error("Failed to get quiz summary counts"))}))},refreshAction=event=>{event.preventDefault();var element=event.target;null!==element.closest("button")&&"local-assessfreq-refresh-quiz-dashboard"===element.closest("button").id?(refreshCounter(!0),processDashboard()):"a"===element.tagName.toLowerCase()&&(refreshPeriod=element.dataset.period,refreshCounter(!0),UserPreference.setUserPreference("local_assessfreq_quiz_refresh_preference",refreshPeriod))},triggerZoomGraph=event=>{let call=event.target.closest("div").dataset.call,params={data:JSON.stringify({call:call,hoursahead:hoursAhead,hoursbehind:hoursBehind})};ZoomModal.zoomGraph(event,params,"get_quiz_inprogress_chart")},quizzesAheadSet=event=>{if(event.preventDefault(),"a"===event.target.tagName.toLowerCase()){let hours=event.target.dataset.metric;UserPreference.setUserPreference("local_assessfreq_quizzes_inprogress_table_hoursahead_preference",hours).then((()=>{hoursAhead=hours,processDashboard()})).fail((()=>{_notification.default.exception(new Error("Failed to update user preference: hours ahead"))}))}},quizzesBehindSet=event=>{if(event.preventDefault(),"a"===event.target.tagName.toLowerCase()){let hours=event.target.dataset.metric;UserPreference.setUserPreference("local_assessfreq_quizzes_inprogress_table_hoursbehind_preference",hours).then((()=>{hoursBehind=hours,processDashboard()})).fail((()=>{_notification.default.exception(new Error("Failed to update user preference: hours behind"))}))}};_exports.init=context=>{contextid=context,ZoomModal.init(context),TableHandler.init(0,contextid,null,"local-assessfreq-quiz-inprogress-table","get_quizzes_inprogress_table","local_assessfreq_quiz_table_inprogress_preference","local-assessfreq-quiz-inprogress-table-search"),ChartData.init(cards,context,"get_quiz_inprogress_chart","local_assessfreq/chart"),UserPreference.getUserPreference("local_assessfreq_quiz_refresh_preference").then((response=>{refreshPeriod=response.preferences[0].value?response.preferences[0].value:60})).fail((()=>{_notification.default.exception(new Error("Failed to get use preference: refresh"))})),UserPreference.getUserPreference("local_assessfreq_quiz_table_inprogress_sort_preference").then((response=>{tablesort=response.preferences[0].value?response.preferences[0].value:"name_asc"})).fail((()=>{_notification.default.exception(new Error("Failed to get use preference: tablesort"))})),UserPreference.getUserPreference("local_assessfreq_quizzes_inprogress_table_hoursahead_preference").then((response=>{hoursAhead=response.preferences[0].value?response.preferences[0].value:0})).fail((()=>{_notification.default.exception(new Error("Failed to get use preference: hoursahead"))})),UserPreference.getUserPreference("local_assessfreq_quizzes_inprogress_table_hoursbehind_preference").then((response=>{hoursBehind=response.preferences[0].value?response.preferences[0].value:0})).fail((()=>{_notification.default.exception(new Error("Failed to get use preference: hoursbehind"))})),document.getElementById("local-assessfreq-period-container").addEventListener("click",refreshAction),document.getElementById("local-assessfreq-quiz-summary-inprogress-graph-zoom").addEventListener("click",triggerZoomGraph),document.getElementById("local-assessfreq-quiz-summary-upcomming-graph-zoom").addEventListener("click",triggerZoomGraph),document.getElementById("local-assessfreq-quiz-student-table-hoursahead").addEventListener("click",quizzesAheadSet),document.getElementById("local-assessfreq-quiz-student-table-hoursbehind").addEventListener("click",quizzesBehindSet),processDashboard()}}));
-
-//# sourceMappingURL=dashboard_quiz_inprogress.min.js.map
\ No newline at end of file
diff --git a/amd/build/dashboard_quiz_inprogress.min.js.map b/amd/build/dashboard_quiz_inprogress.min.js.map
deleted file mode 100644
index c0cf6aa7..00000000
--- a/amd/build/dashboard_quiz_inprogress.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"dashboard_quiz_inprogress.min.js","sources":["../src/dashboard_quiz_inprogress.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 * Javascript for quizzes in progress display and processing.\n *\n * @module local_assessfreq/dashboard_quiz_inprogress\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\nimport Templates from 'core/templates';\nimport * as ChartData from 'local_assessfreq/chart_data';\nimport * as TableHandler from 'local_assessfreq/table_handler';\nimport * as UserPreference from 'local_assessfreq/user_preferences';\nimport * as ZoomModal from 'local_assessfreq/zoom_modal';\n\n/**\n * Module level variables.\n */\nvar contextid;\nvar refreshPeriod = 60;\nvar counterid;\nvar tablesort = 'name_asc';\nvar hoursAhead = 0;\nvar hoursBehind = 0;\n\n/**\n * Hours filter array.\n *\n * @type {array} Title to display on modal.\n */\nvar hoursFilter;\n\nconst cards = [\n {cardId: 'local-assessfreq-quiz-summary-upcomming-graph', call: 'upcomming_quizzes', aspect: true},\n {cardId: 'local-assessfreq-quiz-summary-inprogress-graph', call: 'all_participants_inprogress', aspect: true}\n];\n\n/**\n * Function for refreshing the counter.\n *\n * @param {boolean} reset the current count process.\n */\nconst refreshCounter = (reset = true) => {\n let progressElement = document.getElementById('local-assessfreq-period-progress');\n\n // Reset the current count process.\n if (reset === true) {\n clearInterval(counterid);\n counterid = null;\n progressElement.setAttribute('style', 'width: 100%');\n progressElement.setAttribute('aria-valuenow', 100);\n }\n\n // Exit early if there is already a counter running.\n if (counterid) {\n return;\n }\n\n counterid = setInterval(() => {\n let progressWidthAria = progressElement.getAttribute('aria-valuenow');\n const progressStep = 100 / refreshPeriod;\n\n if ((progressWidthAria - progressStep) > 0) {\n progressElement.setAttribute('style', 'width: ' + (progressWidthAria - progressStep) + '%');\n progressElement.setAttribute('aria-valuenow', (progressWidthAria - progressStep));\n } else {\n clearInterval(counterid);\n counterid = null;\n progressElement.setAttribute('style', 'width: 100%');\n progressElement.setAttribute('aria-valuenow', 100);\n processDashboard();\n refreshCounter();\n }\n }, (1000));\n};\n\n/**\n * Starts the processing of the dashboard.\n */\nconst processDashboard = () => {\n // Get summary quiz data.\n Ajax.call([{\n methodname: 'local_assessfreq_get_inprogress_counts',\n args: {},\n }])[0].then((response) => {\n let quizSummary = JSON.parse(response);\n let summaryElement = document.getElementById('local-assessfreq-quiz-dashboard-inprogress-summary-card');\n let summarySpinner = summaryElement.getElementsByClassName('overlay-icon-container')[0];\n let tableSearchInputElement = document.getElementById('local-assessfreq-quiz-inprogress-table-search');\n let tableSearchResetElement = document.getElementById('local-assessfreq-quiz-inprogress-table-search-reset');\n let tableSearchRowsElement = document.getElementById('local-assessfreq-quiz-inprogress-table-rows');\n let tableSortElement = document.getElementById('local-assessfreq-inprogress-table-sort');\n\n summaryElement.classList.remove('hide'); // Show the card.\n\n // Populate summary card with details.\n Templates.render('local_assessfreq/quiz-dashboard-inprogress-summary-card-content', quizSummary)\n .done((html) => {\n summarySpinner.classList.add('hide');\n\n let contentcontainer = document.getElementById('local-assessfreq-quiz-dashboard-inprogress-summary-card-content');\n Templates.replaceNodeContents(contentcontainer, html, '');\n }).fail(() => {\n Notification.exception(new Error('Failed to load quiz counts template.'));\n return;\n });\n\n hoursFilter = [hoursAhead, hoursBehind];\n ChartData.getCardCharts(0, hoursFilter);\n TableHandler.getTable(0, hoursFilter, tablesort);\n refreshCounter();\n\n // Table event listeners.\n tableSearchInputElement.addEventListener('keyup', TableHandler.tableSearch);\n tableSearchInputElement.addEventListener('paste', TableHandler.tableSearch);\n tableSearchResetElement.addEventListener('click', TableHandler.tableSearchReset);\n tableSearchRowsElement.addEventListener('click', TableHandler.tableSearchRowSet);\n tableSortElement.addEventListener('click', TableHandler.tableSortButtonAction);\n\n return;\n }).fail(() => {\n Notification.exception(new Error('Failed to get quiz summary counts'));\n });\n};\n\n/**\n * Handle processing of refresh and period button actions.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst refreshAction = (event) => {\n event.preventDefault();\n var element = event.target;\n\n if (element.closest('button') !== null && element.closest('button').id === 'local-assessfreq-refresh-quiz-dashboard') {\n refreshCounter(true);\n processDashboard();\n } else if (element.tagName.toLowerCase() === 'a') {\n refreshPeriod = element.dataset.period;\n refreshCounter(true);\n UserPreference.setUserPreference('local_assessfreq_quiz_refresh_preference', refreshPeriod);\n }\n};\n\n/**\n * Trigger the zoom graph. Thin wrapper to add extra data to click event.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst triggerZoomGraph = (event) => {\n let call = event.target.closest('div').dataset.call;\n let params = {'data': JSON.stringify({'call': call, 'hoursahead': hoursAhead, 'hoursbehind': hoursBehind})};\n let method = 'get_quiz_inprogress_chart';\n\n ZoomModal.zoomGraph(event, params, method);\n};\n\n/**\n * Process the hours ahead event from the in progress quizzes table.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst quizzesAheadSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let hours = event.target.dataset.metric;\n UserPreference.setUserPreference('local_assessfreq_quizzes_inprogress_table_hoursahead_preference', hours)\n .then(() => {\n hoursAhead = hours;\n processDashboard(); // Reload the table.\n })\n .fail(() => {\n Notification.exception(new Error('Failed to update user preference: hours ahead'));\n });\n }\n};\n\n/**\n * Process the hours behind event from the in progress quizzes table.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst quizzesBehindSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let hours = event.target.dataset.metric;\n UserPreference.setUserPreference('local_assessfreq_quizzes_inprogress_table_hoursbehind_preference', hours)\n .then(() => {\n hoursBehind = hours;\n processDashboard(); // Reload the table.\n })\n .fail(() => {\n Notification.exception(new Error('Failed to update user preference: hours behind'));\n });\n }\n};\n\n/**\n * Initialise method for quizzes in progress dashboard rendering.\n *\n * @param {int} context The context id.\n */\nexport const init = (context) => {\n contextid = context;\n ZoomModal.init(context); // Create the zoom modal.\n TableHandler.init(\n 0,\n contextid,\n null,\n 'local-assessfreq-quiz-inprogress-table',\n 'get_quizzes_inprogress_table',\n 'local_assessfreq_quiz_table_inprogress_preference',\n 'local-assessfreq-quiz-inprogress-table-search'\n );\n ChartData.init(cards, context, 'get_quiz_inprogress_chart', 'local_assessfreq/chart');\n\n UserPreference.getUserPreference('local_assessfreq_quiz_refresh_preference')\n .then((response) => {\n refreshPeriod = response.preferences[0].value ? response.preferences[0].value : 60;\n })\n .fail(() => {\n Notification.exception(new Error('Failed to get use preference: refresh'));\n });\n\n UserPreference.getUserPreference('local_assessfreq_quiz_table_inprogress_sort_preference')\n .then((response) => {\n tablesort = response.preferences[0].value ? response.preferences[0].value : 'name_asc';\n })\n .fail(() => {\n Notification.exception(new Error('Failed to get use preference: tablesort'));\n });\n\n UserPreference.getUserPreference('local_assessfreq_quizzes_inprogress_table_hoursahead_preference')\n .then((response) => {\n hoursAhead = response.preferences[0].value ? response.preferences[0].value : 0;\n })\n .fail(() => {\n Notification.exception(new Error('Failed to get use preference: hoursahead'));\n });\n\n UserPreference.getUserPreference('local_assessfreq_quizzes_inprogress_table_hoursbehind_preference')\n .then((response) => {\n hoursBehind = response.preferences[0].value ? response.preferences[0].value : 0;\n })\n .fail(() => {\n Notification.exception(new Error('Failed to get use preference: hoursbehind'));\n });\n\n // Event handling for refresh and period buttons.\n let refreshElement = document.getElementById('local-assessfreq-period-container');\n refreshElement.addEventListener('click', refreshAction);\n\n // Set up zoom event listeners.\n let summaryZoom = document.getElementById('local-assessfreq-quiz-summary-inprogress-graph-zoom');\n summaryZoom.addEventListener('click', triggerZoomGraph);\n\n let upcommingZoom = document.getElementById('local-assessfreq-quiz-summary-upcomming-graph-zoom');\n upcommingZoom.addEventListener('click', triggerZoomGraph);\n\n // Set up behind and ahead quizzes event listeners.\n let quizzesAheadElement = document.getElementById('local-assessfreq-quiz-student-table-hoursahead');\n quizzesAheadElement.addEventListener('click', quizzesAheadSet);\n\n let quizzesBehindElement = document.getElementById('local-assessfreq-quiz-student-table-hoursbehind');\n quizzesBehindElement.addEventListener('click', quizzesBehindSet);\n\n processDashboard();\n\n};\n"],"names":["contextid","counterid","hoursFilter","refreshPeriod","tablesort","hoursAhead","hoursBehind","cards","cardId","call","aspect","refreshCounter","reset","progressElement","document","getElementById","clearInterval","setAttribute","setInterval","progressWidthAria","getAttribute","progressStep","processDashboard","methodname","args","then","response","quizSummary","JSON","parse","summaryElement","summarySpinner","getElementsByClassName","tableSearchInputElement","tableSearchResetElement","tableSearchRowsElement","tableSortElement","classList","remove","render","done","html","add","contentcontainer","replaceNodeContents","fail","exception","Error","ChartData","getCardCharts","TableHandler","getTable","addEventListener","tableSearch","tableSearchReset","tableSearchRowSet","tableSortButtonAction","refreshAction","event","preventDefault","element","target","closest","id","tagName","toLowerCase","dataset","period","UserPreference","setUserPreference","triggerZoomGraph","params","stringify","ZoomModal","zoomGraph","quizzesAheadSet","hours","metric","quizzesBehindSet","context","init","getUserPreference","preferences","value"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/dayview.min.js b/amd/build/dayview.min.js
deleted file mode 100644
index 8c7b3f8f..00000000
--- a/amd/build/dayview.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Javascript for heatmap calendar generation and display.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define("local_assessfreq/dayview",["core/str","core/notification","core/modal","local_assessfreq/modal_large","core/templates","core/ajax"],(function(Str,Notification,Modal,ModalLarge,Templates,Ajax){var modalObj,Dayview={};const spinner='
',stringArr=[{key:"sun",component:"calendar"},{key:"mon",component:"calendar"},{key:"tue",component:"calendar"},{key:"wed",component:"calendar"},{key:"thu",component:"calendar"},{key:"fri",component:"calendar"},{key:"sat",component:"calendar"},{key:"jan",component:"local_assessfreq"},{key:"feb",component:"local_assessfreq"},{key:"mar",component:"local_assessfreq"},{key:"apr",component:"local_assessfreq"},{key:"may",component:"local_assessfreq"},{key:"jun",component:"local_assessfreq"},{key:"jul",component:"local_assessfreq"},{key:"aug",component:"local_assessfreq"},{key:"sep",component:"local_assessfreq"},{key:"oct",component:"local_assessfreq"},{key:"nov",component:"local_assessfreq"},{key:"dec",component:"local_assessfreq"}];var stringResult,systemTimezone="Australia/Melbourne",dayViewTitle="";const getUserDate=function(timestamp,format){return new Promise((resolve=>{const systemTimezoneTime=new Date(1e3*timestamp).toLocaleString("en-US",{timeZone:systemTimezone});let date=new Date(systemTimezoneTime);const year=date.getFullYear(),month=stringResult[7+date.getMonth()],day=date.getDate(),strftimetime=date.getHours()+":"+("0"+date.getMinutes()).substr(-2);resolve("strftimetime"===format?strftimetime:day+" "+month+" "+year+", "+strftimetime)}))},formatData=async function(response){let responseArr=JSON.parse(response),scaler=5/72;for(let i=0;i100&&(width=100-leftMargin),responseArr[i].leftmargin=leftMargin,responseArr[i].width=width,responseArr[i].end=await getUserDate(responseArr[i].timeend,"strftimetime")}return new Promise((resolve=>{resolve(responseArr)}))};return Dayview.display=function(date){modalObj.setBody(spinner),modalObj.show();let args={date:date,modules:["all"]},jsonArgs=JSON.stringify(args);Ajax.call([{methodname:"local_assessfreq_get_day_events",args:{jsondata:jsonArgs}}])[0].then(formatData).then((responseArr=>{let context={rows:responseArr};const year=responseArr[0].endyear,dayDate=responseArr[0].endday+" "+stringResult[6+parseInt(responseArr[0].endmonth)]+" "+year;modalObj.setTitle(dayViewTitle+" "+dayDate),modalObj.setBody(Templates.render("local_assessfreq/dayview",context))})).fail((()=>{Notification.exception(new Error("Failed to load day view"))}))},Dayview.init=function(){Str.get_strings(stringArr).catch((()=>{Notification.exception(new Error("Failed to load strings"))})).then((stringReturn=>{stringResult=stringReturn})),Ajax.call([{methodname:"local_assessfreq_get_system_timezone",args:{}}],!0,!1)[0].then((response=>{systemTimezone=response})).fail((()=>{Notification.exception(new Error("Failed to get system timezone"))})),Str.get_string("schedule","local_assessfreq").then((title=>{dayViewTitle=title,Modal.create({type:ModalLarge.TYPE,title:title,body:spinner,large:!0}).then((modal=>{modalObj=modal}))})).catch(Notification.exception)},Dayview}));
-
-//# sourceMappingURL=dayview.min.js.map
\ No newline at end of file
diff --git a/amd/build/dayview.min.js.map b/amd/build/dayview.min.js.map
deleted file mode 100644
index 19659d1a..00000000
--- a/amd/build/dayview.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"dayview.min.js","sources":["../src/dayview.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 * Javascript for heatmap calendar generation and display.\n *\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n ['core/str', 'core/notification', 'core/modal', 'local_assessfreq/modal_large', 'core/templates', 'core/ajax'],\n function (Str, Notification, Modal, ModalLarge, Templates, Ajax) {\n\n /**\n * Module level variables.\n */\n var Dayview = {};\n var modalObj;\n const spinner = '
'\n + ''\n + '
';\n\n const stringArr = [\n {key: 'sun', component: 'calendar'},\n {key: 'mon', component: 'calendar'},\n {key: 'tue', component: 'calendar'},\n {key: 'wed', component: 'calendar'},\n {key: 'thu', component: 'calendar'},\n {key: 'fri', component: 'calendar'},\n {key: 'sat', component: 'calendar'},\n {key: 'jan', component: 'local_assessfreq'},\n {key: 'feb', component: 'local_assessfreq'},\n {key: 'mar', component: 'local_assessfreq'},\n {key: 'apr', component: 'local_assessfreq'},\n {key: 'may', component: 'local_assessfreq'},\n {key: 'jun', component: 'local_assessfreq'},\n {key: 'jul', component: 'local_assessfreq'},\n {key: 'aug', component: 'local_assessfreq'},\n {key: 'sep', component: 'local_assessfreq'},\n {key: 'oct', component: 'local_assessfreq'},\n {key: 'nov', component: 'local_assessfreq'},\n {key: 'dec', component: 'local_assessfreq'},\n ];\n var stringResult;\n var systemTimezone = 'Australia/Melbourne';\n var dayViewTitle = '';\n\n const getUserDate = function (timestamp, format) {\n return new Promise((resolve) => {\n const systemTimezoneTime = new Date(timestamp * 1000).toLocaleString('en-US', {timeZone: systemTimezone});\n let date = new Date(systemTimezoneTime);\n const year = date.getFullYear();\n const month = stringResult[(7 + date.getMonth())];\n const day = date.getDate();\n const hours = date.getHours();\n const minutes = '0' + date.getMinutes();\n\n const strftimetime = hours + ':' + minutes.substr(-2); // Will display time in 10:30 format.\n const strftimedatetime = day + ' ' + month + ' ' + year + ', ' + strftimetime;\n\n if (format === 'strftimetime') {\n resolve(strftimetime);\n } else {\n resolve(strftimedatetime);\n }\n\n });\n };\n\n const formatData = async function (response) {\n let responseArr = JSON.parse(response);\n\n // We are displaying the event as a bar whose width represents the start and end time of the event.\n // We need to scale the width of the bar to match the width of the container. Therefore 100% width of the container\n // equals 24 hours (one day).\n // There are 1440 mins per day. 1440 mins equals 100%, therefore 1 min = (100/1440)%. 5/72 == 100/1440.\n let scaler = 5 / 72;\n\n for (let i = 0; i < responseArr.length; i++) {\n const year = responseArr[i].endyear;\n const month = (responseArr[i].endmonth) - 1; // Minus 1 for difference between months in PHP and JS.\n const day = responseArr[i].endday;\n const dayStart = (new Date(year, month, day).getTime()) / 1000;\n const timeStart = new Date(responseArr[i].timestart * 1000).toLocaleString('en-US', {timeZone: systemTimezone});\n const timeStartTimestamp = (new Date(timeStart).getTime()) / 1000;\n const timeEnd = new Date(responseArr[i].timeend * 1000).toLocaleString('en-US', {timeZone: systemTimezone});\n const timeEndTimestamp = (new Date(timeEnd).getTime()) / 1000;\n let secondsSinceDayStart = timeStartTimestamp - dayStart;\n let leftMargin = 0;\n let width = 0;\n\n if (secondsSinceDayStart <= 0) {\n secondsSinceDayStart = 0;\n width = ((timeEndTimestamp - dayStart) / 60) * scaler;\n responseArr[i].start = await getUserDate(responseArr[i].timestart, 'strftimedatetime');\n } else {\n leftMargin = (secondsSinceDayStart / 60) * scaler;\n width = ((timeEndTimestamp - timeStartTimestamp) / 60) * scaler;\n responseArr[i].start = await getUserDate(responseArr[i].timestart, 'strftimetime');\n }\n\n if (leftMargin + width > 100) {\n width = 100 - leftMargin;\n }\n\n responseArr[i].leftmargin = leftMargin;\n responseArr[i].width = width;\n responseArr[i].end = await getUserDate(responseArr[i].timeend, 'strftimetime');\n }\n\n return new Promise((resolve) => {\n resolve(responseArr);\n });\n };\n\n /**\n * Initialise the base modal to be used.\n *\n * @param {int} date The date to display the day view for.\n *\n */\n Dayview.display = function (date) {\n modalObj.setBody(spinner);\n modalObj.show();\n let args = {\n date: date,\n modules: ['all']\n };\n let jsonArgs = JSON.stringify(args);\n Ajax.call([{\n methodname: 'local_assessfreq_get_day_events',\n args: {jsondata: jsonArgs},\n }])[0]\n .then(formatData)\n .then((responseArr) => {\n\n let context = {rows: responseArr};\n const year = responseArr[0].endyear;\n const day = responseArr[0].endday;\n const month = stringResult[(6 + parseInt(responseArr[0].endmonth))];\n const dayDate = day + ' ' + month + ' ' + year;\n\n modalObj.setTitle(dayViewTitle + ' ' + dayDate);\n modalObj.setBody(Templates.render('local_assessfreq/dayview', context));\n\n }).fail(() => {\n Notification.exception(new Error('Failed to load day view'));\n });\n };\n\n /**\n * Initialise the base modal to be used.\n *\n */\n Dayview.init = function () {\n // Load the strings we'll need later.\n Str.get_strings(stringArr).catch(() => { // Get required strings.\n Notification.exception(new Error('Failed to load strings'));\n return;\n }).then(stringReturn => { // Save string to global to be used later.\n stringResult = stringReturn;\n });\n\n // Get the system timzone.\n Ajax.call([{\n methodname: 'local_assessfreq_get_system_timezone',\n args: {},\n }], true, false)[0].then((response) => {\n systemTimezone = response;\n return;\n }).fail(() => {\n Notification.exception(new Error('Failed to get system timezone'));\n });\n\n Str.get_string('schedule', 'local_assessfreq').then((title) => {\n dayViewTitle = title;\n\n // Create the Modal.\n Modal.create({\n type: ModalLarge.TYPE,\n title: title,\n body: spinner,\n large: true\n })\n .then((modal) => {\n modalObj = modal;\n\n });\n }).catch(Notification.exception);\n\n };\n\n return Dayview;\n }\n);\n"],"names":["define","Str","Notification","Modal","ModalLarge","Templates","Ajax","modalObj","Dayview","spinner","stringArr","key","component","stringResult","systemTimezone","dayViewTitle","getUserDate","timestamp","format","Promise","resolve","systemTimezoneTime","Date","toLocaleString","timeZone","date","year","getFullYear","month","getMonth","day","getDate","strftimetime","getHours","getMinutes","substr","formatData","async","response","responseArr","JSON","parse","scaler","i","length","endyear","endmonth","endday","dayStart","getTime","timeStart","timestart","timeStartTimestamp","timeEnd","timeend","timeEndTimestamp","secondsSinceDayStart","leftMargin","width","start","leftmargin","end","display","setBody","show","args","modules","jsonArgs","stringify","call","methodname","jsondata","then","context","rows","dayDate","parseInt","setTitle","render","fail","exception","Error","init","get_strings","catch","stringReturn","get_string","title","create","type","TYPE","body","large","modal"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/debouncer.min.js.map b/amd/build/debouncer.min.js.map
index d989c5ea..ceb85d0a 100644
--- a/amd/build/debouncer.min.js.map
+++ b/amd/build/debouncer.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"debouncer.min.js","sources":["../src/debouncer.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 * Debounce JS module.\n *\n * @module local_assessfreq/debouncer\n * @copyright 2020 Guillermo Gomez \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n */\n\n/**\n * Quick and dirty debounce method for the settings.\n * This stops the ajax method that updates the table from being updated\n * while the user is still checking options.\n *\n * @method debouncer\n * @param {function} func The function we want to keep calling.\n * @param {number} wait Our timeout.\n * @return {function}\n */\nexport const debouncer = (func, wait) => {\n let timeout;\n\n return function executedFunction(...args) {\n const later = () => {\n clearTimeout(timeout);\n func(...args);\n };\n\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n };\n};\n"],"names":["func","wait","timeout","args","later","clearTimeout","setTimeout"],"mappings":"yKAkCyB,CAACA,KAAMC,YACxBC,eAEG,yCAA6BC,6CAAAA,iCAC1BC,MAAQ,KACVC,aAAaH,SACbF,QAAQG,OAGZE,aAAaH,SACbA,QAAUI,WAAWF,MAAOH"}
\ No newline at end of file
+{"version":3,"file":"debouncer.min.js","sources":["../src/debouncer.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 * Debounce JS module.\n *\n * @module local_assessfreq/debouncer\n * @package\n * @copyright 2020 Guillermo Gomez \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n */\n\n/**\n * Quick and dirty debounce method for the settings.\n * This stops the ajax method that updates the table from being updated\n * while the user is still checking options.\n *\n * @method debouncer\n * @param {function} func The function we want to keep calling.\n * @param {number} wait Our timeout.\n * @return {function}\n */\nexport const debouncer = (func, wait) => {\n let timeout;\n\n return function executedFunction(...args) {\n const later = () => {\n clearTimeout(timeout);\n func(...args);\n };\n\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n };\n};\n"],"names":["func","wait","timeout","args","later","clearTimeout","setTimeout"],"mappings":"yKAmCyB,CAACA,KAAMC,YACxBC,eAEG,yCAA6BC,6CAAAA,iCAC1BC,MAAQ,KACVC,aAAaH,SACbF,QAAQG,OAGZE,aAAaH,SACbA,QAAUI,WAAWF,MAAOH"}
\ No newline at end of file
diff --git a/amd/build/form_modal.min.js b/amd/build/form_modal.min.js
deleted file mode 100644
index 6a7baee2..00000000
--- a/amd/build/form_modal.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Javascript for report card display and processing.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define("local_assessfreq/form_modal",["core/str","core/modal","core/fragment","core/ajax","core/notification"],(function(Str,Modal,Fragment,Ajax,Notification){var contextid,modalObj,callback,FormModal={},resetOptions=[];const spinner='
',observerConfig={attributes:!0,childList:!1,subtree:!0},observer=new MutationObserver((function(mutationsList){for(let i=0;i{let quizArray=JSON.parse(response),selectElement=document.getElementById("id_quiz"),selectElementLength=selectElement.options.length;null!==document.getElementById("noquizwarning")&&document.getElementById("noquizwarning").remove();for(let j=selectElementLength-1;j>=0;j--)selectElement.options[j]=null;if(quizArray.length>0){for(let k=0;k{selectElement.appendChild(option)})),document.getElementById("id_quiz").value=0,selectElement.disabled=!0})).fail((()=>{Notification.exception(new Error("Failed to get quizzes"))}));break}}})),updateModalBody=function(formdata){void 0===formdata&&(formdata={});let params={jsonformdata:JSON.stringify(formdata)};new Promise(((resolve,reject)=>{Str.get_strings([{key:"selectcourse",component:"local_assessfreq"},{key:"loadingquiz",component:"local_assessfreq"}]).catch((()=>{reject(new Error("Failed to load strings"))})).then((stringReturn=>{for(let i=0;i{Str.get_string("searchquiz","local_assessfreq").then((title=>{modalObj.setTitle(title),modalObj.setBody(Fragment.loadFragment("local_assessfreq","new_base_form",contextid,params));let modalContainer=document.querySelectorAll('[data-region*="modal-container"]')[0];observer.observe(modalContainer,observerConfig)})).catch((()=>{Notification.exception(new Error("Failed to load string: searchquiz"))}))}))},processModalForm=function(e){e.preventDefault();let quizElement=document.getElementById("id_quiz"),quizId=quizElement.options[quizElement.selectedIndex].value,courseId=document.getElementById("id_courses").dataset.course;void 0===courseId||quizId<1?null===document.getElementById("noquizwarning")&&Str.get_string("noquizselected","local_assessfreq").then((warning=>{let element=document.createElement("div");element.innerHTML=warning,element.id="noquizwarning",element.classList.add("alert","alert-danger"),modalObj.getBody().prepend(element)})).catch((()=>{Notification.exception(new Error("Failed to load string: searchquiz"))})):(modalObj.hide(),modalObj.setBody(""),observer.disconnect(),callback(quizId,courseId))},displayModalForm=function(){updateModalBody(),modalObj.show()};return FormModal.init=function(context,processDashboard){contextid=context,callback=processDashboard,Str.get_string("loading","local_assessfreq").then((title=>{Modal.create({type:Modal.types.DEFAULT,title:title,body:spinner,large:!0}).then((modal=>{(modalObj=modal).getRoot().on("click","#id_submitbutton",processModalForm),modalObj.getRoot().on("click","#id_cancel",(e=>{e.preventDefault(),modalObj.setBody(spinner),modalObj.hide()}))}))})).catch(Notification.exception),document.getElementById("local-assessfreq-find-quiz").addEventListener("click",displayModalForm)},FormModal}));
-
-//# sourceMappingURL=form_modal.min.js.map
\ No newline at end of file
diff --git a/amd/build/form_modal.min.js.map b/amd/build/form_modal.min.js.map
deleted file mode 100644
index 8501197b..00000000
--- a/amd/build/form_modal.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"form_modal.min.js","sources":["../src/form_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 .\n\n/**\n * Javascript for report card display and processing.\n *\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n ['core/str', 'core/modal', 'core/fragment', 'core/ajax', 'core/notification'],\n function (Str, Modal, Fragment, Ajax, Notification) {\n\n /**\n * Module level variables.\n */\n var FormModal = {};\n var contextid;\n var modalObj;\n var resetOptions = [];\n var callback;\n\n const spinner = '
'\n + ''\n + '
';\n\n const observerConfig = { attributes: true, childList: false, subtree: true };\n\n const ObserverCallback = function (mutationsList) {\n for (let i = 0; i < mutationsList.length; i++) {\n let element = mutationsList[i].target;\n if (element.tagName.toLowerCase() === 'span' && element.classList.contains('badge')) {\n element.addEventListener('click', updateModalBody);\n document.getElementById('id_courses').dataset.course = element.dataset.value;\n\n document.getElementById('id_quiz').value = -1;\n Ajax.call([{\n methodname: 'local_assessfreq_get_quizzes',\n args: {\n query: mutationsList[i].target.dataset.value\n },\n }])[0].done((response) => {\n let quizArray = JSON.parse(response);\n let selectElement = document.getElementById('id_quiz');\n let selectElementLength = selectElement.options.length;\n if (document.getElementById('noquizwarning') !== null) {\n document.getElementById('noquizwarning').remove();\n }\n // Clear exisitng options.\n for (let j = selectElementLength - 1; j >= 0; j--) {\n selectElement.options[j] = null;\n }\n\n if (quizArray.length > 0) {\n // Add new options.\n for (let k = 0; k < quizArray.length; k++) {\n let opt = quizArray[k];\n let el = document.createElement('option');\n el.textContent = opt.name;\n el.value = opt.id;\n selectElement.appendChild(el);\n }\n selectElement.removeAttribute('disabled');\n if (document.getElementById('noquizwarning') !== null) {\n document.getElementById('noquizwarning').remove();\n }\n } else {\n resetOptions.forEach((option) => {\n selectElement.appendChild(option);\n });\n document.getElementById('id_quiz').value = 0;\n selectElement.disabled = true;\n }\n\n }).fail(() => {\n Notification.exception(new Error('Failed to get quizzes'));\n });\n\n break;\n }\n }\n };\n\n const observer = new MutationObserver(ObserverCallback);\n\n /**\n * Create the modal window.\n *\n * @private\n */\n const createModal = function () {\n Str.get_string('loading', 'local_assessfreq').then((title) => {\n // Create the Modal.\n Modal.create({\n type: Modal.types.DEFAULT,\n title: title,\n body: spinner,\n large: true\n })\n .then((modal) => {\n modalObj = modal;\n\n // Explicitly handle form click events.\n modalObj.getRoot().on('click', '#id_submitbutton', processModalForm);\n modalObj.getRoot().on('click', '#id_cancel', (e) => {\n e.preventDefault();\n modalObj.setBody(spinner);\n modalObj.hide();\n });\n });\n return;\n }).catch(Notification.exception);\n };\n\n const getOptionPlaceholders = function () {\n return new Promise((resolve, reject) => {\n const stringArr = [\n {key: 'selectcourse', component: 'local_assessfreq'},\n {key: 'loadingquiz', component: 'local_assessfreq'},\n ];\n\n Str.get_strings(stringArr).catch(() => { // Get required strings.\n reject(new Error('Failed to load strings'));\n return;\n }).then(stringReturn => { // Save string to global to be used later.\n for (let i = 0; i < stringReturn.length; i++) {\n let el = document.createElement('option');\n el.textContent = stringReturn[i];\n el.value = 0 - i;\n resetOptions.push(el);\n }\n resolve();\n });\n });\n };\n\n /**\n * Updates the body of the modal window.\n *\n * @param {Object} formdata\n * @private\n */\n const updateModalBody = function (formdata) {\n if (typeof formdata === \"undefined\") {\n formdata = {};\n }\n\n let params = {\n 'jsonformdata': JSON.stringify(formdata)\n };\n\n getOptionPlaceholders()\n .then(() => {\n Str.get_string('searchquiz', 'local_assessfreq').then((title) => {\n modalObj.setTitle(title);\n modalObj.setBody(Fragment.loadFragment('local_assessfreq', 'new_base_form', contextid, params));\n let modalContainer = document.querySelectorAll('[data-region*=\"modal-container\"]')[0];\n observer.observe(modalContainer, observerConfig);\n\n return;\n }).catch(() => {\n Notification.exception(new Error('Failed to load string: searchquiz'));\n });\n });\n };\n\n /**\n * Updates Moodle form with selected information.\n *\n * @param {Object} e\n * @private\n */\n const processModalForm = function (e) {\n e.preventDefault(); // Stop modal from closing.\n\n let quizElement = document.getElementById('id_quiz');\n let quizId = quizElement.options[quizElement.selectedIndex].value;\n let courseId = document.getElementById('id_courses').dataset.course;\n\n if (courseId === undefined || quizId < 1) {\n if (document.getElementById('noquizwarning') === null) {\n Str.get_string('noquizselected', 'local_assessfreq').then((warning) => {\n let element = document.createElement('div');\n element.innerHTML = warning;\n element.id = 'noquizwarning';\n element.classList.add('alert', 'alert-danger');\n modalObj.getBody().prepend(element);\n\n return;\n }).catch(() => {\n Notification.exception(new Error('Failed to load string: searchquiz'));\n });\n }\n } else {\n modalObj.hide(); // Close modal.\n modalObj.setBody(''); // Cleaer form.\n observer.disconnect(); // Remove observer.\n callback(quizId, courseId); // Trigger dashboard update.\n }\n\n };\n\n /**\n * Display the Modal form.\n */\n const displayModalForm = function () {\n updateModalBody();\n modalObj.show();\n };\n\n /**\n * Initialise method for quiz dashboard rendering.\n *\n * @param {int} context The context id for the dashboard.\n * @param {function} processDashboard The callback function to process the dashboard.\n *\n */\n FormModal.init = function (context, processDashboard) {\n contextid = context;\n callback = processDashboard;\n createModal();\n\n let createBroadcastButton = document.getElementById('local-assessfreq-find-quiz');\n createBroadcastButton.addEventListener('click', displayModalForm);\n };\n\n return FormModal;\n }\n);\n"],"names":["define","Str","Modal","Fragment","Ajax","Notification","contextid","modalObj","callback","FormModal","resetOptions","spinner","observerConfig","attributes","childList","subtree","observer","MutationObserver","mutationsList","i","length","element","target","tagName","toLowerCase","classList","contains","addEventListener","updateModalBody","document","getElementById","dataset","course","value","call","methodname","args","query","done","response","quizArray","JSON","parse","selectElement","selectElementLength","options","remove","j","k","opt","el","createElement","textContent","name","id","appendChild","removeAttribute","forEach","option","disabled","fail","exception","Error","formdata","params","stringify","Promise","resolve","reject","get_strings","key","component","catch","then","stringReturn","push","get_string","title","setTitle","setBody","loadFragment","modalContainer","querySelectorAll","observe","processModalForm","e","preventDefault","quizElement","quizId","selectedIndex","courseId","undefined","warning","innerHTML","add","getBody","prepend","hide","disconnect","displayModalForm","show","init","context","processDashboard","create","type","types","DEFAULT","body","large","modal","getRoot","on"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/modal_large.min.js b/amd/build/modal_large.min.js
index 0a4d9a2c..19b59b2b 100644
--- a/amd/build/modal_large.min.js
+++ b/amd/build/modal_large.min.js
@@ -1,9 +1,11 @@
/**
* Javascript for large modal .
*
+ * @module local_assessfreq/modal_large
+ * @package
* @copyright 2020 Matt Porritt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("local_assessfreq/modal_large",["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry"],(function($,Notification,CustomEvents,Modal,ModalRegistry){var registered=!1,ModalLarge=function(root){Modal.call(this,root)};return ModalLarge.TYPE="local_assesfreq-large_modal",(ModalLarge.prototype=Object.create(Modal.prototype)).constructor=ModalLarge,ModalLarge.prototype.registerEventListeners=function(){Modal.prototype.registerEventListeners.call(this)},registered||(ModalRegistry.register(ModalLarge.TYPE,ModalLarge,"local_assessfreq/modal_large"),registered=!0),ModalLarge}));
+define("local_assessfreq/modal_large",["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry"],(function($,Notification,CustomEvents,Modal,ModalRegistry){let registered=!1,ModalLarge=function(root){Modal.call(this,root)};return ModalLarge.TYPE="local_assesfreq-large_modal",(ModalLarge.prototype=Object.create(Modal.prototype)).constructor=ModalLarge,ModalLarge.prototype.registerEventListeners=function(){Modal.prototype.registerEventListeners.call(this)},registered||(ModalRegistry.register(ModalLarge.TYPE,ModalLarge,"local_assessfreq/modal_large"),registered=!0),ModalLarge}));
//# sourceMappingURL=modal_large.min.js.map
\ No newline at end of file
diff --git a/amd/build/modal_large.min.js.map b/amd/build/modal_large.min.js.map
index a0e2851c..dfdc2028 100644
--- a/amd/build/modal_large.min.js.map
+++ b/amd/build/modal_large.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"modal_large.min.js","sources":["../src/modal_large.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 * Javascript for large modal .\n *\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n ['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_registry'],\n function ($, Notification, CustomEvents, Modal, ModalRegistry) {\n\n var registered = false;\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n var ModalLarge = function (root) {\n Modal.call(this, root);\n };\n\n ModalLarge.TYPE = 'local_assesfreq-large_modal';\n ModalLarge.prototype = Object.create(Modal.prototype);\n ModalLarge.prototype.constructor = ModalLarge;\n\n /**\n * Set up all of the event handling for the modal.\n *\n * @method registerEventListeners\n */\n ModalLarge.prototype.registerEventListeners = function () {\n // Apply parent event listeners.\n Modal.prototype.registerEventListeners.call(this);\n };\n\n // Automatically register with the modal registry the first time this module is imported so that you can create modals\n // of this type using the modal factory.\n if (!registered) {\n ModalRegistry.register(ModalLarge.TYPE, ModalLarge, 'local_assessfreq/modal_large');\n registered = true;\n }\n\n return ModalLarge;\n }\n);\n"],"names":["define","$","Notification","CustomEvents","Modal","ModalRegistry","registered","ModalLarge","root","call","this","TYPE","prototype","Object","create","constructor","registerEventListeners","register"],"mappings":";;;;;;AAsBAA,sCACI,CAAC,SAAU,oBAAqB,iCAAkC,aAAc,wBAChF,SAAUC,EAAGC,aAAcC,aAAcC,MAAOC,mBAExCC,YAAa,EAObC,WAAa,SAAUC,MACvBJ,MAAMK,KAAKC,KAAMF,cAGrBD,WAAWI,KAAO,+BAClBJ,WAAWK,UAAYC,OAAOC,OAAOV,MAAMQ,YACtBG,YAAcR,WAOnCA,WAAWK,UAAUI,uBAAyB,WAE1CZ,MAAMQ,UAAUI,uBAAuBP,KAAKC,OAK3CJ,aACDD,cAAcY,SAASV,WAAWI,KAAMJ,WAAY,gCACpDD,YAAa,GAGVC"}
\ No newline at end of file
+{"version":3,"file":"modal_large.min.js","sources":["../src/modal_large.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 * Javascript for large modal .\n *\n * @module local_assessfreq/modal_large\n * @package\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n ['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_registry'],\n function($, Notification, CustomEvents, Modal, ModalRegistry) {\n\n let registered = false;\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n let ModalLarge = function(root) {\n Modal.call(this, root);\n };\n\n ModalLarge.TYPE = 'local_assesfreq-large_modal';\n ModalLarge.prototype = Object.create(Modal.prototype);\n ModalLarge.prototype.constructor = ModalLarge;\n\n /**\n * Set up all of the event handling for the modal.\n *\n * @method registerEventListeners\n */\n ModalLarge.prototype.registerEventListeners = function () {\n // Apply parent event listeners.\n Modal.prototype.registerEventListeners.call(this);\n };\n\n // Automatically register with the modal registry the first time this module is imported so that you can create modals\n // of this type using the modal factory.\n if (!registered) {\n ModalRegistry.register(ModalLarge.TYPE, ModalLarge, 'local_assessfreq/modal_large');\n registered = true;\n }\n\n return ModalLarge;\n }\n);\n"],"names":["define","$","Notification","CustomEvents","Modal","ModalRegistry","registered","ModalLarge","root","call","this","TYPE","prototype","Object","create","constructor","registerEventListeners","register"],"mappings":";;;;;;;;AAwBAA,sCACI,CAAC,SAAU,oBAAqB,iCAAkC,aAAc,wBAChF,SAASC,EAAGC,aAAcC,aAAcC,MAAOC,mBAEvCC,YAAa,EAObC,WAAa,SAASC,MACtBJ,MAAMK,KAAKC,KAAMF,cAGrBD,WAAWI,KAAO,+BAClBJ,WAAWK,UAAYC,OAAOC,OAAOV,MAAMQ,YACtBG,YAAcR,WAOnCA,WAAWK,UAAUI,uBAAyB,WAE1CZ,MAAMQ,UAAUI,uBAAuBP,KAAKC,OAK3CJ,aACDD,cAAcY,SAASV,WAAWI,KAAMJ,WAAY,gCACpDD,YAAa,GAGVC"}
\ No newline at end of file
diff --git a/amd/build/override_modal.min.js b/amd/build/override_modal.min.js
index 5f571ae4..1afe8683 100644
--- a/amd/build/override_modal.min.js
+++ b/amd/build/override_modal.min.js
@@ -1,9 +1,10 @@
/**
* Javascript for report card display and processing.
*
+ * @package
* @copyright 2020 Matt Porritt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("local_assessfreq/override_modal",["jquery","core/str","core/modal_factory","core/modal_events","core/fragment","core/ajax","core/notification"],(function($,Str,Modal,ModalEvents,Fragment,Ajax,Notification){var contextid,modalObj,callback,quizid,userid,hoursFilter,OverrideModal={};const spinner='
',createModal=function(){Str.get_string("loading").then((title=>{Modal.create({type:ModalFactory.types.DEFAULT,title:title,body:spinner,large:!0}).then((modal=>{modalObj=modal,modalObj.getRoot().on("click","#id_submitbutton",processModalForm),modalObj.getRoot().on("click","#id_cancel",(function(e){e.preventDefault(),modalObj.setBody(spinner),modalObj.hide()}))}))}))},updateModalBody=function(activity,user,formdata){void 0===formdata&&(formdata={});let params={jsonformdata:JSON.stringify(formdata),activitytype:activitytype,activityid:activity,userid:user};modalObj.setBody(spinner),Str.get_string("modal:useroverride","local_assessfreq").then((title=>{modalObj.setTitle(title),modalObj.setBody(Fragment.loadFragment("local_assessfreq","new_override_form",contextid,params))}))};function processModalForm(e){e.preventDefault();let overrideform=modalObj.getRoot().find("form").serialize(),formjson=JSON.stringify(overrideform),invalid=$.merge(modalObj.getRoot().find('[aria-invalid="true"]'),modalObj.getRoot().find(".error"));invalid.length?invalid.first().focus():Ajax.call([{methodname:"local_assessfreq_process_override_form",args:{jsonformdata:formjson,activityid:activityid,activitytype:activitytype}}])[0].done((()=>{modalObj.setBody(spinner),modalObj.hide(),void 0!==tableHandler&&tableHandler.getTable()})).fail((()=>{updateModalBody(activityid,userid,overrideform)}))}return OverrideModal.displayModalForm=function(activity,user){activityid=activity,userid=user,updateModalBody(activityid,user),modalObj.show()},OverrideModal.init=function(context,module){let tablehandler=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0;activitytype=module,contextid=context,tableHandler=tablehandler,createModal()},OverrideModal}));
//# sourceMappingURL=override_modal.min.js.map
\ No newline at end of file
diff --git a/amd/build/override_modal.min.js.map b/amd/build/override_modal.min.js.map
index 3352be45..19e086b2 100644
--- a/amd/build/override_modal.min.js.map
+++ b/amd/build/override_modal.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"override_modal.min.js","sources":["../src/override_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 .\n\n/**\n * Javascript for report card display and processing.\n *\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n ['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/fragment', 'core/ajax', 'core/notification'],\n function ($,Str, Modal, ModalEvents, Fragment, Ajax, Notification) {\n\n /**\n * Module level variables.\n */\n var OverrideModal = {};\n var contextid;\n var modalObj;\n var callback;\n var quizid;\n var userid;\n var hoursFilter;\n\n const spinner = '
'\n + ''\n + '
';\n\n /**\n * Create the modal window.\n *\n * @private\n */\n const createModal = function () {\n Str.get_string('loading', 'local_assessfreq').then((title) => {\n // Create the Modal.\n Modal.create({\n type: Modal.types.DEFAULT,\n title: title,\n body: spinner,\n large: true\n })\n .then((modal) => {\n modalObj = modal;\n // Explicitly handle form click events.\n modalObj.getRoot().on('click', '#id_submitbutton', processModalForm);\n modalObj.getRoot().on('click', '#id_cancel', function (e) {\n e.preventDefault();\n modalObj.setBody(spinner);\n modalObj.hide();\n });\n });\n return;\n }).catch(() => {\n Notification.exception(new Error('Failed to load string: loading'));\n });\n };\n\n /**\n * Updates the body of the modal window.\n *\n * @param {int} quiz The quiz id.\n * @param {int} user The user id.\n * @param {object} formdata The form data.\n */\n const updateModalBody = function (quiz, user, formdata) {\n if (typeof formdata === \"undefined\") {\n formdata = {};\n }\n\n let params = {\n 'jsonformdata': JSON.stringify(formdata),\n 'quizid': quiz,\n 'userid': user\n };\n\n modalObj.setBody(spinner);\n Str.get_string('useroverride', 'local_assessfreq').then((title) => {\n modalObj.setTitle(title);\n modalObj.setBody(Fragment.loadFragment('local_assessfreq', 'new_override_form', contextid, params));\n return;\n }).catch(() => {\n Notification.exception(new Error('Failed to load string: useroverride'));\n });\n };\n\n /**\n * Updates Moodle form with selected information.\n *\n * @param {Object} e\n * @private\n */\n function processModalForm(e) {\n e.preventDefault(); // Stop modal from closing.\n\n // Form data.\n let overrideform = modalObj.getRoot().find('form').serialize();\n let formjson = JSON.stringify(overrideform);\n\n // Handle invalid form fields for better UX.\n // I hate that I had to use JQuery for this.\n var invalid = $.merge(\n modalObj.getRoot().find('[aria-invalid=\"true\"]'),\n modalObj.getRoot().find('.error')\n );\n\n if (invalid.length) {\n invalid.first().focus();\n return;\n }\n\n // Submit form via ajax.\n Ajax.call([{\n methodname: 'local_assessfreq_process_override_form',\n args: {\n 'jsonformdata': formjson,\n 'quizid': quizid\n },\n }])[0].done(() => {\n // For submission succeeded.\n modalObj.setBody(spinner);\n modalObj.hide();\n if (hoursFilter) {\n callback(quizid, hoursFilter);\n } else {\n callback(quizid);\n }\n }).fail(() => {\n // Form submission failed server side, redisplay with errors.\n updateModalBody(quizid, userid, overrideform);\n });\n }\n\n /**\n * Display the Modal form.\n *\n * @param {int} quiz The quiz id.\n * @param {int} user The user id.\n * @param {int} hours The hours to filter the quiz by.\n */\n OverrideModal.displayModalForm = function (quiz, user, hours = null) {\n quizid = quiz;\n userid = user;\n hoursFilter = hours;\n updateModalBody(quiz, user);\n modalObj.show();\n };\n\n /**\n * Initialise method for quiz dashboard rendering.\n *\n * @param {int} context The context id for the dashboard.\n * @param {function} callbackFunction The callback function to call after the modal is closed.\n * @param {int} hours The hours to filter the quiz by.\n */\n OverrideModal.init = function (context, callbackFunction, hours = null) {\n contextid = context;\n callback = callbackFunction;\n hoursFilter = hours;\n createModal();\n };\n\n return OverrideModal;\n }\n);\n"],"names":["define","$","Str","Modal","ModalEvents","Fragment","Ajax","Notification","contextid","modalObj","callback","quizid","userid","hoursFilter","OverrideModal","spinner","createModal","get_string","then","title","create","type","types","DEFAULT","body","large","modal","getRoot","on","processModalForm","e","preventDefault","setBody","hide","catch","exception","Error","updateModalBody","quiz","user","formdata","params","JSON","stringify","setTitle","loadFragment","overrideform","find","serialize","formjson","invalid","merge","length","first","focus","call","methodname","args","done","fail","displayModalForm","hours","show","init","context","callbackFunction"],"mappings":";;;;;;AAsBAA,yCACI,CAAC,SAAU,WAAY,qBAAsB,oBAAqB,gBAAiB,YAAa,sBAChG,SAAUC,EAAEC,IAAKC,MAAOC,YAAaC,SAAUC,KAAMC,kBAM7CC,UACAC,SACAC,SACAC,OACAC,OACAC,YANAC,cAAgB,SAQdC,QAAU,sFASVC,YAAc,WAChBd,IAAIe,WAAW,UAAW,oBAAoBC,MAAMC,QAEhDhB,MAAMiB,OAAO,CACTC,KAAMlB,MAAMmB,MAAMC,QAClBJ,MAAOA,MACPK,KAAMT,QACNU,OAAO,IAEVP,MAAMQ,SACHjB,SAAWiB,OAEFC,UAAUC,GAAG,QAAS,mBAAoBC,kBACnDpB,SAASkB,UAAUC,GAAG,QAAS,cAAc,SAAUE,GACnDA,EAAEC,iBACFtB,SAASuB,QAAQjB,SACjBN,SAASwB,gBAIlBC,OAAM,KACL3B,aAAa4B,UAAU,IAAIC,MAAM,uCAWnCC,gBAAkB,SAAUC,KAAMC,KAAMC,eAClB,IAAbA,WACPA,SAAW,QAGXC,OAAS,cACOC,KAAKC,UAAUH,iBACrBF,YACAC,MAGd9B,SAASuB,QAAQjB,SACjBb,IAAIe,WAAW,eAAgB,oBAAoBC,MAAMC,QACrDV,SAASmC,SAASzB,OAClBV,SAASuB,QAAQ3B,SAASwC,aAAa,mBAAoB,oBAAqBrC,UAAWiC,YAE5FP,OAAM,KACL3B,aAAa4B,UAAU,IAAIC,MAAM,qDAUhCP,iBAAiBC,GACtBA,EAAEC,qBAGEe,aAAerC,SAASkB,UAAUoB,KAAK,QAAQC,YAC/CC,SAAWP,KAAKC,UAAUG,kBAI1BI,QAAUjD,EAAEkD,MACZ1C,SAASkB,UAAUoB,KAAK,yBACxBtC,SAASkB,UAAUoB,KAAK,WAGxBG,QAAQE,OACRF,QAAQG,QAAQC,QAKpBhD,KAAKiD,KAAK,CAAC,CACPC,WAAY,yCACZC,KAAM,cACcR,gBACNtC,WAEd,GAAG+C,MAAK,KAERjD,SAASuB,QAAQjB,SACjBN,SAASwB,OACLpB,YACAH,SAASC,OAAQE,aAEjBH,SAASC,WAEdgD,MAAK,KAEJtB,gBAAgB1B,OAAQC,OAAQkC,wBAWxChC,cAAc8C,iBAAmB,SAAUtB,KAAMC,UAAMsB,6DAAQ,KAC3DlD,OAAS2B,KACT1B,OAAS2B,KACT1B,YAAcgD,MACdxB,gBAAgBC,KAAMC,MACtB9B,SAASqD,QAUbhD,cAAciD,KAAO,SAAUC,QAASC,sBAAkBJ,6DAAQ,KAC9DrD,UAAYwD,QACZtD,SAAWuD,iBACXpD,YAAcgD,MACd7C,eAGGF"}
\ No newline at end of file
+{"version":3,"file":"override_modal.min.js","sources":["../src/override_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 .\n\n/**\n * Javascript for report card display and processing.\n *\n * @package\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n ['jquery', 'core/str', 'core/modal', 'core/modal_factory', 'core/modal_events', 'core/fragment', 'core/ajax'],\n function($, Str, Modal, ModalFactory, ModalEvents, Fragment, Ajax) {\n\n /**\n * Module level variables.\n */\n let OverrideModal = {};\n let contextid;\n let activitytype;\n let modalObj;\n let activityid;\n let userid;\n let tableHandler;\n\n const spinner = '
'\n + ''\n + '
';\n\n /**\n * Create the modal window.\n *\n * @private\n */\n const createModal = function() {\n Str.get_string('loading').then((title) => {\n // Create the Modal.\n Modal.create({\n type: ModalFactory.types.DEFAULT,\n title: title,\n body: spinner,\n large: true\n }).then((modal) => {\n modalObj = modal;\n // Explicitly handle form click events.\n modalObj.getRoot().on('click', '#id_submitbutton', processModalForm);\n modalObj.getRoot().on('click', '#id_cancel', function(e) {\n e.preventDefault();\n modalObj.setBody(spinner);\n modalObj.hide();\n });\n });\n });\n };\n\n /**\n * Updates the body of the modal window.\n *\n * @param {Integer} activity\n * @param {Integer} user\n * @param {Object} formdata\n * @private\n */\n const updateModalBody = function(activity, user, formdata) {\n if (typeof formdata === \"undefined\") {\n formdata = {};\n }\n\n let params = {\n 'jsonformdata': JSON.stringify(formdata),\n 'activitytype': activitytype,\n 'activityid': activity,\n 'userid': user\n };\n\n modalObj.setBody(spinner);\n Str.get_string('modal:useroverride', 'local_assessfreq').then((title) => {\n modalObj.setTitle(title);\n modalObj.setBody(Fragment.loadFragment('local_assessfreq', 'new_override_form', contextid, params));\n });\n };\n\n /**\n * Updates Moodle form with selected information.\n *\n * @param {Object} e\n * @private\n */\n function processModalForm(e) {\n e.preventDefault(); // Stop modal from closing.\n\n // Form data.\n let overrideform = modalObj.getRoot().find('form').serialize();\n let formjson = JSON.stringify(overrideform);\n\n // Handle invalid form fields for better UX.\n // I hate that I had to use JQuery for this.\n let invalid = $.merge(\n modalObj.getRoot().find('[aria-invalid=\"true\"]'),\n modalObj.getRoot().find('.error')\n );\n\n if (invalid.length) {\n invalid.first().focus();\n return;\n }\n\n // Submit form via ajax.\n Ajax.call([{\n methodname: 'local_assessfreq_process_override_form',\n args: {\n 'jsonformdata': formjson,\n 'activityid': activityid,\n 'activitytype': activitytype,\n },\n }])[0].done(() => {\n // For submission succeeded.\n modalObj.setBody(spinner);\n modalObj.hide();\n if (tableHandler !== undefined) {\n tableHandler.getTable();\n }\n }).fail(() => {\n // Form submission failed server side, redisplay with errors.\n updateModalBody(activityid, userid, overrideform);\n });\n }\n\n /**\n * Display the Modal form.\n * @param {Integer} activity\n * @param {Integer} user\n */\n OverrideModal.displayModalForm = function(activity, user) {\n activityid = activity;\n userid = user;\n updateModalBody(activityid, user);\n modalObj.show();\n };\n\n /**\n * Initialise method for dashboard rendering.\n * @param {Integer} context\n * @param {String} module\n * @param {TableHandler} tablehandler If defined will trigger a table refresh on form save.\n */\n OverrideModal.init = function(context, module, tablehandler = undefined) {\n activitytype = module;\n contextid = context;\n tableHandler = tablehandler;\n createModal();\n };\n\n return OverrideModal;\n }\n);\n"],"names":["define","$","Str","Modal","ModalFactory","ModalEvents","Fragment","Ajax","contextid","activitytype","modalObj","activityid","userid","tableHandler","OverrideModal","spinner","createModal","get_string","then","title","create","type","types","DEFAULT","body","large","modal","getRoot","on","processModalForm","e","preventDefault","setBody","hide","updateModalBody","activity","user","formdata","params","JSON","stringify","setTitle","loadFragment","overrideform","find","serialize","formjson","invalid","merge","length","first","focus","call","methodname","args","done","undefined","getTable","fail","displayModalForm","show","init","context","module","tablehandler"],"mappings":";;;;;;;AAuBAA,yCACI,CAAC,SAAU,WAAY,aAAc,qBAAsB,oBAAqB,gBAAiB,cACjG,SAASC,EAAGC,IAAKC,MAAOC,aAAcC,YAAaC,SAAUC,UAMrDC,UACAC,aACAC,SACAC,WACAC,OACAC,aANAC,cAAgB,SAQdC,QAAU,sFASVC,YAAc,WAChBd,IAAIe,WAAW,WAAWC,MAAMC,QAE5BhB,MAAMiB,OAAO,CACTC,KAAMjB,aAAakB,MAAMC,QACzBJ,MAAOA,MACPK,KAAMT,QACNU,OAAO,IACRP,MAAMQ,QACDhB,SAAWgB,MAEXhB,SAASiB,UAAUC,GAAG,QAAS,mBAAoBC,kBACnDnB,SAASiB,UAAUC,GAAG,QAAS,cAAc,SAASE,GAClDA,EAAEC,iBACFrB,SAASsB,QAAQjB,SACjBL,SAASuB,iBAcvBC,gBAAkB,SAASC,SAAUC,KAAMC,eACrB,IAAbA,WACPA,SAAW,QAGXC,OAAS,cACOC,KAAKC,UAAUH,uBACf5B,wBACF0B,gBACJC,MAGd1B,SAASsB,QAAQjB,SACjBb,IAAIe,WAAW,qBAAsB,oBAAoBC,MAAMC,QAC3DT,SAAS+B,SAAStB,OAClBT,SAASsB,QAAQ1B,SAASoC,aAAa,mBAAoB,oBAAqBlC,UAAW8B,sBAU1FT,iBAAiBC,GACtBA,EAAEC,qBAGEY,aAAejC,SAASiB,UAAUiB,KAAK,QAAQC,YAC/CC,SAAWP,KAAKC,UAAUG,cAI1BI,QAAU9C,EAAE+C,MACZtC,SAASiB,UAAUiB,KAAK,yBACxBlC,SAASiB,UAAUiB,KAAK,WAGxBG,QAAQE,OACRF,QAAQG,QAAQC,QAKpB5C,KAAK6C,KAAK,CAAC,CACPC,WAAY,yCACZC,KAAM,cACcR,oBACFnC,wBACEF,iBAEpB,GAAG8C,MAAK,KAER7C,SAASsB,QAAQjB,SACjBL,SAASuB,YACYuB,IAAjB3C,cACAA,aAAa4C,cAElBC,MAAK,KAEJxB,gBAAgBvB,WAAYC,OAAQ+B,wBAS5C7B,cAAc6C,iBAAmB,SAASxB,SAAUC,MAChDzB,WAAawB,SACbvB,OAASwB,KACTF,gBAAgBvB,WAAYyB,MAC5B1B,SAASkD,QASb9C,cAAc+C,KAAO,SAASC,QAASC,YAAQC,yEAAeR,EAC1D/C,aAAesD,OACfvD,UAAYsD,QACZjD,aAAemD,aACfhD,eAGGF"}
\ No newline at end of file
diff --git a/amd/build/student_search.min.js b/amd/build/student_search.min.js
index c2b1dfed..2d401351 100644
--- a/amd/build/student_search.min.js
+++ b/amd/build/student_search.min.js
@@ -3,6 +3,7 @@ define("local_assessfreq/student_search",["exports","jquery","core/notification"
* Javascript for student search display and processing.
*
* @module local_assessfreq/student_search
+ * @package local_assessfreq
* @copyright 2020 Matt Porritt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/var contextid;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_notification=_interopRequireDefault(_notification),_override_modal=_interopRequireDefault(_override_modal),TableHandler=_interopRequireWildcard(TableHandler),UserPreference=_interopRequireWildcard(UserPreference);var counterid,hoursAhead=4,hoursBehind=1,refreshPeriod=60;const refreshCounter=function(){let reset=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],progressElement=document.getElementById("local-assessfreq-period-progress");!0===reset&&(clearInterval(counterid),counterid=null,progressElement.setAttribute("style","width: 100%"),progressElement.setAttribute("aria-valuenow",100)),counterid||(counterid=setInterval((()=>{let progressWidthAria=progressElement.getAttribute("aria-valuenow");const progressStep=100/refreshPeriod;progressWidthAria-progressStep>0?(progressElement.setAttribute("style","width: "+(progressWidthAria-progressStep)+"%"),progressElement.setAttribute("aria-valuenow",progressWidthAria-progressStep)):(clearInterval(counterid),counterid=null,progressElement.setAttribute("style","width: 100%"),progressElement.setAttribute("aria-valuenow",100),TableHandler.getTable(0,[hoursAhead,hoursBehind],null),refreshCounter())}),1e3))},tableSearchAheadSet=event=>{if(event.preventDefault(),"a"===event.target.tagName.toLowerCase()){let hours=event.target.dataset.metric;UserPreference.setUserPreference("local_assessfreq_student_search_table_hoursahead_preference",hours).then((()=>{hoursAhead=hours,TableHandler.getTable(0,[hoursAhead,hoursBehind],null)})).fail((()=>{_notification.default.exception(new Error("Failed to update user preference: hours ahead"))}))}},tableSearchBehindSet=event=>{if(event.preventDefault(),"a"===event.target.tagName.toLowerCase()){let hours=event.target.dataset.metric;UserPreference.setUserPreference("local_assessfreq_student_search_table_hoursbehind_preference",hours).then((()=>{hoursBehind=hours,TableHandler.getTable(0,[hoursAhead,hoursBehind],null)})).fail((()=>{_notification.default.exception(new Error("Failed to update user preference: hours behind"))}))}},refreshAction=event=>{event.preventDefault();var element=event.target;null!==element.closest("button")&&"local-assessfreq-refresh-quiz-dashboard"===element.closest("button").id?(refreshCounter(!0),TableHandler.getTable(0,[hoursAhead,hoursBehind],null)):"a"===element.tagName.toLowerCase()&&(refreshPeriod=element.dataset.period,refreshCounter(!0),UserPreference.setUserPreference("local_assessfreq_quiz_refresh_preference",refreshPeriod))};_exports.init=context=>{contextid=context,TableHandler.init(0,contextid,"local-assessfreq-student-search-table","local-assessfreq-student-search","get_student_search_table","local_assessfreq_student_search_table_rows_preference","local-assessfreq-quiz-student-table-search","local_assessfreq_student_search_table","local_assessfreq_set_table_preference");let tableSearchInputElement=document.getElementById("local-assessfreq-quiz-student-table-search"),tableSearchResetElement=document.getElementById("local-assessfreq-quiz-student-table-search-reset"),tableSearchRowsElement=document.getElementById("local-assessfreq-quiz-student-table-rows"),tableSearchAheadElement=document.getElementById("local-assessfreq-quiz-student-table-hoursahead"),tableSearchBehindElement=document.getElementById("local-assessfreq-quiz-student-table-hoursbehind"),refreshElement=document.getElementById("local-assessfreq-period-container");tableSearchInputElement.addEventListener("keyup",TableHandler.tableSearch),tableSearchInputElement.addEventListener("paste",TableHandler.tableSearch),tableSearchResetElement.addEventListener("click",TableHandler.tableSearchReset),tableSearchRowsElement.addEventListener("click",TableHandler.tableSearchRowSet),tableSearchAheadElement.addEventListener("click",tableSearchAheadSet),tableSearchBehindElement.addEventListener("click",tableSearchBehindSet),refreshElement.addEventListener("click",refreshAction),_jquery.default.when(UserPreference.getUserPreference("local_assessfreq_student_search_table_hoursahead_preference").then((response=>{hoursAhead=response.preferences[0].value?response.preferences[0].value:4})).fail((()=>{_notification.default.exception(new Error("Failed to get use preference: hoursahead"))})),UserPreference.getUserPreference("local_assessfreq_student_search_table_hoursbehind_preference").then((response=>{hoursBehind=response.preferences[0].value?response.preferences[0].value:1})).fail((()=>{_notification.default.exception(new Error("Failed to get use preference: hoursahead"))}))).done((function(){TableHandler.getTable(0,[hoursAhead,hoursBehind],null),_override_modal.default.init(context,TableHandler.getTable,[hoursAhead,hoursBehind])}))}}));
diff --git a/amd/build/student_search.min.js.map b/amd/build/student_search.min.js.map
index 7d134685..6a4a2b5b 100644
--- a/amd/build/student_search.min.js.map
+++ b/amd/build/student_search.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"student_search.min.js","sources":["../src/student_search.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 * Javascript for student search display and processing.\n *\n * @module local_assessfreq/student_search\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Notification from 'core/notification';\nimport OverrideModal from 'local_assessfreq/override_modal';\nimport * as TableHandler from 'local_assessfreq/table_handler';\nimport * as UserPreference from 'local_assessfreq/user_preferences';\n\n/**\n * Module level variables.\n */\nvar contextid;\nvar hoursAhead = 4;\nvar hoursBehind = 1;\nvar refreshPeriod = 60;\nvar counterid;\n\n/**\n * Function for refreshing the counter.\n *\n * @param {boolean} reset the current count process.\n */\nconst refreshCounter = (reset = true) => {\n let progressElement = document.getElementById('local-assessfreq-period-progress');\n\n // Reset the current count process.\n if (reset === true) {\n clearInterval(counterid);\n counterid = null;\n progressElement.setAttribute('style', 'width: 100%');\n progressElement.setAttribute('aria-valuenow', 100);\n }\n\n // Exit early if there is already a counter running.\n if (counterid) {\n return;\n }\n\n counterid = setInterval(() => {\n let progressWidthAria = progressElement.getAttribute('aria-valuenow');\n const progressStep = 100 / refreshPeriod;\n\n if ((progressWidthAria - progressStep) > 0) {\n progressElement.setAttribute('style', 'width: ' + (progressWidthAria - progressStep) + '%');\n progressElement.setAttribute('aria-valuenow', (progressWidthAria - progressStep));\n } else {\n clearInterval(counterid);\n counterid = null;\n progressElement.setAttribute('style', 'width: 100%');\n progressElement.setAttribute('aria-valuenow', 100);\n TableHandler.getTable(0, [hoursAhead, hoursBehind], null);\n refreshCounter();\n }\n }, (1000));\n};\n\n/**\n * Process the hours ahead event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst tableSearchAheadSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let hours = event.target.dataset.metric;\n UserPreference.setUserPreference('local_assessfreq_student_search_table_hoursahead_preference', hours)\n .then(() => {\n hoursAhead = hours;\n TableHandler.getTable(0, [hoursAhead, hoursBehind], null); // Reload the table. // Reload the table.\n })\n .fail(() => {\n Notification.exception(new Error('Failed to update user preference: hours ahead'));\n });\n }\n};\n\n/**\n * Process the hours behind event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst tableSearchBehindSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let hours = event.target.dataset.metric;\n UserPreference.setUserPreference('local_assessfreq_student_search_table_hoursbehind_preference', hours)\n .then(() => {\n hoursBehind = hours;\n TableHandler.getTable(0, [hoursAhead, hoursBehind], null); // Reload the table. // Reload the table.\n })\n .fail(() => {\n Notification.exception(new Error('Failed to update user preference: hours behind'));\n });\n }\n};\n\n/**\n * Handle processing of refresh and period button actions.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst refreshAction = (event) => {\n event.preventDefault();\n var element = event.target;\n\n if (element.closest('button') !== null && element.closest('button').id === 'local-assessfreq-refresh-quiz-dashboard') {\n refreshCounter(true);\n TableHandler.getTable(0, [hoursAhead, hoursBehind], null);\n } else if (element.tagName.toLowerCase() === 'a') {\n refreshPeriod = element.dataset.period;\n refreshCounter(true);\n UserPreference.setUserPreference('local_assessfreq_quiz_refresh_preference', refreshPeriod);\n }\n};\n\n/**\n * Initialise method for student search.\n *\n * @param {integer} context The current context id.\n */\nexport const init = (context) => {\n contextid = context;\n TableHandler.init(\n 0,\n contextid,\n 'local-assessfreq-student-search-table',\n 'local-assessfreq-student-search',\n 'get_student_search_table',\n 'local_assessfreq_student_search_table_rows_preference',\n 'local-assessfreq-quiz-student-table-search',\n 'local_assessfreq_student_search_table',\n 'local_assessfreq_set_table_preference'\n );\n\n // Add required initial event listeners.\n let tableSearchInputElement = document.getElementById('local-assessfreq-quiz-student-table-search');\n let tableSearchResetElement = document.getElementById('local-assessfreq-quiz-student-table-search-reset');\n let tableSearchRowsElement = document.getElementById('local-assessfreq-quiz-student-table-rows');\n let tableSearchAheadElement = document.getElementById('local-assessfreq-quiz-student-table-hoursahead');\n let tableSearchBehindElement = document.getElementById('local-assessfreq-quiz-student-table-hoursbehind');\n let refreshElement = document.getElementById('local-assessfreq-period-container');\n\n tableSearchInputElement.addEventListener('keyup', TableHandler.tableSearch);\n tableSearchInputElement.addEventListener('paste', TableHandler.tableSearch);\n tableSearchResetElement.addEventListener('click', TableHandler.tableSearchReset);\n tableSearchRowsElement.addEventListener('click', TableHandler.tableSearchRowSet);\n tableSearchAheadElement.addEventListener('click', tableSearchAheadSet);\n tableSearchBehindElement.addEventListener('click', tableSearchBehindSet);\n refreshElement.addEventListener('click', refreshAction);\n\n $.when(\n UserPreference.getUserPreference('local_assessfreq_student_search_table_hoursahead_preference')\n .then((response) => {\n hoursAhead = response.preferences[0].value ? response.preferences[0].value : 4;\n })\n .fail(() => {\n Notification.exception(new Error('Failed to get use preference: hoursahead'));\n }),\n UserPreference.getUserPreference('local_assessfreq_student_search_table_hoursbehind_preference')\n .then((response) => {\n hoursBehind = response.preferences[0].value ? response.preferences[0].value : 1;\n })\n .fail(() => {\n Notification.exception(new Error('Failed to get use preference: hoursahead'));\n })\n ).done(function () {\n TableHandler.getTable(0, [hoursAhead, hoursBehind], null);\n OverrideModal.init(context, TableHandler.getTable, [hoursAhead, hoursBehind]);\n });\n};\n"],"names":["contextid","counterid","hoursAhead","hoursBehind","refreshPeriod","refreshCounter","reset","progressElement","document","getElementById","clearInterval","setAttribute","setInterval","progressWidthAria","getAttribute","progressStep","TableHandler","getTable","tableSearchAheadSet","event","preventDefault","target","tagName","toLowerCase","hours","dataset","metric","UserPreference","setUserPreference","then","fail","exception","Error","tableSearchBehindSet","refreshAction","element","closest","id","period","context","init","tableSearchInputElement","tableSearchResetElement","tableSearchRowsElement","tableSearchAheadElement","tableSearchBehindElement","refreshElement","addEventListener","tableSearch","tableSearchReset","tableSearchRowSet","when","getUserPreference","response","preferences","value","done"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"student_search.min.js","sources":["../src/student_search.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 * Javascript for student search display and processing.\n *\n * @module local_assessfreq/student_search\n * @package local_assessfreq\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Notification from 'core/notification';\nimport OverrideModal from 'local_assessfreq/override_modal';\nimport * as TableHandler from 'local_assessfreq/table_handler';\nimport * as UserPreference from 'local_assessfreq/user_preferences';\n\n/**\n * Module level variables.\n */\nvar contextid;\nvar hoursAhead = 4;\nvar hoursBehind = 1;\nvar refreshPeriod = 60;\nvar counterid;\n\n/**\n * Function for refreshing the counter.\n *\n * @param {boolean} reset the current count process.\n */\nconst refreshCounter = (reset = true) => {\n let progressElement = document.getElementById('local-assessfreq-period-progress');\n\n // Reset the current count process.\n if (reset === true) {\n clearInterval(counterid);\n counterid = null;\n progressElement.setAttribute('style', 'width: 100%');\n progressElement.setAttribute('aria-valuenow', 100);\n }\n\n // Exit early if there is already a counter running.\n if (counterid) {\n return;\n }\n\n counterid = setInterval(() => {\n let progressWidthAria = progressElement.getAttribute('aria-valuenow');\n const progressStep = 100 / refreshPeriod;\n\n if ((progressWidthAria - progressStep) > 0) {\n progressElement.setAttribute('style', 'width: ' + (progressWidthAria - progressStep) + '%');\n progressElement.setAttribute('aria-valuenow', (progressWidthAria - progressStep));\n } else {\n clearInterval(counterid);\n counterid = null;\n progressElement.setAttribute('style', 'width: 100%');\n progressElement.setAttribute('aria-valuenow', 100);\n TableHandler.getTable(0, [hoursAhead, hoursBehind], null);\n refreshCounter();\n }\n }, (1000));\n};\n\n/**\n * Process the hours ahead event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst tableSearchAheadSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let hours = event.target.dataset.metric;\n UserPreference.setUserPreference('local_assessfreq_student_search_table_hoursahead_preference', hours)\n .then(() => {\n hoursAhead = hours;\n TableHandler.getTable(0, [hoursAhead, hoursBehind], null); // Reload the table. // Reload the table.\n })\n .fail(() => {\n Notification.exception(new Error('Failed to update user preference: hours ahead'));\n });\n }\n};\n\n/**\n * Process the hours behind event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst tableSearchBehindSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let hours = event.target.dataset.metric;\n UserPreference.setUserPreference('local_assessfreq_student_search_table_hoursbehind_preference', hours)\n .then(() => {\n hoursBehind = hours;\n TableHandler.getTable(0, [hoursAhead, hoursBehind], null); // Reload the table. // Reload the table.\n })\n .fail(() => {\n Notification.exception(new Error('Failed to update user preference: hours behind'));\n });\n }\n};\n\n/**\n * Handle processing of refresh and period button actions.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst refreshAction = (event) => {\n event.preventDefault();\n var element = event.target;\n\n if (element.closest('button') !== null && element.closest('button').id === 'local-assessfreq-refresh-quiz-dashboard') {\n refreshCounter(true);\n TableHandler.getTable(0, [hoursAhead, hoursBehind], null);\n } else if (element.tagName.toLowerCase() === 'a') {\n refreshPeriod = element.dataset.period;\n refreshCounter(true);\n UserPreference.setUserPreference('local_assessfreq_quiz_refresh_preference', refreshPeriod);\n }\n};\n\n/**\n * Initialise method for student search.\n *\n * @param {integer} context The current context id.\n */\nexport const init = (context) => {\n contextid = context;\n TableHandler.init(\n 0,\n contextid,\n 'local-assessfreq-student-search-table',\n 'local-assessfreq-student-search',\n 'get_student_search_table',\n 'local_assessfreq_student_search_table_rows_preference',\n 'local-assessfreq-quiz-student-table-search',\n 'local_assessfreq_student_search_table',\n 'local_assessfreq_set_table_preference'\n );\n\n // Add required initial event listeners.\n let tableSearchInputElement = document.getElementById('local-assessfreq-quiz-student-table-search');\n let tableSearchResetElement = document.getElementById('local-assessfreq-quiz-student-table-search-reset');\n let tableSearchRowsElement = document.getElementById('local-assessfreq-quiz-student-table-rows');\n let tableSearchAheadElement = document.getElementById('local-assessfreq-quiz-student-table-hoursahead');\n let tableSearchBehindElement = document.getElementById('local-assessfreq-quiz-student-table-hoursbehind');\n let refreshElement = document.getElementById('local-assessfreq-period-container');\n\n tableSearchInputElement.addEventListener('keyup', TableHandler.tableSearch);\n tableSearchInputElement.addEventListener('paste', TableHandler.tableSearch);\n tableSearchResetElement.addEventListener('click', TableHandler.tableSearchReset);\n tableSearchRowsElement.addEventListener('click', TableHandler.tableSearchRowSet);\n tableSearchAheadElement.addEventListener('click', tableSearchAheadSet);\n tableSearchBehindElement.addEventListener('click', tableSearchBehindSet);\n refreshElement.addEventListener('click', refreshAction);\n\n $.when(\n UserPreference.getUserPreference('local_assessfreq_student_search_table_hoursahead_preference')\n .then((response) => {\n hoursAhead = response.preferences[0].value ? response.preferences[0].value : 4;\n })\n .fail(() => {\n Notification.exception(new Error('Failed to get use preference: hoursahead'));\n }),\n UserPreference.getUserPreference('local_assessfreq_student_search_table_hoursbehind_preference')\n .then((response) => {\n hoursBehind = response.preferences[0].value ? response.preferences[0].value : 1;\n })\n .fail(() => {\n Notification.exception(new Error('Failed to get use preference: hoursahead'));\n })\n ).done(function () {\n TableHandler.getTable(0, [hoursAhead, hoursBehind], null);\n OverrideModal.init(context, TableHandler.getTable, [hoursAhead, hoursBehind]);\n });\n};\n"],"names":["contextid","counterid","hoursAhead","hoursBehind","refreshPeriod","refreshCounter","reset","progressElement","document","getElementById","clearInterval","setAttribute","setInterval","progressWidthAria","getAttribute","progressStep","TableHandler","getTable","tableSearchAheadSet","event","preventDefault","target","tagName","toLowerCase","hours","dataset","metric","UserPreference","setUserPreference","then","fail","exception","Error","tableSearchBehindSet","refreshAction","element","closest","id","period","context","init","tableSearchInputElement","tableSearchResetElement","tableSearchRowsElement","tableSearchAheadElement","tableSearchBehindElement","refreshElement","addEventListener","tableSearch","tableSearchReset","tableSearchRowSet","when","getUserPreference","response","preferences","value","done"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/summary_participants.min.js b/amd/build/summary_participants.min.js
deleted file mode 100644
index e03b1e59..00000000
--- a/amd/build/summary_participants.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Javascript for summary participants graph.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define("local_assessfreq/summary_participants",["core/fragment","core/templates","core/str","core/notification"],(function(Fragment,Templates,Str,Notification){var Summary={chart:function(assessids,contextid){assessids.forEach((assessid=>{let chartElement=document.getElementById(assessid+"-summary-graph"),params={data:JSON.stringify({quiz:assessid,call:"participant_summary"})};Fragment.loadFragment("local_assessfreq","get_quiz_chart",contextid,params).done((response=>{let resObj=JSON.parse(response);if(1!=resObj.hasdata)Str.get_string("nodata","local_assessfreq").then((str=>{const noDatastr=document.createElement("h3");noDatastr.innerHTML=str,chartElement.innerHTML=noDatastr.outerHTML})).catch((()=>{Notification.exception(new Error("Failed to load string: nodata"))}));else{let legend={position:"left"},context={withtable:!1,chartdata:JSON.stringify(resObj.chart),aspect:!1,legend:JSON.stringify(legend)};Templates.render("local_assessfreq/chart",context).done(((html,js)=>{Templates.replaceNodeContents(chartElement,html,js)})).fail((()=>{Notification.exception(new Error("Failed to load chart template."))}))}})).fail((()=>{Notification.exception(new Error("Failed to load card."))}))}))}};return Summary}));
-
-//# sourceMappingURL=summary_participants.min.js.map
\ No newline at end of file
diff --git a/amd/build/summary_participants.min.js.map b/amd/build/summary_participants.min.js.map
deleted file mode 100644
index 1897539d..00000000
--- a/amd/build/summary_participants.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"summary_participants.min.js","sources":["../src/summary_participants.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 * Javascript for summary participants graph.\n *\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n ['core/fragment', 'core/templates', 'core/str', 'core/notification'],\n function (Fragment, Templates, Str, Notification) {\n\n /**\n * Module level variables.\n */\n var Summary = {};\n\n Summary.chart = function (assessids, contextid) {\n assessids.forEach((assessid) => {\n let chartElement = document.getElementById(assessid + '-summary-graph');\n let params = {'data': JSON.stringify({'quiz' : assessid, 'call': 'participant_summary'})};\n\n Fragment.loadFragment('local_assessfreq', 'get_quiz_chart', contextid, params)\n .done((response) => {\n let resObj = JSON.parse(response);\n if (resObj.hasdata == true) {\n let legend = {position: 'left'};\n let context = {\n 'withtable' : false,\n 'chartdata' : JSON.stringify(resObj.chart),\n 'aspect' : false,\n 'legend' : JSON.stringify(legend)\n };\n Templates.render('local_assessfreq/chart', context).done((html, js) => {\n // Load card body.\n Templates.replaceNodeContents(chartElement, html, js);\n }).fail(() => {\n Notification.exception(new Error('Failed to load chart template.'));\n return;\n });\n return;\n } else {\n Str.get_string('nodata', 'local_assessfreq').then((str) => {\n const noDatastr = document.createElement('h3');\n noDatastr.innerHTML = str;\n chartElement.innerHTML = noDatastr.outerHTML;\n return;\n }).catch(() => {\n Notification.exception(new Error('Failed to load string: nodata'));\n });\n }\n }).fail(() => {\n Notification.exception(new Error('Failed to load card.'));\n return;\n });\n });\n };\n\n return Summary;\n }\n);\n"],"names":["define","Fragment","Templates","Str","Notification","Summary","assessids","contextid","forEach","assessid","chartElement","document","getElementById","params","JSON","stringify","loadFragment","done","response","resObj","parse","hasdata","get_string","then","str","noDatastr","createElement","innerHTML","outerHTML","catch","exception","Error","legend","position","context","chart","render","html","js","replaceNodeContents","fail"],"mappings":";;;;;;AAsBAA,+CACI,CAAC,gBAAiB,iBAAkB,WAAY,sBAChD,SAAUC,SAAUC,UAAWC,IAAKC,kBAK5BC,QAAU,CAEdA,MAAgB,SAAUC,UAAWC,WACjCD,UAAUE,SAASC,eACXC,aAAeC,SAASC,eAAeH,SAAW,kBAClDI,OAAS,MAASC,KAAKC,UAAU,MAAUN,cAAkB,yBAEjER,SAASe,aAAa,mBAAoB,iBAAkBT,UAAWM,QACtEI,MAAMC,eACCC,OAASL,KAAKM,MAAMF,aACF,GAAlBC,OAAOE,QAiBPlB,IAAImB,WAAW,SAAU,oBAAoBC,MAAMC,YACzCC,UAAYd,SAASe,cAAc,MACzCD,UAAUE,UAAYH,IACtBd,aAAaiB,UAAYF,UAAUG,aAEpCC,OAAM,KACLzB,aAAa0B,UAAU,IAAIC,MAAM,8CAtBjCC,OAAS,CAACC,SAAU,QACpBC,QAAU,YACI,YACApB,KAAKC,UAAUI,OAAOgB,eACzB,SACArB,KAAKC,UAAUiB,SAE9B9B,UAAUkC,OAAO,yBAA0BF,SAASjB,MAAK,CAACoB,KAAMC,MAE5DpC,UAAUqC,oBAAoB7B,aAAc2B,KAAMC,OACnDE,MAAK,KACJpC,aAAa0B,UAAU,IAAIC,MAAM,0CAc1CS,MAAK,KACJpC,aAAa0B,UAAU,IAAIC,MAAM,wCAMtC1B"}
\ No newline at end of file
diff --git a/amd/build/table_handler.min.js b/amd/build/table_handler.min.js
index 6b506324..3ca3a6a0 100644
--- a/amd/build/table_handler.min.js
+++ b/amd/build/table_handler.min.js
@@ -3,8 +3,9 @@ define("local_assessfreq/table_handler",["exports","core/ajax","core/fragment","
* Table handler JS module.
*
* @module local_assessfreq/table_handler
+ * @package
* @copyright 2020 Guillermo Gomez
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */let cardElement,contextId,elementId,fragmentValue,hoursFilter;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.tableSortButtonAction=_exports.tableSearchRowSet=_exports.tableSearchReset=_exports.tableSearch=_exports.init=_exports.getTable=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);let rowPreference,sortValue,searchElement,id,methodName,quizId=0,overridden=!1;const getTable=function(quiz){let hours=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,sortValueTable=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,page=arguments.length>3?arguments[3]:void 0;void 0!==page&&!0!==overridden||(page=0),overridden=!1;let search=document.getElementById(searchElement).value.trim(),tableElement=document.getElementById(elementId),spinner=tableElement.getElementsByClassName("overlay-icon-container")[0],tableBody=tableElement.getElementsByClassName("table-body")[0],values={search:search,page:page};if(quiz>0&&(quizId=quiz,values.quiz=quizId),hours&&(hoursFilter=hours,values.hoursahead=hoursFilter[0],values.hoursbehind=hoursFilter[1]),sortValueTable){sortValue=sortValueTable;let sortArray=sortValue.split("_"),sortOn=sortArray[0],direction=sortArray[1];values.sorton=sortOn,values.direction=direction}let params={data:JSON.stringify(values)};spinner.classList.remove("hide"),_fragment.default.loadFragment("local_assessfreq",fragmentValue,contextId,params).done(((response,js)=>{tableBody.innerHTML=response,js&&_templates.default.runTemplateJS(js),spinner.classList.add("hide"),tableEventListeners()})).fail((()=>{_notification.default.exception(new Error("Failed to update table."))}))};_exports.getTable=getTable;const debounceTable=Debouncer.debouncer((()=>{getTable(quizId,hoursFilter,sortValue)}),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:methodName,args:{tableid:id,preference:"sortby",values:JSON.stringify(sortArray)}}])[0].then((()=>{getTable(quizId,hoursFilter,sortValue)}))},tableHide=event=>{event.preventDefault();let hideArray={};const linkUrl=new URL(event.target.closest("a").href),links=document.getElementById(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{getTable(quizId,hoursFilter,sortValue)}))},tableReset=event=>{event.preventDefault(),_ajax.default.call([{methodname:methodName,args:{tableid:id,preference:"reset",values:JSON.stringify({})}}])[0].then((()=>{getTable(quizId,hoursFilter,sortValue)}))};_exports.tableSearch=event=>{if("Meta"===event.key||event.ctrlKey)return!1;(0===event.target.value.length||event.target.value.length>2)&&debounceTable()};_exports.tableSearchReset=()=>{let tableSearchInputElement=document.getElementById(searchElement);tableSearchInputElement.value="",tableSearchInputElement.focus(),getTable(quizId,hoursFilter,sortValue)};_exports.tableSearchRowSet=event=>{if(event.preventDefault(),"a"===event.target.tagName.toLowerCase()){let rows=event.target.dataset.metric;UserPreference.setUserPreference(rowPreference,rows).then((()=>{getTable(quizId,hoursFilter,sortValue)})).fail((()=>{_notification.default.exception(new Error("Failed to update user preference: rows"))}))}};const tableNav=event=>{event.preventDefault();const page=new URL(event.target.closest("a").href).searchParams.get("page");page&&getTable(quizId,hoursFilter,sortValue,page)};_exports.tableSortButtonAction=event=>{event.preventDefault();var element=event.target;if("a"===element.tagName.toLowerCase()&&element.dataset.sort!==sortValue){sortValue=element.dataset.sort;let links=element.parentNode.getElementsByTagName("a");for(let i=0;i{const tableElement=document.getElementById(elementId);let tableNavElement;if(cardElement){const tableCardElement=document.getElementById(cardElement),links=tableElement.querySelectorAll("a"),resetLink=tableElement.getElementsByClassName("resettable"),overrideLinks=tableElement.getElementsByClassName("action-icon override"),disabledLinks=tableElement.getElementsByClassName("action-icon disabled");tableNavElement=tableCardElement.querySelectorAll("nav");for(let i=0;i0&&resetLink[0].addEventListener("click",tableReset);for(let i=0;i{event.preventDefault()}))}else tableNavElement=tableElement.querySelectorAll("nav");tableNavElement.forEach((navElement=>{navElement.addEventListener("click",tableNav)}))},triggerOverrideModal=event=>{event.preventDefault();let userid=event.target.closest("a").id.substring(25);if(userid.includes("-")){let elements=userid.split("-");quizId=elements.pop(),userid=elements.pop()}_override_modal.default.displayModalForm(quizId,userid,hoursFilter)};_exports.init=function(quiz,context,tableCardElement,tableElementId,tableFragmentValue,tableRowPreference,tableSearchElement){let tableId=arguments.length>7&&void 0!==arguments[7]?arguments[7]:null,tableMethodName=arguments.length>8&&void 0!==arguments[8]?arguments[8]:null;quizId=quiz,contextId=context,cardElement=tableCardElement,elementId=tableElementId,fragmentValue=tableFragmentValue,rowPreference=tableRowPreference,searchElement=tableSearchElement,id=tableId,methodName=tableMethodName}}));
+ */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 8fb95138..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 * @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\n/**\n * Module level variables.\n */\nlet cardElement;\nlet contextId;\nlet elementId;\nlet fragmentValue;\nlet hoursFilter;\nlet quizId = 0;\nlet overridden = false;\nlet rowPreference;\nlet sortValue;\nlet searchElement;\n\n/**\n * Table id variable.\n *\n * @type {string}\n */\nlet id;\n\n/**\n * Table method name variable.\n *\n * @type {string}\n */\nlet methodName;\n\n/**\n * Display the table that contains all the students in the exam as well as their attempts.\n *\n * @param {int} quiz The Quiz Id.\n * @param {array|null} hours Array with hour ahead or behind preference.\n * @param {string|null} sortValueTable Sort preference.\n * @param {int|string|null} page Page number.\n */\nexport const getTable = (quiz, hours = null, sortValueTable = null, page) => {\n if (typeof page === \"undefined\" || overridden === true) {\n page = 0;\n }\n\n overridden = false;\n\n let search = document.getElementById(searchElement).value.trim();\n let tableElement = document.getElementById(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 (quiz > 0) {\n quizId = quiz;\n values.quiz = quizId;\n }\n if (hours) {\n hoursFilter = hours;\n values.hoursahead = hoursFilter[0];\n values.hoursbehind = hoursFilter[1];\n }\n if (sortValueTable) {\n sortValue = sortValueTable;\n let sortArray = sortValue.split('_');\n let sortOn = sortArray[0];\n let direction = sortArray[1];\n values.sorton = sortOn;\n values.direction = direction;\n }\n\n let params = {'data': JSON.stringify(values)};\n\n spinner.classList.remove('hide'); // Show spinner if not already shown.\n Fragment.loadFragment('local_assessfreq', fragmentValue, 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 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 */\nconst debounceTable = Debouncer.debouncer(() => {\n getTable(quizId, hoursFilter, sortValue);\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 */\nconst 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 Ajax.call([{\n methodname: methodName,\n args: {\n tableid: id,\n preference: 'sortby',\n values: JSON.stringify(sortArray)\n },\n }])[0].then(() => {\n getTable(quizId, hoursFilter, sortValue); // 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 */\nconst tableHide = (event) => {\n event.preventDefault();\n\n let hideArray = {};\n const linkUrl = new URL(event.target.closest('a').href);\n const tableElement = document.getElementById(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 Ajax.call([{\n methodname: methodName,\n args: {\n tableid: id,\n preference: 'collapse',\n values: JSON.stringify(hideArray)\n },\n }])[0].then(() => {\n getTable(quizId, hoursFilter, sortValue); // 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 */\nconst tableReset = (event) => {\n event.preventDefault();\n\n // Set option via ajax.\n Ajax.call([{\n methodname: methodName,\n args: {\n tableid: id,\n preference: 'reset',\n values: JSON.stringify({})\n },\n }])[0].then(() => {\n getTable(quizId, hoursFilter, sortValue); // Reload the table.\n });\n\n};\n\n/**\n * Process the search events from the student table.\n *\n * @param {Event} event The triggered event for the element.\n *\n */\nexport const 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 debounceTable();\n }\n};\n\n/**\n * Process the search reset click event from the student table.\n *\n */\nexport const tableSearchReset = () => {\n let tableSearchInputElement = document.getElementById(searchElement);\n tableSearchInputElement.value = '';\n tableSearchInputElement.focus();\n getTable(quizId, hoursFilter, sortValue);\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 */\nexport const tableSearchRowSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let rows = event.target.dataset.metric;\n UserPreference.setUserPreference(rowPreference, rows)\n .then(() => {\n getTable(quizId, hoursFilter, sortValue); // 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 */\nconst 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 getTable(quizId, hoursFilter, sortValue, 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 */\nexport const tableSortButtonAction = (event) => {\n event.preventDefault();\n var element = event.target;\n\n if (element.tagName.toLowerCase() === 'a' && element.dataset.sort !== sortValue) {\n 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('local_assessfreq_quiz_table_inprogress_sort_preference', sortValue);\n\n debounceTable(); // Call function to update table.\n }\n};\n\n/**\n * Re-add event listeners when the student table is updated.\n */\nconst tableEventListeners = () => {\n const tableElement = document.getElementById(elementId);\n let tableNavElement;\n if (cardElement) {\n const tableCardElement = document.getElementById(cardElement);\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 tableNavElement = tableCardElement.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', tableHide);\n } else if (linkUrl.search.indexOf('tsort') !== -1) {\n links[i].addEventListener('click', tableSort);\n }\n }\n\n if (resetLink.length > 0) {\n resetLink[0].addEventListener('click', tableReset);\n }\n\n for (let i = 0; i < overrideLinks.length; i++) {\n overrideLinks[i].addEventListener('click', triggerOverrideModal);\n }\n\n for (let i = 0; i < disabledLinks.length; i++) {\n disabledLinks[i].addEventListener('click', (event) => {\n event.preventDefault();\n });\n }\n } else {\n tableNavElement = tableElement.querySelectorAll('nav');\n }\n\n tableNavElement.forEach((navElement) => {\n navElement.addEventListener('click', 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 */\nconst 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 quizId = elements.pop();\n userid = elements.pop();\n }\n\n OverrideModal.displayModalForm(quizId, userid, hoursFilter);\n};\n\n/**\n * Initialise method for table handler.\n *\n * @param {int} quiz The quiz id.\n * @param {int} context The context id.\n * @param {string} tableCardElement The table card element.\n * @param {string} tableElementId The table element id.\n * @param {string} tableFragmentValue The table fragment value.\n * @param {string} tableRowPreference The table row preference.\n * @param {string} tableSearchElement The table search element.\n * @param {string|null} tableId The table id.\n * @param {string|null} tableMethodName The table method name.\n */\nexport const init = (quiz,\n context,\n tableCardElement,\n tableElementId,\n tableFragmentValue,\n tableRowPreference,\n tableSearchElement,\n tableId = null,\n tableMethodName = null) => {\n quizId = quiz;\n contextId = context;\n cardElement = tableCardElement;\n elementId = tableElementId;\n fragmentValue = tableFragmentValue;\n rowPreference = tableRowPreference;\n searchElement = tableSearchElement;\n id = tableId;\n methodName = tableMethodName;\n };\n"],"names":["cardElement","contextId","elementId","fragmentValue","hoursFilter","rowPreference","sortValue","searchElement","id","methodName","quizId","overridden","getTable","quiz","hours","sortValueTable","page","search","document","getElementById","value","trim","tableElement","spinner","getElementsByClassName","tableBody","values","hoursahead","hoursbehind","sortArray","split","sortOn","direction","sorton","params","JSON","stringify","classList","remove","loadFragment","done","response","js","innerHTML","runTemplateJS","add","tableEventListeners","fail","exception","Error","debounceTable","Debouncer","debouncer","tableSort","event","preventDefault","linkUrl","URL","target","closest","href","targetSortBy","searchParams","get","targetSortOrder","call","methodname","args","tableid","preference","then","tableHide","hideArray","links","querySelectorAll","targetAction","targetColumn","action","column","indexOf","i","length","hideLinkUrl","tableReset","key","ctrlKey","tableSearchInputElement","focus","tagName","toLowerCase","rows","dataset","metric","UserPreference","setUserPreference","tableNav","element","sort","parentNode","getElementsByTagName","tableNavElement","tableCardElement","resetLink","overrideLinks","disabledLinks","addEventListener","triggerOverrideModal","forEach","navElement","userid","substring","includes","elements","pop","displayModalForm","context","tableElementId","tableFragmentValue","tableRowPreference","tableSearchElement","tableId","tableMethodName"],"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/build/user_preferences.min.js b/amd/build/user_preferences.min.js
index ab61f9b9..2540e4f3 100644
--- a/amd/build/user_preferences.min.js
+++ b/amd/build/user_preferences.min.js
@@ -3,6 +3,7 @@ define("local_assessfreq/user_preferences",["exports","core/ajax","core/notifica
* User preferences JS module.
*
* @module local_assessfreq/user_preferences
+ * @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.setUserPreference=_exports.getUserPreference=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);_exports.setUserPreference=(type,value)=>{const request={methodname:"core_user_update_user_preferences",args:{preferences:[{type:type,value:value}]}};return _ajax.default.call([request])[0].fail((()=>{_notification.default.exception(new Error("Failed to update user preference"))}))};_exports.getUserPreference=name=>{const request={methodname:"core_user_get_user_preferences",args:{name:name}};return _ajax.default.call([request])[0]}}));
diff --git a/amd/build/user_preferences.min.js.map b/amd/build/user_preferences.min.js.map
index 4806adb6..d0054997 100644
--- a/amd/build/user_preferences.min.js.map
+++ b/amd/build/user_preferences.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"user_preferences.min.js","sources":["../src/user_preferences.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 * User preferences JS module.\n *\n * @module local_assessfreq/user_preferences\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 Notification from 'core/notification';\n\n/**\n * Generic handler to persist user preferences.\n *\n * @method setUserPreference\n * @param {string} type The name of the attribute you're updating\n * @param {string} value The value of the attribute you're updating\n * @return {promise} jQuery promise\n */\nexport const setUserPreference = (type, value) => {\n const request = {\n methodname: 'core_user_update_user_preferences',\n args: {\n preferences: [{type: type, value: value}]\n }\n };\n\n return Ajax.call([request])[0]\n .fail(() => {\n Notification.exception(new Error('Failed to update user preference'));\n });\n};\n\n/**\n * Generic handler to get user preference.\n *\n * @method getUserPreference\n * @param {string} name The name of the attribute you're getting.\n * @return {promise} jQuery promise\n */\nexport const getUserPreference = (name) => {\n const request = {\n methodname: 'core_user_get_user_preferences',\n args: {\n 'name': name\n }\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["type","value","request","methodname","args","preferences","Ajax","call","fail","exception","Error","name"],"mappings":";;;;;;;6OAkCiC,CAACA,KAAMC,eAC9BC,QAAU,CACZC,WAAY,oCACZC,KAAM,CACFC,YAAa,CAAC,CAACL,KAAMA,KAAMC,MAAOA,iBAInCK,cAAKC,KAAK,CAACL,UAAU,GAC3BM,MAAK,2BACWC,UAAU,IAAIC,MAAM,oEAWPC,aACxBT,QAAU,CACZC,WAAY,iCACZC,KAAM,MACMO,cAITL,cAAKC,KAAK,CAACL,UAAU"}
\ No newline at end of file
+{"version":3,"file":"user_preferences.min.js","sources":["../src/user_preferences.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 * User preferences JS module.\n *\n * @module local_assessfreq/user_preferences\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 Notification from 'core/notification';\n\n/**\n * Generic handler to persist user preferences.\n *\n * @method setUserPreference\n * @param {string} type The name of the attribute you're updating\n * @param {string} value The value of the attribute you're updating\n * @return {promise} jQuery promise\n */\nexport const setUserPreference = (type, value) => {\n const request = {\n methodname: 'core_user_update_user_preferences',\n args: {\n preferences: [{type: type, value: value}]\n }\n };\n\n return Ajax.call([request])[0]\n .fail(() => {\n Notification.exception(new Error('Failed to update user preference'));\n });\n};\n\n/**\n * Generic handler to get user preference.\n *\n * @method getUserPreference\n * @param {string} name The name of the attribute you're getting.\n * @return {promise} jQuery promise\n */\nexport const getUserPreference = (name) => {\n const request = {\n methodname: 'core_user_get_user_preferences',\n args: {\n 'name': name\n }\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["type","value","request","methodname","args","preferences","Ajax","call","fail","exception","Error","name"],"mappings":";;;;;;;;6OAmCiC,CAACA,KAAMC,eAC9BC,QAAU,CACZC,WAAY,oCACZC,KAAM,CACFC,YAAa,CAAC,CAACL,KAAMA,KAAMC,MAAOA,iBAInCK,cAAKC,KAAK,CAACL,UAAU,GAC3BM,MAAK,2BACWC,UAAU,IAAIC,MAAM,oEAWPC,aACxBT,QAAU,CACZC,WAAY,iCACZC,KAAM,MACMO,cAITL,cAAKC,KAAK,CAACL,UAAU"}
\ No newline at end of file
diff --git a/amd/build/zoom_modal.min.js b/amd/build/zoom_modal.min.js
deleted file mode 100644
index 952b3e04..00000000
--- a/amd/build/zoom_modal.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Javascript for report card display and processing.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define("local_assessfreq/zoom_modal",["core/str","core/modal","core/fragment","core/ajax","core/templates","local_assessfreq/modal_large","core/notification"],(function(Str,Modal,Fragment,Ajax,Templates,ModalLarge,Notification){var contextid,modalObj,ZoomModal={};ZoomModal.zoomGraph=function(event,params,method){let title=event.target.parentElement.dataset.title;Fragment.loadFragment("local_assessfreq",method,contextid,params).done((response=>{let resObj=JSON.parse(response);if(1==resObj.hasdata){var context={withtable:!1,chartdata:JSON.stringify(resObj.chart),aspect:!1};return modalObj.setTitle(title),modalObj.setBody(Templates.render("local_assessfreq/chart",context)),void modalObj.show()}Str.get_string("nodata","local_assessfreq").then((str=>{const noDatastr=document.createElement("h3");noDatastr.innerHTML=str,modalObj.setTitle(title),modalObj.setBody(noDatastr.outerHTML),modalObj.show()})).catch((()=>{Notification.exception(new Error("Failed to load string: nodata"))}))})).fail((()=>{Notification.exception(new Error("Failed to load zoomed graph"))}))};return ZoomModal.init=function(context){contextid=context,new Promise((resolve=>{Str.get_string("loading","core").then((title=>{Modal.create({type:ModalLarge.TYPE,title:title,body:'
',large:!0}).then((modal=>{modalObj=modal,resolve()}))})).catch(Notification.exception)}))},ZoomModal}));
-
-//# sourceMappingURL=zoom_modal.min.js.map
\ No newline at end of file
diff --git a/amd/build/zoom_modal.min.js.map b/amd/build/zoom_modal.min.js.map
deleted file mode 100644
index bf129277..00000000
--- a/amd/build/zoom_modal.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"zoom_modal.min.js","sources":["../src/zoom_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 .\n\n/**\n * Javascript for report card display and processing.\n *\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n ['core/str', 'core/modal', 'core/fragment', 'core/ajax', 'core/templates', 'local_assessfreq/modal_large',\n 'core/notification'],\n function (Str, Modal, Fragment, Ajax, Templates, ModalLarge, Notification) {\n\n /**\n * Module level variables.\n */\n var ZoomModal = {};\n var contextid;\n var modalObj;\n const spinner = '
'\n + ''\n + '
';\n\n /**\n * Provides zoom functionality for card graphs.\n *\n * @param {object} event The event object.\n * @param {object} params The parameters for the fragment call.\n * @param {string} method The method to call in the fragment.\n */\n ZoomModal.zoomGraph = function (event, params, method) {\n let title = event.target.parentElement.dataset.title;\n\n Fragment.loadFragment('local_assessfreq', method, contextid, params)\n .done((response) => {\n let resObj = JSON.parse(response);\n if (resObj.hasdata == true) {\n var context = { 'withtable' : false, 'chartdata' : JSON.stringify(resObj.chart), aspect: false};\n modalObj.setTitle(title);\n modalObj.setBody(Templates.render('local_assessfreq/chart', context));\n modalObj.show();\n return;\n } else {\n Str.get_string('nodata', 'local_assessfreq').then((str) => {\n const noDatastr = document.createElement('h3');\n noDatastr.innerHTML = str;\n modalObj.setTitle(title);\n modalObj.setBody(noDatastr.outerHTML);\n modalObj.show();\n return;\n }).catch(() => {\n Notification.exception(new Error('Failed to load string: nodata'));\n });\n }\n }).fail(() => {\n Notification.exception(new Error('Failed to load zoomed graph'));\n return;\n });\n\n };\n\n /**\n * Create the modal window for graph zooming.\n *\n * @private\n */\n const createModal = function () {\n return new Promise((resolve) => {\n Str.get_string('loading', 'core').then((title) => {\n // Create the Modal.\n Modal.create({\n type: ModalLarge.TYPE,\n title: title,\n body: spinner,\n large: true\n })\n .then((modal) => {\n modalObj = modal;\n resolve();\n });\n }).catch(Notification.exception);\n });\n };\n\n /**\n * Initialise method for quiz dashboard rendering.\n *\n * @param {int} context The context id for the dashboard.\n */\n ZoomModal.init = function (context) {\n contextid = context;\n createModal();\n };\n\n return ZoomModal;\n }\n);\n"],"names":["define","Str","Modal","Fragment","Ajax","Templates","ModalLarge","Notification","contextid","modalObj","ZoomModal","zoomGraph","event","params","method","title","target","parentElement","dataset","loadFragment","done","response","resObj","JSON","parse","hasdata","context","stringify","chart","aspect","setTitle","setBody","render","show","get_string","then","str","noDatastr","document","createElement","innerHTML","outerHTML","catch","exception","Error","fail","init","Promise","resolve","create","type","TYPE","body","large","modal"],"mappings":";;;;;;AAsBAA,qCACI,CAAC,WAAY,aAAc,gBAAiB,YAAa,iBAAkB,+BAC3E,sBACA,SAAUC,IAAKC,MAAOC,SAAUC,KAAMC,UAAWC,WAAYC,kBAMrDC,UACAC,SAFAC,UAAY,GAchBA,UAAUC,UAAY,SAAUC,MAAOC,OAAQC,YACvCC,MAAQH,MAAMI,OAAOC,cAAcC,QAAQH,MAE/CZ,SAASgB,aAAa,mBAAoBL,OAAQN,UAAWK,QAC5DO,MAAMC,eACCC,OAASC,KAAKC,MAAMH,aACF,GAAlBC,OAAOG,QAAiB,KACpBC,QAAU,YAAgB,YAAqBH,KAAKI,UAAUL,OAAOM,OAAQC,QAAQ,UACzFpB,SAASqB,SAASf,OAClBN,SAASsB,QAAQ1B,UAAU2B,OAAO,yBAA0BN,eAC5DjB,SAASwB,OAGThC,IAAIiC,WAAW,SAAU,oBAAoBC,MAAMC,YACzCC,UAAYC,SAASC,cAAc,MACzCF,UAAUG,UAAYJ,IACtB3B,SAASqB,SAASf,OAClBN,SAASsB,QAAQM,UAAUI,WAC3BhC,SAASwB,UAEVS,OAAM,KACLnC,aAAaoC,UAAU,IAAIC,MAAM,wCAG1CC,MAAK,KACJtC,aAAaoC,UAAU,IAAIC,MAAM,2CAkCzClC,UAAUoC,KAAO,SAAUpB,SACvBlB,UAAYkB,QAvBL,IAAIqB,SAASC,UAChB/C,IAAIiC,WAAW,UAAW,QAAQC,MAAMpB,QAEpCb,MAAM+C,OAAO,CACTC,KAAM5C,WAAW6C,KACjBpC,MAAOA,MACPqC,KAtDA,sFAuDAC,OAAO,IAEVlB,MAAMmB,QACH7C,SAAW6C,MACXN,gBAELN,MAAMnC,aAAaoC,eAcvBjC"}
\ No newline at end of file
diff --git a/amd/src/calendar.js b/amd/src/calendar.js
deleted file mode 100644
index 726a0259..00000000
--- a/amd/src/calendar.js
+++ /dev/null
@@ -1,526 +0,0 @@
-// 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 .
-
-/**
- * Javascript for heatmap calendar generation and display.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define(['core/str', 'core/notification', 'core/ajax'], function (Str, Notification, Ajax) {
-
- /**
- * Module level variables.
- */
- var Calendar = {};
- var eventArray = [];
- const stringArr = [
- {key: 'sun', component: 'calendar'},
- {key: 'mon', component: 'calendar'},
- {key: 'tue', component: 'calendar'},
- {key: 'wed', component: 'calendar'},
- {key: 'thu', component: 'calendar'},
- {key: 'fri', component: 'calendar'},
- {key: 'sat', component: 'calendar'},
- {key: 'jan', component: 'local_assessfreq'},
- {key: 'feb', component: 'local_assessfreq'},
- {key: 'mar', component: 'local_assessfreq'},
- {key: 'apr', component: 'local_assessfreq'},
- {key: 'may', component: 'local_assessfreq'},
- {key: 'jun', component: 'local_assessfreq'},
- {key: 'jul', component: 'local_assessfreq'},
- {key: 'aug', component: 'local_assessfreq'},
- {key: 'sep', component: 'local_assessfreq'},
- {key: 'oct', component: 'local_assessfreq'},
- {key: 'nov', component: 'local_assessfreq'},
- {key: 'dec', component: 'local_assessfreq'},
- ];
- var stringResult;
- var heatRangeMax;
- var heatRangeMin;
- var colorArray;
- var processModules;
- var heatRangeScale = {'1': 0, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0};
-
- /**
- * Pick a contrasting text color based on the background color.
- *
- * @param {String} hexcolor A hexcolor value.
- * @return {String} The contrasting color (black or white).
- */
- const getContrast = function (hexcolor) {
-
- if (typeof (hexcolor) === "undefined") {
- return '#000000';
- }
-
- // If a leading # is provided, remove it.
- if (hexcolor.slice(0, 1) === '#') {
- hexcolor = hexcolor.slice(1);
- }
-
- // Convert to RGB value.
- var r = parseInt(hexcolor.substr(0,2),16);
- var g = parseInt(hexcolor.substr(2,2),16);
- var b = parseInt(hexcolor.substr(4,2),16);
-
- // Get YIQ ratio.
- var yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
-
- // Check contrast.
- return (yiq >= 128) ? '#000000' : '#FFFFFF';
- };
-
- /**
- * Check how many days in a month code.
- * from https://dzone.com/articles/determining-number-days-month.
- *
- * @method daysInMonth
- * @param {Number} month The month to get the number of days for.
- * @param {Number} year The year to get the number of days for.
- */
- const daysInMonth = function (month, year) {
- return 32 - new Date(year, month, 32).getDate();
- };
-
- /**
- * Get the heat colors to use in the heat map via Ajax.
- *
- * @method getHeatColors
- */
- const getHeatColors = function () {
- return new Promise((resolve, reject) => {
- Ajax.call([{
- methodname: 'local_assessfreq_get_heat_colors',
- args: {},
- }], true, false)[0].done(function (response) {
- colorArray = JSON.parse(response);
- resolve(colorArray);
- }).fail(function () {
- reject(new Error('Failed to get heat colors'));
- });
- });
- };
-
- /**
- * Get the event names that we are processing.
- *
- * @method getProcessEvents
- */
- const getProcessModules = function () {
- return new Promise((resolve, reject) => {
- Ajax.call([{
- methodname: 'local_assessfreq_get_process_modules',
- args: {},
- }], true, false)[0].done(function (response) {
- processModules = JSON.parse(response);
- resolve(processModules);
- }).fail(function () {
- reject(new Error('Failed to get process events'));
- });
- });
- };
-
- /**
- * Calculate the min and max values to use in the heatmap.
- *
- * @method daysInMonth
- * @param {Object} eventArray All the event count for the heatmap.
- * @param {Object} dateObj Date details.
- */
- const calcHeatRange = function (eventArray, dateObj) {
- return new Promise((resolve) => {
-
- // Resolve early if there are no events.
- if (typeof (eventArray) === "undefined") {
- heatRangeMax = 0;
- heatRangeMin = 0;
-
- resolve(eventArray);
- }
- // If scheduled tasks have not run yet we may not have any data.
- let eventArrayLength = Object.keys(eventArray).length;
- if ((eventArrayLength > 0) && (eventArray[dateObj.year] !== "undefined")) {
- let eventcount = new Array;
- let year = eventArray[dateObj.year];
-
- // Iterate through all the event counts.
- // This code looks nasty but there is only 366 days in a year.
- for (let i = 0; i < 12; i++) {
- if (typeof year[i] !== "undefined") {
- let month = year[i];
- for (let j = 0; j < 32; j++) {
- if (typeof month[j] !== "undefined") {
- eventcount.push(month[j].number);
- }
- }
- }
- }
-
- // Get min and max values to calculate heat spread.
- heatRangeMax = Math.max(...eventcount);
- heatRangeMin = Math.min(...eventcount);
- } else {
- heatRangeMax = 0;
- heatRangeMin = 0;
- }
-
- resolve(eventArray);
- });
- };
-
- /**
- * Translate assessment frequency to a heat value.
- *
- * @method getHeat
- * @param {Number} eventCount The count to get the heat value.
- * @return {Number} heat The heat value.
- */
- const getHeat = function (eventCount) {
- let scaleMin = 1;
-
- if (eventCount == heatRangeMin) {
- return scaleMin;
- }
-
- const scaleRange = 5; // 0 - 5 steps.
- const localRange = heatRangeMax - heatRangeMin;
- const localPercent = (eventCount - heatRangeMin) / localRange;
- let heat = Math.round((localPercent * scaleRange) + 1);
-
- // Clamp values.
- if (heat < 1) {
- heat = 1;
- }
-
- if (heat > 6) {
- heat = 6;
- }
-
- return heat;
- };
-
- /**
- * Get the events to display in the calendar via ajax call.
- *
- * @method getEvents
- *
- * @param {Object} args The arguments to pass to the ajax call.
- * @param {Number} args.year The year to get the events for.
- * @param {String} args.metric The metric to get the events for.
- * @param {Array} args.modules The modules to get the events for.
- *
- * @return {Promise}
- */
- const getEvents = function ({year, metric, modules}) {
- return new Promise((resolve, reject) => {
- let args = {
- year: year,
- metric: metric,
- modules: modules
- };
- let jsonArgs = JSON.stringify(args);
-
- // Get the events to use in the mapping.
- Ajax.call([{
- methodname: 'local_assessfreq_get_frequency',
- args: {
- jsondata: jsonArgs
- },
- }])[0].done((response) => {
- eventArray = JSON.parse(response);
- resolve(eventArray);
- }).fail(() => {
- reject(new Error('Failed to get events'));
- });
- });
- };
-
- /**
- * Get the events for a particular month and year.
- *
- * @param {Number} year The year to get the number of days for.
- * @param {Number} month The month to get the number of days for.
- * @return {Array} monthevents The events for the supplied month.
- */
- const getMonthEvents = function (year, month) {
- let monthevents;
-
- if ((typeof eventArray[year] !== "undefined") && (typeof eventArray[year][month] !== "undefined")) {
- monthevents = eventArray[year][month];
- }
-
- return monthevents;
- };
-
- /**
- * Create the table structure for the calendar months.
- *
- * @param {Object} args The arguments to pass to the ajax call.
- * @param {Number} args.year The year to get the events for.
- * @param {Number} args.startMonth The month to start the calendar
- * @param {Number} args.endMonth The month to end the calendar
- *
- * @return {Promise}
- */
- const createTables = function ({year, startMonth, endMonth}) {
- return new Promise((resolve, reject) => {
- let calendarContainer = document.createElement('div');
- let month = startMonth;
-
- // Itterate through and build are tables.
- for (let i = startMonth; i <= endMonth; i++) {
- // Setup some elements.
- let container = document.createElement('div');
- container.classList.add('local-assessfreq-month');
- let table = document.createElement('table');
- table.classList.add('table-striped');
- let thead = document.createElement('thead');
- let tbody = document.createElement('tbody');
- tbody.id = 'calendar-body-' + i;
- let monthRow = document.createElement('tr');
- let dayrow = document.createElement('tr');
- let monthHeader = document.createElement('th');
- monthHeader.colSpan = 7;
- monthHeader.innerHTML = stringResult[(7 + month)];
-
- for (let j = 0; j < 7; j++) {
- let dayHeader = document.createElement('th');
- dayHeader.innerHTML = stringResult[j];
- dayrow.appendChild(dayHeader);
- }
-
- // Construct the table.
- monthRow.appendChild(monthHeader);
-
- thead.appendChild(monthRow);
- thead.appendChild(dayrow);
-
- table.appendChild(thead);
- table.appendChild(tbody);
-
- container.appendChild(table);
-
- // Add to parent.
- calendarContainer.appendChild(container);
-
- // Increment variables.
- month++;
- }
-
- if ((typeof year === 'undefined') || (typeof startMonth === 'undefined') || (typeof endMonth === 'undefined')) {
- reject(Error('Failed to create calendar tables.'));
- } else {
- const resultObj = {
- calendarContainer : calendarContainer,
- year : year,
- startMonth : startMonth
- };
- resolve(resultObj);
- }
- });
- };
-
- /**
- * Generate the tooltip HTML.
- *
- * @param {Object} dayArray The details of the events for that day/
- * @return {String} tipHTML The HTML for the tooltip.
- */
- const getTooltip = function (dayArray) {
- let tipHTML = '';
-
- for (let [key, value] of Object.entries(dayArray)) {
- tipHTML += '' + processModules[key] + ': ' + value + ' ';
- }
-
- return tipHTML;
- };
-
- /**
- * Generate calendar markup for the month.
- *
- * @param {Object} table The base table to populate.
- * @param {Number} year The year to generate calendar for.
- * @param {Number} month The monthe to generate calendar for.
- */
- const populateCalendarDays = function (table, year, month) {
- let firstDay = (new Date(year, month)).getDay(); // Get the starting day of the month.
- let monthEvents = getMonthEvents(year, (month + 1)); // We add one due to month diferences between PHP and JS.
- let date = 1; // Creating all cells.
-
- for (let i = 0; i < 6; i++) {
- let row = document.createElement("tr"); // Creates a table row.
-
- // Creating individual cells, filing them up with data.
- for (let j = 0; j < 7; j++) {
- if (i === 0 && j < firstDay) {
- var cell = document.createElement("td");
- var cellText = document.createTextNode("");
- cell.dataset.event = 'false';
- } else if (date > daysInMonth(month, year)) { // Break if we have generated all the days for this month.
- break;
- } else {
- cell = document.createElement("td");
- cellText = document.createTextNode(date);
- if ((typeof monthEvents !== "undefined") && (monthEvents.hasOwnProperty(date))) {
- let heat = getHeat(monthEvents[date]['number']);
-
- if (heatRangeScale[heat] == 0 || heatRangeScale[heat] > monthEvents[date]['number']) {
- heatRangeScale[heat] = monthEvents[date]['number'];
- }
-
- cell.style.backgroundColor = colorArray[heat];
- cell.style.color = getContrast(colorArray[heat]);
-
- // Add tooltip to cell.
- cell.dataset.toggle = 'tooltip';
- cell.dataset.html = 'true';
- cell.dataset.event = 'true';
- cell.dataset.date = year + '-' + (month + 1) + '-' + date;
- cell.title = getTooltip(monthEvents[date]);
- cell.style.cursor = "pointer";
- }
- date++;
- }
-
- cell.appendChild(cellText);
- row.appendChild(cell);
- }
- table.appendChild(row); // Appending each row into calendar body.
- }
- };
-
- /**
- * Controls the population of the calendar in to the base tables.
- *
- * @param {Object} args The arguments to pass to the ajax call.
- * @param {Object} args.calendarContainer The container to populate the calendar into.
- * @param {Number} args.year The year to get the events for.
- * @param {Number} args.startMonth The month to start the calendar
- *
- * @return {Promise}
- */
- const populateCalendar = function ({calendarContainer, year, startMonth}) {
- return new Promise((resolve, reject) => {
- // Get the table boodies.
- let tables = calendarContainer.getElementsByTagName("tbody");
- let month = startMonth;
-
- // For each table body populate with calendar.
- for (var i = 0; i < tables.length; i++) {
- let table = tables[i];
- populateCalendarDays(table, year, month);
- month++;
- }
-
- if (typeof calendarContainer === 'undefined') {
- reject(Error('Failed to populate calendar tables.'));
- } else {
- resolve(calendarContainer);
- }
- });
- };
-
- /**
- * Create the heatmap scale for the calendar.
- *
- * @method createHeatScale
- */
- Calendar.createHeatScale = function () {
- return new Promise((resolve) => {
- let table = document.createElement('table');
- let tbody = document.createElement('tbody');
- let trow = document.createElement('tr');
-
- for (var i = 1; i < 7; i++) {
- if (heatRangeScale[i] !== 0) {
- let cell = document.createElement('td');
- let cellText = document.createTextNode(heatRangeScale[i] + '+');
-
- cell.appendChild(cellText);
- cell.style.backgroundColor = colorArray[i];
- cell.style.color = getContrast(colorArray[i]);
-
- trow.appendChild(cell);
- }
- }
-
- tbody.appendChild(trow);
- table.appendChild(tbody);
-
- // Reset heat range scale.
- heatRangeScale = {'1': 0, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0};
-
- resolve(table);
- });
- };
-
- /**
- * Initialise method for report calendar heatmap creation.
- *
- * @param {Number} year The year to generate the heatmap for.
- * @param {Number} startMonth The month to start with for the heatmap calendar.
- * @param {Number} endMonth The month to end with for the heatmap calendar.
- * @param {String} metric The type of metric to display, 'students' or 'aseess'.
- * @param {Array} modules The modules to display in the heatamp.
- * @return {Promise}
- */
- Calendar.generate = function (year, startMonth, endMonth, metric, modules) {
- return new Promise((resolve, reject) => {
- const dateObj = {
- year : year,
- startMonth : startMonth,
- endMonth : endMonth
- };
-
- const eventObj = {
- year : year,
- metric : metric,
- modules : modules
- };
-
- Str.get_strings(stringArr).catch(() => { // Get required strings.
- Notification.exception(new Error('Failed to load strings'));
- return;
- }).then(stringReturn => { // Save string to global to be used later.
- stringResult = stringReturn;
- return eventObj;
- })
- .then(getEvents)
- .then((eventArray) => {
- calcHeatRange(eventArray, dateObj);
- })
- .then(getHeatColors)
- .then(getProcessModules)
- .then(() => {
- return dateObj;
- })
- .then(createTables) // Create tables for calendar.
- .then(populateCalendar)
- .then((calendarHTML) => { // Return the result of the generate function.
- if (typeof calendarHTML !== 'undefined') {
- resolve(calendarHTML);
- } else {
- reject(Error('Could not generate calendar'));
- }
- });
- });
-
- };
-
- return Calendar;
-});
diff --git a/amd/src/chart_data.js b/amd/src/chart_data.js
deleted file mode 100644
index 3587f213..00000000
--- a/amd/src/chart_data.js
+++ /dev/null
@@ -1,116 +0,0 @@
-// 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 .
-
-/**
- * Chart data JS module.
- *
- * @module local_assessfreq/char_data
- * @copyright 2020 Guillermo Gomez
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-import Fragment from 'core/fragment';
-import Notification from 'core/notification';
-import * as Str from 'core/str';
-import Templates from 'core/templates';
-
-/**
- * Module level variables.
- */
-let cards;
-let contextId;
-let fragment;
-let template;
-
-/**
- * For each of the cards on the dashboard get their corresponding chart data.
- * Data is based on the year variable from the corresponding dropdown.
- * Chart data is loaded via ajax.
- *
- * @param {int|null} quizId The quiz Id.
- * @param {array|null} hoursFilter Array with hour ahead or behind preference.
- * @param {int|null} yearSelect Year selected.
- */
-export const getCardCharts = (quizId, hoursFilter, yearSelect) => {
- cards.forEach((cardData) => {
- let cardElement = document.getElementById(cardData.cardId);
- let spinner = cardElement.getElementsByClassName('overlay-icon-container')[0];
- let chartBody = cardElement.getElementsByClassName('chart-body')[0];
- let values = {'call': cardData.call};
- // Add values to Object depending on dashboard type.
- if (hoursFilter) {
- values.hoursahead = hoursFilter[0];
- values.hoursbehind = hoursFilter[1];
- }
- if (quizId) {
- values.quiz = quizId;
- }
- if (yearSelect) {
- values.year = yearSelect;
- }
- let params = {'data': JSON.stringify(values)};
-
- spinner.classList.remove('hide'); // Show sinner if not already shown.
- Fragment.loadFragment('local_assessfreq', fragment, contextId, params)
- .done((response) => {
- let resObj = JSON.parse(response);
- if (resObj.hasdata === true) {
- let context = {
- 'withtable': true, 'chartdata': JSON.stringify(resObj.chart)
- };
- if (typeof cardData.aspect !== 'undefined') {
- context.aspect = cardData.aspect;
- }
- Templates.render(template, context).done((html, js) => {
- spinner.classList.add('hide'); // Hide spinner if not already hidden.
- // Load card body.
- Templates.replaceNodeContents(chartBody, html, js);
- }).fail(() => {
- Notification.exception(new Error('Failed to load chart template.'));
- return;
- });
- return;
- } else {
- Str.get_string('nodata', 'local_assessfreq').then((str) => {
- const noDatastr = document.createElement('h3');
- noDatastr.innerHTML = str;
- chartBody.innerHTML = noDatastr.outerHTML;
- spinner.classList.add('hide'); // Hide spinner if not already hidden.
- return;
- }).catch(() => {
- Notification.exception(new Error('Failed to load string: nodata'));
- });
- }
- }).fail(() => {
- Notification.exception(new Error('Failed to load card.'));
- return;
- });
- });
-};
-
-/**
- * Initialise method for table handler.
- *
- * @param {array} cardsArray Cards array.
- * @param {int} contextIdChart The context id.
- * @param {string} fragmentChart Fragment name.
- * @param {string} templateChart Template name.
- */
-export const init = (cardsArray, contextIdChart, fragmentChart, templateChart) => {
- cards = cardsArray;
- contextId = contextIdChart;
- fragment = fragmentChart;
- template = templateChart;
-};
diff --git a/amd/src/chart_output_chartjs.js b/amd/src/chart_output_chartjs.js
deleted file mode 100644
index 5f0f1439..00000000
--- a/amd/src/chart_output_chartjs.js
+++ /dev/null
@@ -1,118 +0,0 @@
-// 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 .
-
-/**
- * Chart output for chart.js with custom override for aspect config.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define(['core/chart_output_chartjs'], function (Output) {
-
- /**
- * Module level variables.
- */
- var ChartOutput = {};
- var aspectRatio = false;
- var rtLegendoptions = false;
-
- /**
- * Overrride the config.
- *
- * @protected
- * @return {Object} The axis config.
- */
- Output.prototype._makeConfig = function () {
- var config = {
- type: this._getChartType(),
- data: {
- labels: this._cleanData(this._chart.getLabels()),
- datasets: this._makeDatasetsConfig()
- },
- options: {
- title: {
- display: this._chart.getTitle() !== null,
- text: this._cleanData(this._chart.getTitle())
- }
- }
- };
- var legendOptions = this._chart.getLegendOptions();
- if (legendOptions) {
- config.options.legend = legendOptions;
- }
-
- // Override legend options with those provided at run time.
- if (rtLegendoptions) {
- config.options.legend = rtLegendoptions;
- }
-
- this._chart.getXAxes().forEach(function (axis, i) {
- var axisLabels = axis.getLabels();
-
- config.options.scales = config.options.scales || {};
- config.options.scales.xAxes = config.options.scales.xAxes || [];
- config.options.scales.xAxes[i] = this._makeAxisConfig(axis, 'x', i);
-
- if (axisLabels !== null) {
- config.options.scales.xAxes[i].ticks.callback = function (value, index) {
- return axisLabels[index] || '';
- };
- }
- config.options.scales.xAxes[i].stacked = this._isStacked();
- }.bind(this));
-
- this._chart.getYAxes().forEach(function (axis, i) {
- var axisLabels = axis.getLabels();
-
- config.options.scales = config.options.scales || {};
- config.options.scales.yAxes = config.options.scales.yAxes || [];
- config.options.scales.yAxes[i] = this._makeAxisConfig(axis, 'y', i);
-
- if (axisLabels !== null) {
- config.options.scales.yAxes[i].ticks.callback = function (value) {
- return axisLabels[parseInt(value, 10)] || '';
- };
- }
- config.options.scales.yAxes[i].stacked = this._isStacked();
- }.bind(this));
-
- config.options.tooltips = {
- callbacks: {
- label: this._makeTooltip.bind(this)
- }
- };
-
- config.options.maintainAspectRatio = aspectRatio;
-
- return config;
- };
-
- /**
- * Get the aspect ratio setting and initialise the chart.
- *
- * @param {string} chartImage The image to replace.
- * @param {object} ChartInst The chart instance.
- * @param {boolean} aspect The aspect ratio.
- * @param {object} legend The legend options.
- */
- ChartOutput.init = function (chartImage, ChartInst, aspect, legend) {
- aspectRatio = aspect;
- rtLegendoptions = legend;
- new Output(chartImage, ChartInst);
- };
-
- return ChartOutput;
-
-});
diff --git a/amd/src/course_selector.js b/amd/src/course_selector.js
index 472cc8e5..c9ceafad 100644
--- a/amd/src/course_selector.js
+++ b/amd/src/course_selector.js
@@ -28,7 +28,7 @@ define(['core/ajax', 'core/notification'], function (Ajax, Notification) {
/**
* Module level variables.
*/
- var CourseSelector = {};
+ let CourseSelector = {};
/**
* Source of data for Ajax element.
@@ -36,9 +36,8 @@ define(['core/ajax', 'core/notification'], function (Ajax, Notification) {
* @param {String} selector The selector of the auto complete element.
* @param {String} query The query string.
* @param {Function} callback A callback function receiving an array of results.
- * @return {Void}
- */
- CourseSelector.transport = function (selector, query, callback) {
+ */
+ CourseSelector.transport = function(selector, query, callback) {
Ajax.call([{
methodname: 'local_assessfreq_get_courses',
args: {
@@ -46,9 +45,8 @@ define(['core/ajax', 'core/notification'], function (Ajax, Notification) {
},
}])[0].then((response) => {
let courseArray = JSON.parse(response);
+ // eslint-disable-next-line promise/no-callback-in-promise
callback(courseArray);
- }).fail(() => {
- Notification.exception(new Error('Failed to get events'));
});
};
diff --git a/amd/src/dashboard.js b/amd/src/dashboard.js
new file mode 100644
index 00000000..212abf00
--- /dev/null
+++ b/amd/src/dashboard.js
@@ -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 .
+
+/**
+ * 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;
+
+export const init = () => {
+
+ // Create the course search filter.
+ require(['core/form-autocomplete', 'core/str'], function(Autocomplete, Str) {
+ Str.get_string('courseselect', 'local_assessfreq').then((loading) => {
+ Autocomplete.enhance(
+ '#local-assessfreq-course-filter',
+ false,
+ 'local_assessfreq/course_selector',
+ loading,
+ false,
+ true,
+ );
+ const course_filter = document.getElementById("local-assessfreq-course-filter");
+ course_filter.addEventListener('change', event => {
+ let courseid = event.target.value;
+ let url = new URL(window.location);
+ url.searchParams.set('courseid', courseid);
+ window.location = url;
+ });
+ });
+ });
+
+ // 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 = () => {
+
+ const tabcontent = document.getElementsByClassName("tablinks");
+
+ tabcontent.forEach(el => el.addEventListener('click', event => {
+ let target = event.target.dataset.target;
+
+ let tabcontent = document.getElementsByClassName("tabcontent");
+ for (let i = 0; i < tabcontent.length; i++) {
+ tabcontent[i].style.display = "none";
+ }
+
+ // Get all elements with class="tablinks" and remove the class "active"
+ let tablinks = document.getElementsByClassName("tablinks");
+ for (let i = 0; i < tablinks.length; i++) {
+ tablinks[i].className = tablinks[i].className.replace(" active", "");
+ }
+
+ // Show the current tab, and add an "active" class to the button that opened the tab
+ document.getElementById(target).style.display = "block";
+ event.currentTarget.className += " active";
+ }));
+
+ const currentUrl = document.URL;
+ const urlParts = currentUrl.split('#');
+
+ const anchor = (urlParts.length > 1) ? urlParts[1] : null;
+ // First tab should be open by default unless we have an anchor.
+ if (!anchor || document.querySelector('[data-target="tab-' + anchor + '"]') === null) {
+ document.querySelector('[data-target="tab-heatmap"]').click();
+ } else {
+ document.querySelector('[data-target="tab-' + anchor + '"]').click();
+ }
+};
diff --git a/amd/src/dashboard_assessment.js b/amd/src/dashboard_assessment.js
deleted file mode 100644
index 144087a0..00000000
--- a/amd/src/dashboard_assessment.js
+++ /dev/null
@@ -1,371 +0,0 @@
-// 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 .
-
-/**
- * Javascript for report card display and processing.
- *
- * @module local_assessfreq/dashboard_assessment
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-import Notification from 'core/notification';
-import Calendar from 'local_assessfreq/calendar';
-import * as ChartData from 'local_assessfreq/chart_data';
-import Dayview from 'local_assessfreq/dayview';
-import * as UserPreference from 'local_assessfreq/user_preferences';
-import ZoomModal from 'local_assessfreq/zoom_modal';
-
-/**
- * Module level variables.
- */
-var contextid;
-var yearselect;
-var yearselectheatmap;
-var metricselectheatmap;
-var timeout;
-var modulesJson = '';
-var heatmapOptionsJson = '';
-
-const cards = [
- {cardId: 'local-assessfreq-assess-due-month', call: 'assess_by_month'},
- {cardId: 'local-assessfreq-assess-by-activity', call: 'assess_by_activity'},
- {cardId: 'local-assessfreq-assess-due-month-student', call: 'assess_by_month_student'}
-];
-
-/**
- * Get and process the selected year from the dropdown,
- * and update the corresponding user perference.
- *
- * @param {event} event The triggered event for the element.
- */
-const yearButtonAction = (event) => {
- event.preventDefault();
- var element = event.target;
-
- if (element.tagName.toLowerCase() === 'a' && element.dataset.year !== yearselect) { // Only act on certain elements.
- yearselect = element.dataset.year;
-
- // Save selection as a user preference.
- UserPreference.setUserPreference('local_assessfreq_overview_year_preference', yearselect);
-
- // Update card data based on selected year.
- var yeartitle = document.getElementById('local-assessfreq-report-overview')
- .getElementsByClassName('local-assessfreq-year')[0];
- yeartitle.innerHTML = yearselect;
-
- ChartData.getCardCharts(0, null, yearselect); // Process loading for the assessment cards.
- }
-};
-
-/**
- * Quick and dirty debounce method for the heatmap settings menu.
- * This stops the ajax method that updates the heatmap from being updated
- * while the user is still checking options.
- *
- */
-const updateHeatmapDebounce = () => {
- clearTimeout(timeout);
- timeout = setTimeout(updateHeatmap(), 750);
-};
-
-/**
- * Display heatmap calendar.
- *
- * @param {event} event The triggered event for the element.
- */
-const detailView = (event) => {
- let element = event.target;
- if (element.tagName.toLowerCase() === 'td' && element.dataset.event === 'true') { // Only act on certain elements.
- Dayview.display(element.dataset.date);
- }
-};
-
-/**
- * Start heatmap generation.
- *
- */
-const generateHeatmap = () => {
- let heatmapOptions = JSON.parse(heatmapOptionsJson);
- let year = parseInt(heatmapOptions.year);
- let metric = heatmapOptions.metric;
- let modules = heatmapOptions.modules;
- let heatmapContainer = document.getElementById('local-assessfreq-report-heatmap');
- let spinner = heatmapContainer.getElementsByClassName('overlay-icon-container')[0];
-
- spinner.classList.remove('hide'); // Show spinner if not already shown.
-
- Calendar.generate(year, 0, 11, metric, modules)
- .then(calendar => {
- let calendarContainer = document.getElementById('local-assessfreq-report-heatmap-months');
- calendarContainer.innerHTML = calendar.innerHTML;
- calendarContainer.addEventListener('click', detailView);
- })
- .then(Calendar.createHeatScale)
- .then((heatScale) => {
- let heatScaleContainer = document.getElementById('local-assessfreq-report-heatmap-scale');
- heatScaleContainer.innerHTML = heatScale.outerHTML;
- spinner.classList.add('hide'); // Hide sinner if not already hidden.
- })
- .catch(() => {
- Notification.exception(new Error('Failed to calendar.'));
- return;
- });
-};
-
-const updateDownload = ({year, metric, modules}) => {
- let downloadForm = document.getElementById('local-assessfreq-heatmap-form');
- let formElements = downloadForm.elements;
- let toRemove = new Array();
-
- if (modules.length === 0) {
- modules = ['all'];
- }
-
- for (let i = 0; i < formElements.length; i++) {
- if (formElements[i] === undefined) {
- continue;
- }
- // Update year field.
- if ((formElements[i].type === 'hidden') && (formElements[i].name === 'year')) {
- formElements[i].value = year;
- continue;
- }
-
- // Update metric field.
- if ((formElements[i].type === 'hidden') && (formElements[i].name === 'metric')) {
- formElements[i].value = metric;
- continue;
- }
-
- // Update module fields.
- if ((formElements[i].type === 'hidden') && (formElements[i].name.startsWith('modules'))) {
- toRemove.push(formElements[i]);
- continue;
- }
- }
-
- for (const element of toRemove) {
- element.remove();
- }
-
- for (let i = 0; i < modules.length; i++) {
- let input = document.createElement('input');
- input.type = 'hidden';
- input.name = 'modules[' + modules[i] + ']';
- input.value = modules[i];
-
- downloadForm.appendChild(input);
- }
-};
-
-/**
- * Update the heatmap based on the current filter settings.
- *
- */
-const updateHeatmap = () => {
- // Get current state of select menu items.
- var cardsModulesSelectHeatmapElement = document.getElementById('local-assessfreq-heatmap-modules');
- var links = cardsModulesSelectHeatmapElement.getElementsByTagName('a');
- var modules = [];
-
- for (var i = 0; i < links.length; i++) {
- if (links[i].classList.contains('active')) {
- let module = links[i].dataset.module;
- modules.push(module);
- }
- }
-
- // Save selection as a user preference.
- if (modulesJson !== JSON.stringify(modules)) {
- modulesJson = JSON.stringify(modules);
- UserPreference.setUserPreference('local_assessfreq_heatmap_modules_preference', modulesJson);
- }
-
- // Build settings object.
- var optionsObj = {
- 'year': yearselectheatmap,
- 'metric': metricselectheatmap,
- 'modules': modules
- };
-
- var optionsJson = JSON.stringify(optionsObj);
-
- if (optionsJson !== heatmapOptionsJson) { // Compare to global to see if there are any changes.
- // If list has changed fetch heatmap and update user preference.
- heatmapOptionsJson = optionsJson;
- generateHeatmap();
-
- // Update the download options.
- updateDownload(optionsObj);
- }
-};
-
-/**
- * Get and process the selected year from the dropdown for the heatmap display,
- * and update the corresponding user preference.
- *
- * @param {event} event The triggered event for the element.
- */
-const yearHeatmapButtonAction = (event) => {
- event.preventDefault();
- var element = event.target;
-
- if (element.tagName.toLowerCase() === 'a' && element.dataset.year !== yearselectheatmap) { // Only act on certain elements.
- yearselectheatmap = element.dataset.year;
-
- // Save selection as a user preference.
- UserPreference.setUserPreference('local_assessfreq_heatmap_year_preference', yearselectheatmap);
-
- // Update card data based on selected year.
- var yeartitle = document.getElementById('local-assessfreq-report-heatmap')
- .getElementsByClassName('local-assessfreq-year')[0];
- yeartitle.innerHTML = yearselectheatmap;
-
- updateHeatmapDebounce(); // Call function to update heatmap.
- }
-};
-
-/**
- * Get and process the selected assessment metric from the dropdown for the heatmap display,
- * and update the corresponding user preference.
- *
- * @param {event} event The triggered event for the element.
- */
-const metricHeatmapButtonAction = (event) => {
- event.preventDefault();
- var element = event.target;
-
- if (element.tagName.toLowerCase() === 'a' && element.dataset.metric !== metricselectheatmap) {
- metricselectheatmap = element.dataset.metric;
-
- // Save selection as a user preference.
- UserPreference.setUserPreference('local_assessfreq_heatmap_metric_preference', metricselectheatmap);
-
- updateHeatmapDebounce(); // Call function to update heatmap.
- }
-};
-
-/**
- * Add the event listeners to the modules in the module select dropdown.
- *
- * @param {Object} element The dropdown HTML element that contains the list of modules as links.
- */
-const moduleListChildrenEvents = (element) => {
- var links = element.getElementsByTagName('a');
- var all = links[0];
-
- for (var i = 0; i < links.length; i++) {
- let module = links[i].dataset.module;
-
- if (module.toLowerCase() === 'all') {
- links[i].addEventListener('click', function (event) {
- event.preventDefault();
- // Remove active class from all other links.
- for (var j = 0; j < links.length; j++) {
- links[j].classList.remove('active');
- }
- updateHeatmapDebounce(); // Call function to update heatmap.
- });
- } else if (module.toLowerCase() === 'close') {
- links[i].addEventListener('click', function (event) {
- event.preventDefault();
- event.stopPropagation();
-
- var dropdownmenu = document.getElementById('local-assessfreq-heatmap-modules-filter');
- dropdownmenu.classList.remove('show');
-
- updateHeatmapDebounce(); // Call function to update heatmap.
- });
- } else {
- links[i].addEventListener('click', function (event) {
- event.preventDefault();
- event.stopPropagation();
-
- all.classList.remove('active');
-
- event.target.classList.toggle('active');
- updateHeatmapDebounce();
- });
- }
- }
-};
-
-/**
- * Thin wrapper to add extra data to click event.
- *
- * @param {Event} event The triggered event for the element.
- */
-const triggerZoomGraph = (event) => {
- let call = event.target.closest('div').dataset.call;
- let params = {'data': JSON.stringify({'year': yearselect, 'call': call})};
- let method = 'get_chart';
-
- ZoomModal.zoomGraph(event, params, method);
-};
-
-/**
- * Initialise method for report card rendering.
- *
- * @param {integer} context The current context id.
- */
-export const init = (context) => {
- contextid = context;
-
- // Set up event listener and related actions for year dropdown on report cards.
- let cardsYearSelectElement = document.getElementById('local-assessfreq-cards-year');
- yearselect = cardsYearSelectElement.getElementsByClassName('active')[0].dataset.year;
- cardsYearSelectElement.addEventListener('click', yearButtonAction);
-
- // Set up event listener and related actions for year dropdown on heatmp.
- let cardsYearSelectHeatmapElement = document.getElementById('local-assessfreq-heatmap-year');
- yearselectheatmap = cardsYearSelectHeatmapElement.getElementsByClassName('active')[0].dataset.year;
- cardsYearSelectHeatmapElement.addEventListener('click', yearHeatmapButtonAction);
-
- // Set up event listener and related actions for metric dropdown on heatmp.
- let cardsMetricSelectHeatmapElement = document.getElementById('local-assessfreq-heatmap-metrics');
- metricselectheatmap = cardsMetricSelectHeatmapElement.getElementsByClassName('active')[0].dataset.metric;
- cardsMetricSelectHeatmapElement.addEventListener('click', metricHeatmapButtonAction);
-
- // Set up event listener and related actions for module dropdown on heatmp.
- let cardsModulesSelectHeatmapElement = document.getElementById('local-assessfreq-heatmap-modules');
- moduleListChildrenEvents(cardsModulesSelectHeatmapElement);
-
- // Set up zoom event listeners.
- let dueMonthZoom = document.getElementById('local-assessfreq-assess-due-month-zoom');
- dueMonthZoom.addEventListener('click', triggerZoomGraph);
-
- let dueActivityZoom = document.getElementById('local-assessfreq-assess-by-activity-zoom');
- dueActivityZoom.addEventListener('click', triggerZoomGraph);
-
- let dueStudentZoom = document.getElementById('local-assessfreq-assess-due-month-student-zoom');
- dueStudentZoom.addEventListener('click', triggerZoomGraph);
-
- // Create the zoom modal.
- ZoomModal.init(context);
-
- // Setup the dayview modal.
- Dayview.init();
-
- // Setup the chart data for each card.
- ChartData.init(cards, contextid, 'get_chart', 'core/chart');
-
- // Process loading for the assessment cards.
- ChartData.getCardCharts(0, null, yearselect);
-
- // Get the data for the heatmap.
- updateHeatmap();
-
-};
diff --git a/amd/src/dashboard_quiz.js b/amd/src/dashboard_quiz.js
deleted file mode 100644
index 41d6ed45..00000000
--- a/amd/src/dashboard_quiz.js
+++ /dev/null
@@ -1,251 +0,0 @@
-// 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 .
-
-/**
- * Javascript for report card display and processing.
- *
- * @module local_assessfreq/dashboard_quiz
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-import Ajax from 'core/ajax';
-import Notification from 'core/notification';
-import * as Str from 'core/str';
-import Templates from 'core/templates';
-import * as ChartData from 'local_assessfreq/chart_data';
-import * as FormModal from 'local_assessfreq/form_modal';
-import OverrideModal from 'local_assessfreq/override_modal';
-import * as TableHandler from 'local_assessfreq/table_handler';
-import * as UserPreference from 'local_assessfreq/user_preferences';
-import * as ZoomModal from 'local_assessfreq/zoom_modal';
-
-// Module level variables.
-
-var selectQuizStr = '';
-var contextid;
-var quizId = 0;
-var refreshPeriod = 60;
-var counterid;
-
-const cards = [
- {cardId: 'local-assessfreq-quiz-summary-graph', call: 'participant_summary', aspect: true},
- {cardId: 'local-assessfreq-quiz-summary-trend', call: 'participant_trend', aspect: false}
-];
-
-/**
- * Function for refreshing the counter.
- *
- * @param {boolean} reset the current count process.
- */
-const refreshCounter = (reset = true) => {
- let progressElement = document.getElementById('local-assessfreq-period-progress');
-
- // Reset the current count process.
- if (reset === true) {
- clearInterval(counterid);
- counterid = null;
- progressElement.setAttribute('style', 'width: 100%');
- progressElement.setAttribute('aria-valuenow', 100);
- }
-
- // Exit early if there is already a counter running.
- if (counterid) {
- return;
- }
-
- counterid = setInterval(() => {
- let progressWidthAria = progressElement.getAttribute('aria-valuenow');
- const progressStep = 100 / refreshPeriod;
-
- if ((progressWidthAria - progressStep) > 0) {
- progressElement.setAttribute('style', 'width: ' + (progressWidthAria - progressStep) + '%');
- progressElement.setAttribute('aria-valuenow', (progressWidthAria - progressStep));
- } else {
- clearInterval(counterid);
- counterid = null;
- progressElement.setAttribute('style', 'width: 100%');
- progressElement.setAttribute('aria-valuenow', 100);
- processDashboard(quizId);
- refreshCounter();
- }
- }, (1000));
-};
-
-/**
- * Callback function that is called when a quiz is selected from the form.
- * Starts the processing of the dashboard.
- *
- * @param {int} quiz The quiz Id.
- */
-const processDashboard = (quiz) => {
- quizId = quiz;
- let titleElement = document.getElementById('local-assessfreq-quiz-title');
- titleElement.innerHTML = selectQuizStr;
- // Get quiz data.
- Ajax.call([{
- methodname: 'local_assessfreq_get_quiz_data',
- args: {
- quizid: quiz
- },
- }])[0].then((response) => {
-
- let quizArray = JSON.parse(response);
- let cardsElement = document.getElementById('local-assessfreq-quiz-dashboard-cards-deck');
- let trendElement = document.getElementById('local-assessfreq-quiz-dashboard-participant-trend-deck');
- let summaryElement = document.getElementById('local-assessfreq-quiz-summary-card');
- let summarySpinner = summaryElement.getElementsByClassName('overlay-icon-container')[0];
- let tableElement = document.getElementById('local-assessfreq-quiz-table');
- let periodElement = document.getElementById('local-assessfreq-period-container');
- let tableSearchInputElement = document.getElementById('local-assessfreq-quiz-student-table-search');
- let tableSearchResetElement = document.getElementById('local-assessfreq-quiz-student-table-search-reset');
- let tableSearchRowsElement = document.getElementById('local-assessfreq-quiz-student-table-rows');
-
- let quizLink = document.createElement('a');
- quizLink.href = quizArray.url;
- quizLink.innerHTML = '';
- titleElement.innerHTML = quizArray.name + ' ';
- titleElement.appendChild(quizLink);
-
- // Update page URL with quiz ID, without reloading page so that page navigation and bookmarking works.
- const currentdUrl = new URL(window.location.href);
- const newUrl = currentdUrl.origin + currentdUrl.pathname + '?id=' + quizId;
- history.pushState({}, '', newUrl);
-
- // Update page title with quiz name.
- Str.get_string('dashboard:quiztitle', 'local_assessfreq', {'quiz': quizArray.name, 'course': quizArray.courseshortname})
- .then((str) => {
- document.title = str;
- }).catch(() => {
- Notification.exception(new Error('Failed to load string: dashboard:quiztitle'));
- });
-
- // Populate quiz summary card with details.
- Templates.render('local_assessfreq/quiz-summary-card-content', quizArray).done((html) => {
- summarySpinner.classList.add('hide');
- let contentcontainer = document.getElementById('local-assessfreq-quiz-summary-card-content');
- Templates.replaceNodeContents(contentcontainer, html, '');
- }).fail(() => {
- Notification.exception(new Error('Failed to load quiz summary template.'));
- return;
- });
-
- // Show the cards.
- cardsElement.classList.remove('hide');
- trendElement.classList.remove('hide');
- tableElement.classList.remove('hide');
- periodElement.classList.remove('hide');
-
- ChartData.getCardCharts(quizId);
- TableHandler.getTable(quizId);
- refreshCounter();
-
- tableSearchInputElement.addEventListener('keyup', TableHandler.tableSearch);
- tableSearchInputElement.addEventListener('paste', TableHandler.tableSearch);
- tableSearchResetElement.addEventListener('click', TableHandler.tableSearchReset);
- tableSearchRowsElement.addEventListener('click', TableHandler.tableSearchRowSet);
-
- return;
- }).fail(() => {
- Notification.exception(new Error('Failed to get quiz data'));
- });
-};
-
-/**
- * Handle processing of refresh and period button actions.
- *
- * @param {Event} event The triggered event for the element.
- */
-const refreshAction = (event) => {
- event.preventDefault();
- var element = event.target;
-
- if (element.closest('button') !== null && element.closest('button').id === 'local-assessfreq-refresh-quiz-dashboard') {
- refreshCounter(true);
- processDashboard(quizId);
- } else if (element.tagName.toLowerCase() === 'a') {
- refreshPeriod = element.dataset.period;
- refreshCounter(true);
- UserPreference.setUserPreference('local_assessfreq_quiz_refresh_preference', refreshPeriod);
- }
-};
-
-/**
- * Trigger the zoom graph. Thin wrapper to add extra data to click event.
- *
- * @param {Event} event The triggered event for the element.
- */
-const triggerZoomGraph = (event) => {
- let call = event.target.closest('div').dataset.call;
- let params = {'data': JSON.stringify({'quiz': quizId, 'call': call})};
- let method = 'get_quiz_chart';
-
- ZoomModal.zoomGraph(event, params, method);
-};
-
-/**
- * Initialise method for quiz dashboard rendering.
- *
- * @param {int} context The context id.
- * @param {int} quiz The quiz id.
- */
-export const init = (context, quiz) => {
- contextid = context;
- FormModal.init(context, processDashboard); // Create modal for quiz selection modal.
- ZoomModal.init(context); // Create the zoom modal.
- OverrideModal.init(context, processDashboard);
- TableHandler.init(
- quizId,
- contextid,
- 'local-assessfreq-quiz-student-table',
- 'local-assessfreq-quiz-table',
- 'get_student_table',
- 'local_assessfreq_quiz_table_rows_preference',
- 'local-assessfreq-quiz-student-table-search',
- 'local_assessfreq_student_table',
- 'local_assessfreq_set_table_preference'
- );
- ChartData.init(cards, context, 'get_quiz_chart', 'local_assessfreq/chart');
- Str.get_string('loadingquiztitle', 'local_assessfreq').then((str) => {
- selectQuizStr = str;
- }).catch(() => {
- Notification.exception(new Error('Failed to load string: loadingquiz'));
- }).then(() => {
- if (quiz > 0) {
- quizId = quiz;
- processDashboard(quiz);
- }
- });
-
- UserPreference.getUserPreference('local_assessfreq_quiz_refresh_preference')
- .then((response) => {
- refreshPeriod = response.preferences[0].value ? response.preferences[0].value : 60;
- })
- .fail(() => {
- Notification.exception(new Error('Failed to get use preference: refresh'));
- });
-
- // Event handling for refresh and period buttons.
- let refreshElement = document.getElementById('local-assessfreq-period-container');
- refreshElement.addEventListener('click', refreshAction);
-
- // Set up zoom event listeners.
- let summaryZoom = document.getElementById('local-assessfreq-quiz-summary-graph-zoom');
- summaryZoom.addEventListener('click', triggerZoomGraph);
-
- let trendZoom = document.getElementById('local-assessfreq-quiz-summary-trend-zoom');
- trendZoom.addEventListener('click', triggerZoomGraph);
-
-};
diff --git a/amd/src/dashboard_quiz_inprogress.js b/amd/src/dashboard_quiz_inprogress.js
deleted file mode 100644
index 674f7049..00000000
--- a/amd/src/dashboard_quiz_inprogress.js
+++ /dev/null
@@ -1,285 +0,0 @@
-// 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 .
-
-/**
- * Javascript for quizzes in progress display and processing.
- *
- * @module local_assessfreq/dashboard_quiz_inprogress
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-import Ajax from 'core/ajax';
-import Notification from 'core/notification';
-import Templates from 'core/templates';
-import * as ChartData from 'local_assessfreq/chart_data';
-import * as TableHandler from 'local_assessfreq/table_handler';
-import * as UserPreference from 'local_assessfreq/user_preferences';
-import * as ZoomModal from 'local_assessfreq/zoom_modal';
-
-/**
- * Module level variables.
- */
-var contextid;
-var refreshPeriod = 60;
-var counterid;
-var tablesort = 'name_asc';
-var hoursAhead = 0;
-var hoursBehind = 0;
-
-/**
- * Hours filter array.
- *
- * @type {array} Title to display on modal.
- */
-var hoursFilter;
-
-const cards = [
- {cardId: 'local-assessfreq-quiz-summary-upcomming-graph', call: 'upcomming_quizzes', aspect: true},
- {cardId: 'local-assessfreq-quiz-summary-inprogress-graph', call: 'all_participants_inprogress', aspect: true}
-];
-
-/**
- * Function for refreshing the counter.
- *
- * @param {boolean} reset the current count process.
- */
-const refreshCounter = (reset = true) => {
- let progressElement = document.getElementById('local-assessfreq-period-progress');
-
- // Reset the current count process.
- if (reset === true) {
- clearInterval(counterid);
- counterid = null;
- progressElement.setAttribute('style', 'width: 100%');
- progressElement.setAttribute('aria-valuenow', 100);
- }
-
- // Exit early if there is already a counter running.
- if (counterid) {
- return;
- }
-
- counterid = setInterval(() => {
- let progressWidthAria = progressElement.getAttribute('aria-valuenow');
- const progressStep = 100 / refreshPeriod;
-
- if ((progressWidthAria - progressStep) > 0) {
- progressElement.setAttribute('style', 'width: ' + (progressWidthAria - progressStep) + '%');
- progressElement.setAttribute('aria-valuenow', (progressWidthAria - progressStep));
- } else {
- clearInterval(counterid);
- counterid = null;
- progressElement.setAttribute('style', 'width: 100%');
- progressElement.setAttribute('aria-valuenow', 100);
- processDashboard();
- refreshCounter();
- }
- }, (1000));
-};
-
-/**
- * Starts the processing of the dashboard.
- */
-const processDashboard = () => {
- // Get summary quiz data.
- Ajax.call([{
- methodname: 'local_assessfreq_get_inprogress_counts',
- args: {},
- }])[0].then((response) => {
- let quizSummary = JSON.parse(response);
- let summaryElement = document.getElementById('local-assessfreq-quiz-dashboard-inprogress-summary-card');
- let summarySpinner = summaryElement.getElementsByClassName('overlay-icon-container')[0];
- let tableSearchInputElement = document.getElementById('local-assessfreq-quiz-inprogress-table-search');
- let tableSearchResetElement = document.getElementById('local-assessfreq-quiz-inprogress-table-search-reset');
- let tableSearchRowsElement = document.getElementById('local-assessfreq-quiz-inprogress-table-rows');
- let tableSortElement = document.getElementById('local-assessfreq-inprogress-table-sort');
-
- summaryElement.classList.remove('hide'); // Show the card.
-
- // Populate summary card with details.
- Templates.render('local_assessfreq/quiz-dashboard-inprogress-summary-card-content', quizSummary)
- .done((html) => {
- summarySpinner.classList.add('hide');
-
- let contentcontainer = document.getElementById('local-assessfreq-quiz-dashboard-inprogress-summary-card-content');
- Templates.replaceNodeContents(contentcontainer, html, '');
- }).fail(() => {
- Notification.exception(new Error('Failed to load quiz counts template.'));
- return;
- });
-
- hoursFilter = [hoursAhead, hoursBehind];
- ChartData.getCardCharts(0, hoursFilter);
- TableHandler.getTable(0, hoursFilter, tablesort);
- refreshCounter();
-
- // Table event listeners.
- tableSearchInputElement.addEventListener('keyup', TableHandler.tableSearch);
- tableSearchInputElement.addEventListener('paste', TableHandler.tableSearch);
- tableSearchResetElement.addEventListener('click', TableHandler.tableSearchReset);
- tableSearchRowsElement.addEventListener('click', TableHandler.tableSearchRowSet);
- tableSortElement.addEventListener('click', TableHandler.tableSortButtonAction);
-
- return;
- }).fail(() => {
- Notification.exception(new Error('Failed to get quiz summary counts'));
- });
-};
-
-/**
- * Handle processing of refresh and period button actions.
- *
- * @param {Event} event The triggered event for the element.
- */
-const refreshAction = (event) => {
- event.preventDefault();
- var element = event.target;
-
- if (element.closest('button') !== null && element.closest('button').id === 'local-assessfreq-refresh-quiz-dashboard') {
- refreshCounter(true);
- processDashboard();
- } else if (element.tagName.toLowerCase() === 'a') {
- refreshPeriod = element.dataset.period;
- refreshCounter(true);
- UserPreference.setUserPreference('local_assessfreq_quiz_refresh_preference', refreshPeriod);
- }
-};
-
-/**
- * Trigger the zoom graph. Thin wrapper to add extra data to click event.
- *
- * @param {Event} event The triggered event for the element.
- */
-const triggerZoomGraph = (event) => {
- let call = event.target.closest('div').dataset.call;
- let params = {'data': JSON.stringify({'call': call, 'hoursahead': hoursAhead, 'hoursbehind': hoursBehind})};
- let method = 'get_quiz_inprogress_chart';
-
- ZoomModal.zoomGraph(event, params, method);
-};
-
-/**
- * Process the hours ahead event from the in progress quizzes table.
- *
- * @param {Event} event The triggered event for the element.
- */
-const quizzesAheadSet = (event) => {
- event.preventDefault();
- if (event.target.tagName.toLowerCase() === 'a') {
- let hours = event.target.dataset.metric;
- UserPreference.setUserPreference('local_assessfreq_quizzes_inprogress_table_hoursahead_preference', hours)
- .then(() => {
- hoursAhead = hours;
- processDashboard(); // Reload the table.
- })
- .fail(() => {
- Notification.exception(new Error('Failed to update user preference: hours ahead'));
- });
- }
-};
-
-/**
- * Process the hours behind event from the in progress quizzes table.
- *
- * @param {Event} event The triggered event for the element.
- */
-const quizzesBehindSet = (event) => {
- event.preventDefault();
- if (event.target.tagName.toLowerCase() === 'a') {
- let hours = event.target.dataset.metric;
- UserPreference.setUserPreference('local_assessfreq_quizzes_inprogress_table_hoursbehind_preference', hours)
- .then(() => {
- hoursBehind = hours;
- processDashboard(); // Reload the table.
- })
- .fail(() => {
- Notification.exception(new Error('Failed to update user preference: hours behind'));
- });
- }
-};
-
-/**
- * Initialise method for quizzes in progress dashboard rendering.
- *
- * @param {int} context The context id.
- */
-export const init = (context) => {
- contextid = context;
- ZoomModal.init(context); // Create the zoom modal.
- TableHandler.init(
- 0,
- contextid,
- null,
- 'local-assessfreq-quiz-inprogress-table',
- 'get_quizzes_inprogress_table',
- 'local_assessfreq_quiz_table_inprogress_preference',
- 'local-assessfreq-quiz-inprogress-table-search'
- );
- ChartData.init(cards, context, 'get_quiz_inprogress_chart', 'local_assessfreq/chart');
-
- UserPreference.getUserPreference('local_assessfreq_quiz_refresh_preference')
- .then((response) => {
- refreshPeriod = response.preferences[0].value ? response.preferences[0].value : 60;
- })
- .fail(() => {
- Notification.exception(new Error('Failed to get use preference: refresh'));
- });
-
- UserPreference.getUserPreference('local_assessfreq_quiz_table_inprogress_sort_preference')
- .then((response) => {
- tablesort = response.preferences[0].value ? response.preferences[0].value : 'name_asc';
- })
- .fail(() => {
- Notification.exception(new Error('Failed to get use preference: tablesort'));
- });
-
- UserPreference.getUserPreference('local_assessfreq_quizzes_inprogress_table_hoursahead_preference')
- .then((response) => {
- hoursAhead = response.preferences[0].value ? response.preferences[0].value : 0;
- })
- .fail(() => {
- Notification.exception(new Error('Failed to get use preference: hoursahead'));
- });
-
- UserPreference.getUserPreference('local_assessfreq_quizzes_inprogress_table_hoursbehind_preference')
- .then((response) => {
- hoursBehind = response.preferences[0].value ? response.preferences[0].value : 0;
- })
- .fail(() => {
- Notification.exception(new Error('Failed to get use preference: hoursbehind'));
- });
-
- // Event handling for refresh and period buttons.
- let refreshElement = document.getElementById('local-assessfreq-period-container');
- refreshElement.addEventListener('click', refreshAction);
-
- // Set up zoom event listeners.
- let summaryZoom = document.getElementById('local-assessfreq-quiz-summary-inprogress-graph-zoom');
- summaryZoom.addEventListener('click', triggerZoomGraph);
-
- let upcommingZoom = document.getElementById('local-assessfreq-quiz-summary-upcomming-graph-zoom');
- upcommingZoom.addEventListener('click', triggerZoomGraph);
-
- // Set up behind and ahead quizzes event listeners.
- let quizzesAheadElement = document.getElementById('local-assessfreq-quiz-student-table-hoursahead');
- quizzesAheadElement.addEventListener('click', quizzesAheadSet);
-
- let quizzesBehindElement = document.getElementById('local-assessfreq-quiz-student-table-hoursbehind');
- quizzesBehindElement.addEventListener('click', quizzesBehindSet);
-
- processDashboard();
-
-};
diff --git a/amd/src/dayview.js b/amd/src/dayview.js
deleted file mode 100644
index 2dbfaaed..00000000
--- a/amd/src/dayview.js
+++ /dev/null
@@ -1,208 +0,0 @@
-// 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 .
-
-/**
- * Javascript for heatmap calendar generation and display.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define(
- ['core/str', 'core/notification', 'core/modal', 'local_assessfreq/modal_large', 'core/templates', 'core/ajax'],
- function (Str, Notification, Modal, ModalLarge, Templates, Ajax) {
-
- /**
- * Module level variables.
- */
- var Dayview = {};
- var modalObj;
- const spinner = '
'
- + ''
- + '
';
-
- const stringArr = [
- {key: 'sun', component: 'calendar'},
- {key: 'mon', component: 'calendar'},
- {key: 'tue', component: 'calendar'},
- {key: 'wed', component: 'calendar'},
- {key: 'thu', component: 'calendar'},
- {key: 'fri', component: 'calendar'},
- {key: 'sat', component: 'calendar'},
- {key: 'jan', component: 'local_assessfreq'},
- {key: 'feb', component: 'local_assessfreq'},
- {key: 'mar', component: 'local_assessfreq'},
- {key: 'apr', component: 'local_assessfreq'},
- {key: 'may', component: 'local_assessfreq'},
- {key: 'jun', component: 'local_assessfreq'},
- {key: 'jul', component: 'local_assessfreq'},
- {key: 'aug', component: 'local_assessfreq'},
- {key: 'sep', component: 'local_assessfreq'},
- {key: 'oct', component: 'local_assessfreq'},
- {key: 'nov', component: 'local_assessfreq'},
- {key: 'dec', component: 'local_assessfreq'},
- ];
- var stringResult;
- var systemTimezone = 'Australia/Melbourne';
- var dayViewTitle = '';
-
- const getUserDate = function (timestamp, format) {
- return new Promise((resolve) => {
- const systemTimezoneTime = new Date(timestamp * 1000).toLocaleString('en-US', {timeZone: systemTimezone});
- let date = new Date(systemTimezoneTime);
- const year = date.getFullYear();
- const month = stringResult[(7 + date.getMonth())];
- const day = date.getDate();
- const hours = date.getHours();
- const minutes = '0' + date.getMinutes();
-
- const strftimetime = hours + ':' + minutes.substr(-2); // Will display time in 10:30 format.
- const strftimedatetime = day + ' ' + month + ' ' + year + ', ' + strftimetime;
-
- if (format === 'strftimetime') {
- resolve(strftimetime);
- } else {
- resolve(strftimedatetime);
- }
-
- });
- };
-
- const formatData = async function (response) {
- let responseArr = JSON.parse(response);
-
- // We are displaying the event as a bar whose width represents the start and end time of the event.
- // We need to scale the width of the bar to match the width of the container. Therefore 100% width of the container
- // equals 24 hours (one day).
- // There are 1440 mins per day. 1440 mins equals 100%, therefore 1 min = (100/1440)%. 5/72 == 100/1440.
- let scaler = 5 / 72;
-
- for (let i = 0; i < responseArr.length; i++) {
- const year = responseArr[i].endyear;
- const month = (responseArr[i].endmonth) - 1; // Minus 1 for difference between months in PHP and JS.
- const day = responseArr[i].endday;
- const dayStart = (new Date(year, month, day).getTime()) / 1000;
- const timeStart = new Date(responseArr[i].timestart * 1000).toLocaleString('en-US', {timeZone: systemTimezone});
- const timeStartTimestamp = (new Date(timeStart).getTime()) / 1000;
- const timeEnd = new Date(responseArr[i].timeend * 1000).toLocaleString('en-US', {timeZone: systemTimezone});
- const timeEndTimestamp = (new Date(timeEnd).getTime()) / 1000;
- let secondsSinceDayStart = timeStartTimestamp - dayStart;
- let leftMargin = 0;
- let width = 0;
-
- if (secondsSinceDayStart <= 0) {
- secondsSinceDayStart = 0;
- width = ((timeEndTimestamp - dayStart) / 60) * scaler;
- responseArr[i].start = await getUserDate(responseArr[i].timestart, 'strftimedatetime');
- } else {
- leftMargin = (secondsSinceDayStart / 60) * scaler;
- width = ((timeEndTimestamp - timeStartTimestamp) / 60) * scaler;
- responseArr[i].start = await getUserDate(responseArr[i].timestart, 'strftimetime');
- }
-
- if (leftMargin + width > 100) {
- width = 100 - leftMargin;
- }
-
- responseArr[i].leftmargin = leftMargin;
- responseArr[i].width = width;
- responseArr[i].end = await getUserDate(responseArr[i].timeend, 'strftimetime');
- }
-
- return new Promise((resolve) => {
- resolve(responseArr);
- });
- };
-
- /**
- * Initialise the base modal to be used.
- *
- * @param {int} date The date to display the day view for.
- *
- */
- Dayview.display = function (date) {
- modalObj.setBody(spinner);
- modalObj.show();
- let args = {
- date: date,
- modules: ['all']
- };
- let jsonArgs = JSON.stringify(args);
- Ajax.call([{
- methodname: 'local_assessfreq_get_day_events',
- args: {jsondata: jsonArgs},
- }])[0]
- .then(formatData)
- .then((responseArr) => {
-
- let context = {rows: responseArr};
- const year = responseArr[0].endyear;
- const day = responseArr[0].endday;
- const month = stringResult[(6 + parseInt(responseArr[0].endmonth))];
- const dayDate = day + ' ' + month + ' ' + year;
-
- modalObj.setTitle(dayViewTitle + ' ' + dayDate);
- modalObj.setBody(Templates.render('local_assessfreq/dayview', context));
-
- }).fail(() => {
- Notification.exception(new Error('Failed to load day view'));
- });
- };
-
- /**
- * Initialise the base modal to be used.
- *
- */
- Dayview.init = function () {
- // Load the strings we'll need later.
- Str.get_strings(stringArr).catch(() => { // Get required strings.
- Notification.exception(new Error('Failed to load strings'));
- return;
- }).then(stringReturn => { // Save string to global to be used later.
- stringResult = stringReturn;
- });
-
- // Get the system timzone.
- Ajax.call([{
- methodname: 'local_assessfreq_get_system_timezone',
- args: {},
- }], true, false)[0].then((response) => {
- systemTimezone = response;
- return;
- }).fail(() => {
- Notification.exception(new Error('Failed to get system timezone'));
- });
-
- Str.get_string('schedule', 'local_assessfreq').then((title) => {
- dayViewTitle = title;
-
- // Create the Modal.
- Modal.create({
- type: ModalLarge.TYPE,
- title: title,
- body: spinner,
- large: true
- })
- .then((modal) => {
- modalObj = modal;
-
- });
- }).catch(Notification.exception);
-
- };
-
- return Dayview;
- }
-);
diff --git a/amd/src/debouncer.js b/amd/src/debouncer.js
index 6e0f35ba..531d6050 100644
--- a/amd/src/debouncer.js
+++ b/amd/src/debouncer.js
@@ -17,6 +17,7 @@
* Debounce JS module.
*
* @module local_assessfreq/debouncer
+ * @package
* @copyright 2020 Guillermo Gomez
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
diff --git a/amd/src/form_modal.js b/amd/src/form_modal.js
deleted file mode 100644
index 5d2f6dc4..00000000
--- a/amd/src/form_modal.js
+++ /dev/null
@@ -1,242 +0,0 @@
-// 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 .
-
-/**
- * Javascript for report card display and processing.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define(
- ['core/str', 'core/modal', 'core/fragment', 'core/ajax', 'core/notification'],
- function (Str, Modal, Fragment, Ajax, Notification) {
-
- /**
- * Module level variables.
- */
- var FormModal = {};
- var contextid;
- var modalObj;
- var resetOptions = [];
- var callback;
-
- const spinner = '
'
- + ''
- + '
';
-
- const observerConfig = { attributes: true, childList: false, subtree: true };
-
- const ObserverCallback = function (mutationsList) {
- for (let i = 0; i < mutationsList.length; i++) {
- let element = mutationsList[i].target;
- if (element.tagName.toLowerCase() === 'span' && element.classList.contains('badge')) {
- element.addEventListener('click', updateModalBody);
- document.getElementById('id_courses').dataset.course = element.dataset.value;
-
- document.getElementById('id_quiz').value = -1;
- Ajax.call([{
- methodname: 'local_assessfreq_get_quizzes',
- args: {
- query: mutationsList[i].target.dataset.value
- },
- }])[0].done((response) => {
- let quizArray = JSON.parse(response);
- let selectElement = document.getElementById('id_quiz');
- let selectElementLength = selectElement.options.length;
- if (document.getElementById('noquizwarning') !== null) {
- document.getElementById('noquizwarning').remove();
- }
- // Clear exisitng options.
- for (let j = selectElementLength - 1; j >= 0; j--) {
- selectElement.options[j] = null;
- }
-
- if (quizArray.length > 0) {
- // Add new options.
- for (let k = 0; k < quizArray.length; k++) {
- let opt = quizArray[k];
- let el = document.createElement('option');
- el.textContent = opt.name;
- el.value = opt.id;
- selectElement.appendChild(el);
- }
- selectElement.removeAttribute('disabled');
- if (document.getElementById('noquizwarning') !== null) {
- document.getElementById('noquizwarning').remove();
- }
- } else {
- resetOptions.forEach((option) => {
- selectElement.appendChild(option);
- });
- document.getElementById('id_quiz').value = 0;
- selectElement.disabled = true;
- }
-
- }).fail(() => {
- Notification.exception(new Error('Failed to get quizzes'));
- });
-
- break;
- }
- }
- };
-
- const observer = new MutationObserver(ObserverCallback);
-
- /**
- * Create the modal window.
- *
- * @private
- */
- const createModal = function () {
- Str.get_string('loading', 'local_assessfreq').then((title) => {
- // Create the Modal.
- Modal.create({
- type: Modal.types.DEFAULT,
- title: title,
- body: spinner,
- large: true
- })
- .then((modal) => {
- modalObj = modal;
-
- // Explicitly handle form click events.
- modalObj.getRoot().on('click', '#id_submitbutton', processModalForm);
- modalObj.getRoot().on('click', '#id_cancel', (e) => {
- e.preventDefault();
- modalObj.setBody(spinner);
- modalObj.hide();
- });
- });
- return;
- }).catch(Notification.exception);
- };
-
- const getOptionPlaceholders = function () {
- return new Promise((resolve, reject) => {
- const stringArr = [
- {key: 'selectcourse', component: 'local_assessfreq'},
- {key: 'loadingquiz', component: 'local_assessfreq'},
- ];
-
- Str.get_strings(stringArr).catch(() => { // Get required strings.
- reject(new Error('Failed to load strings'));
- return;
- }).then(stringReturn => { // Save string to global to be used later.
- for (let i = 0; i < stringReturn.length; i++) {
- let el = document.createElement('option');
- el.textContent = stringReturn[i];
- el.value = 0 - i;
- resetOptions.push(el);
- }
- resolve();
- });
- });
- };
-
- /**
- * Updates the body of the modal window.
- *
- * @param {Object} formdata
- * @private
- */
- const updateModalBody = function (formdata) {
- if (typeof formdata === "undefined") {
- formdata = {};
- }
-
- let params = {
- 'jsonformdata': JSON.stringify(formdata)
- };
-
- getOptionPlaceholders()
- .then(() => {
- Str.get_string('searchquiz', 'local_assessfreq').then((title) => {
- modalObj.setTitle(title);
- modalObj.setBody(Fragment.loadFragment('local_assessfreq', 'new_base_form', contextid, params));
- let modalContainer = document.querySelectorAll('[data-region*="modal-container"]')[0];
- observer.observe(modalContainer, observerConfig);
-
- return;
- }).catch(() => {
- Notification.exception(new Error('Failed to load string: searchquiz'));
- });
- });
- };
-
- /**
- * Updates Moodle form with selected information.
- *
- * @param {Object} e
- * @private
- */
- const processModalForm = function (e) {
- e.preventDefault(); // Stop modal from closing.
-
- let quizElement = document.getElementById('id_quiz');
- let quizId = quizElement.options[quizElement.selectedIndex].value;
- let courseId = document.getElementById('id_courses').dataset.course;
-
- if (courseId === undefined || quizId < 1) {
- if (document.getElementById('noquizwarning') === null) {
- Str.get_string('noquizselected', 'local_assessfreq').then((warning) => {
- let element = document.createElement('div');
- element.innerHTML = warning;
- element.id = 'noquizwarning';
- element.classList.add('alert', 'alert-danger');
- modalObj.getBody().prepend(element);
-
- return;
- }).catch(() => {
- Notification.exception(new Error('Failed to load string: searchquiz'));
- });
- }
- } else {
- modalObj.hide(); // Close modal.
- modalObj.setBody(''); // Cleaer form.
- observer.disconnect(); // Remove observer.
- callback(quizId, courseId); // Trigger dashboard update.
- }
-
- };
-
- /**
- * Display the Modal form.
- */
- const displayModalForm = function () {
- updateModalBody();
- modalObj.show();
- };
-
- /**
- * Initialise method for quiz dashboard rendering.
- *
- * @param {int} context The context id for the dashboard.
- * @param {function} processDashboard The callback function to process the dashboard.
- *
- */
- FormModal.init = function (context, processDashboard) {
- contextid = context;
- callback = processDashboard;
- createModal();
-
- let createBroadcastButton = document.getElementById('local-assessfreq-find-quiz');
- createBroadcastButton.addEventListener('click', displayModalForm);
- };
-
- return FormModal;
- }
-);
diff --git a/amd/src/modal_large.js b/amd/src/modal_large.js
index f00fc33e..16422da1 100644
--- a/amd/src/modal_large.js
+++ b/amd/src/modal_large.js
@@ -16,22 +16,24 @@
/**
* Javascript for large modal .
*
+ * @module local_assessfreq/modal_large
+ * @package
* @copyright 2020 Matt Porritt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_registry'],
- function ($, Notification, CustomEvents, Modal, ModalRegistry) {
+ function($, Notification, CustomEvents, Modal, ModalRegistry) {
- var registered = false;
+ let registered = false;
/**
* Constructor for the Modal.
*
* @param {object} root The root jQuery element for the modal
*/
- var ModalLarge = function (root) {
+ let ModalLarge = function(root) {
Modal.call(this, root);
};
diff --git a/amd/src/override_modal.js b/amd/src/override_modal.js
index 23be45b4..f9747f3a 100644
--- a/amd/src/override_modal.js
+++ b/amd/src/override_modal.js
@@ -16,24 +16,25 @@
/**
* Javascript for report card display and processing.
*
+ * @package
* @copyright 2020 Matt Porritt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
- ['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/fragment', 'core/ajax', 'core/notification'],
- function ($,Str, Modal, ModalEvents, Fragment, Ajax, Notification) {
+ ['jquery', 'core/str', 'core/modal', 'core/modal_factory', 'core/modal_events', 'core/fragment', 'core/ajax'],
+ function($, Str, Modal, ModalFactory, ModalEvents, Fragment, Ajax) {
/**
* Module level variables.
*/
- var OverrideModal = {};
- var contextid;
- var modalObj;
- var callback;
- var quizid;
- var userid;
- var hoursFilter;
+ let OverrideModal = {};
+ let contextid;
+ let activitytype;
+ let modalObj;
+ let activityid;
+ let userid;
+ let tableHandler;
const spinner = '
'
+ ''
@@ -44,56 +45,51 @@ define(
*
* @private
*/
- const createModal = function () {
- Str.get_string('loading', 'local_assessfreq').then((title) => {
+ const createModal = function() {
+ Str.get_string('loading').then((title) => {
// Create the Modal.
Modal.create({
- type: Modal.types.DEFAULT,
+ type: ModalFactory.types.DEFAULT,
title: title,
body: spinner,
large: true
- })
- .then((modal) => {
- modalObj = modal;
- // Explicitly handle form click events.
- modalObj.getRoot().on('click', '#id_submitbutton', processModalForm);
- modalObj.getRoot().on('click', '#id_cancel', function (e) {
- e.preventDefault();
- modalObj.setBody(spinner);
- modalObj.hide();
+ }).then((modal) => {
+ modalObj = modal;
+ // Explicitly handle form click events.
+ modalObj.getRoot().on('click', '#id_submitbutton', processModalForm);
+ modalObj.getRoot().on('click', '#id_cancel', function(e) {
+ e.preventDefault();
+ modalObj.setBody(spinner);
+ modalObj.hide();
+ });
});
- });
- return;
- }).catch(() => {
- Notification.exception(new Error('Failed to load string: loading'));
});
};
/**
* Updates the body of the modal window.
*
- * @param {int} quiz The quiz id.
- * @param {int} user The user id.
- * @param {object} formdata The form data.
+ * @param {Integer} activity
+ * @param {Integer} user
+ * @param {Object} formdata
+ * @private
*/
- const updateModalBody = function (quiz, user, formdata) {
+ const updateModalBody = function(activity, user, formdata) {
if (typeof formdata === "undefined") {
formdata = {};
}
let params = {
'jsonformdata': JSON.stringify(formdata),
- 'quizid': quiz,
+ 'activitytype': activitytype,
+ 'activityid': activity,
'userid': user
};
modalObj.setBody(spinner);
- Str.get_string('useroverride', 'local_assessfreq').then((title) => {
+ Str.get_string('modal:useroverride', 'local_assessfreq').then((title) => {
modalObj.setTitle(title);
modalObj.setBody(Fragment.loadFragment('local_assessfreq', 'new_override_form', contextid, params));
- return;
- }).catch(() => {
- Notification.exception(new Error('Failed to load string: useroverride'));
});
};
@@ -112,7 +108,7 @@ define(
// Handle invalid form fields for better UX.
// I hate that I had to use JQuery for this.
- var invalid = $.merge(
+ let invalid = $.merge(
modalObj.getRoot().find('[aria-invalid="true"]'),
modalObj.getRoot().find('.error')
);
@@ -127,49 +123,44 @@ define(
methodname: 'local_assessfreq_process_override_form',
args: {
'jsonformdata': formjson,
- 'quizid': quizid
+ 'activityid': activityid,
+ 'activitytype': activitytype,
},
}])[0].done(() => {
// For submission succeeded.
modalObj.setBody(spinner);
modalObj.hide();
- if (hoursFilter) {
- callback(quizid, hoursFilter);
- } else {
- callback(quizid);
+ if (tableHandler !== undefined) {
+ tableHandler.getTable();
}
}).fail(() => {
// Form submission failed server side, redisplay with errors.
- updateModalBody(quizid, userid, overrideform);
+ updateModalBody(activityid, userid, overrideform);
});
}
/**
* Display the Modal form.
- *
- * @param {int} quiz The quiz id.
- * @param {int} user The user id.
- * @param {int} hours The hours to filter the quiz by.
+ * @param {Integer} activity
+ * @param {Integer} user
*/
- OverrideModal.displayModalForm = function (quiz, user, hours = null) {
- quizid = quiz;
+ OverrideModal.displayModalForm = function(activity, user) {
+ activityid = activity;
userid = user;
- hoursFilter = hours;
- updateModalBody(quiz, user);
+ updateModalBody(activityid, user);
modalObj.show();
};
/**
- * Initialise method for quiz dashboard rendering.
- *
- * @param {int} context The context id for the dashboard.
- * @param {function} callbackFunction The callback function to call after the modal is closed.
- * @param {int} hours The hours to filter the quiz by.
+ * Initialise method for dashboard rendering.
+ * @param {Integer} context
+ * @param {String} module
+ * @param {TableHandler} tablehandler If defined will trigger a table refresh on form save.
*/
- OverrideModal.init = function (context, callbackFunction, hours = null) {
+ OverrideModal.init = function(context, module, tablehandler = undefined) {
+ activitytype = module;
contextid = context;
- callback = callbackFunction;
- hoursFilter = hours;
+ tableHandler = tablehandler;
createModal();
};
diff --git a/amd/src/student_search.js b/amd/src/student_search.js
deleted file mode 100644
index 6bbe09a3..00000000
--- a/amd/src/student_search.js
+++ /dev/null
@@ -1,191 +0,0 @@
-// 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 .
-
-/**
- * Javascript for student search display and processing.
- *
- * @module local_assessfreq/student_search
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-import $ from 'jquery';
-import Notification from 'core/notification';
-import OverrideModal from 'local_assessfreq/override_modal';
-import * as TableHandler from 'local_assessfreq/table_handler';
-import * as UserPreference from 'local_assessfreq/user_preferences';
-
-/**
- * Module level variables.
- */
-var contextid;
-var hoursAhead = 4;
-var hoursBehind = 1;
-var refreshPeriod = 60;
-var counterid;
-
-/**
- * Function for refreshing the counter.
- *
- * @param {boolean} reset the current count process.
- */
-const refreshCounter = (reset = true) => {
- let progressElement = document.getElementById('local-assessfreq-period-progress');
-
- // Reset the current count process.
- if (reset === true) {
- clearInterval(counterid);
- counterid = null;
- progressElement.setAttribute('style', 'width: 100%');
- progressElement.setAttribute('aria-valuenow', 100);
- }
-
- // Exit early if there is already a counter running.
- if (counterid) {
- return;
- }
-
- counterid = setInterval(() => {
- let progressWidthAria = progressElement.getAttribute('aria-valuenow');
- const progressStep = 100 / refreshPeriod;
-
- if ((progressWidthAria - progressStep) > 0) {
- progressElement.setAttribute('style', 'width: ' + (progressWidthAria - progressStep) + '%');
- progressElement.setAttribute('aria-valuenow', (progressWidthAria - progressStep));
- } else {
- clearInterval(counterid);
- counterid = null;
- progressElement.setAttribute('style', 'width: 100%');
- progressElement.setAttribute('aria-valuenow', 100);
- TableHandler.getTable(0, [hoursAhead, hoursBehind], null);
- refreshCounter();
- }
- }, (1000));
-};
-
-/**
- * Process the hours ahead event from the student table.
- *
- * @param {Event} event The triggered event for the element.
- */
-const tableSearchAheadSet = (event) => {
- event.preventDefault();
- if (event.target.tagName.toLowerCase() === 'a') {
- let hours = event.target.dataset.metric;
- UserPreference.setUserPreference('local_assessfreq_student_search_table_hoursahead_preference', hours)
- .then(() => {
- hoursAhead = hours;
- TableHandler.getTable(0, [hoursAhead, hoursBehind], null); // Reload the table. // Reload the table.
- })
- .fail(() => {
- Notification.exception(new Error('Failed to update user preference: hours ahead'));
- });
- }
-};
-
-/**
- * Process the hours behind event from the student table.
- *
- * @param {Event} event The triggered event for the element.
- */
-const tableSearchBehindSet = (event) => {
- event.preventDefault();
- if (event.target.tagName.toLowerCase() === 'a') {
- let hours = event.target.dataset.metric;
- UserPreference.setUserPreference('local_assessfreq_student_search_table_hoursbehind_preference', hours)
- .then(() => {
- hoursBehind = hours;
- TableHandler.getTable(0, [hoursAhead, hoursBehind], null); // Reload the table. // Reload the table.
- })
- .fail(() => {
- Notification.exception(new Error('Failed to update user preference: hours behind'));
- });
- }
-};
-
-/**
- * Handle processing of refresh and period button actions.
- *
- * @param {Event} event The triggered event for the element.
- */
-const refreshAction = (event) => {
- event.preventDefault();
- var element = event.target;
-
- if (element.closest('button') !== null && element.closest('button').id === 'local-assessfreq-refresh-quiz-dashboard') {
- refreshCounter(true);
- TableHandler.getTable(0, [hoursAhead, hoursBehind], null);
- } else if (element.tagName.toLowerCase() === 'a') {
- refreshPeriod = element.dataset.period;
- refreshCounter(true);
- UserPreference.setUserPreference('local_assessfreq_quiz_refresh_preference', refreshPeriod);
- }
-};
-
-/**
- * Initialise method for student search.
- *
- * @param {integer} context The current context id.
- */
-export const init = (context) => {
- contextid = context;
- TableHandler.init(
- 0,
- contextid,
- 'local-assessfreq-student-search-table',
- 'local-assessfreq-student-search',
- 'get_student_search_table',
- 'local_assessfreq_student_search_table_rows_preference',
- 'local-assessfreq-quiz-student-table-search',
- 'local_assessfreq_student_search_table',
- 'local_assessfreq_set_table_preference'
- );
-
- // Add required initial event listeners.
- let tableSearchInputElement = document.getElementById('local-assessfreq-quiz-student-table-search');
- let tableSearchResetElement = document.getElementById('local-assessfreq-quiz-student-table-search-reset');
- let tableSearchRowsElement = document.getElementById('local-assessfreq-quiz-student-table-rows');
- let tableSearchAheadElement = document.getElementById('local-assessfreq-quiz-student-table-hoursahead');
- let tableSearchBehindElement = document.getElementById('local-assessfreq-quiz-student-table-hoursbehind');
- let refreshElement = document.getElementById('local-assessfreq-period-container');
-
- tableSearchInputElement.addEventListener('keyup', TableHandler.tableSearch);
- tableSearchInputElement.addEventListener('paste', TableHandler.tableSearch);
- tableSearchResetElement.addEventListener('click', TableHandler.tableSearchReset);
- tableSearchRowsElement.addEventListener('click', TableHandler.tableSearchRowSet);
- tableSearchAheadElement.addEventListener('click', tableSearchAheadSet);
- tableSearchBehindElement.addEventListener('click', tableSearchBehindSet);
- refreshElement.addEventListener('click', refreshAction);
-
- $.when(
- UserPreference.getUserPreference('local_assessfreq_student_search_table_hoursahead_preference')
- .then((response) => {
- hoursAhead = response.preferences[0].value ? response.preferences[0].value : 4;
- })
- .fail(() => {
- Notification.exception(new Error('Failed to get use preference: hoursahead'));
- }),
- UserPreference.getUserPreference('local_assessfreq_student_search_table_hoursbehind_preference')
- .then((response) => {
- hoursBehind = response.preferences[0].value ? response.preferences[0].value : 1;
- })
- .fail(() => {
- Notification.exception(new Error('Failed to get use preference: hoursahead'));
- })
- ).done(function () {
- TableHandler.getTable(0, [hoursAhead, hoursBehind], null);
- OverrideModal.init(context, TableHandler.getTable, [hoursAhead, hoursBehind]);
- });
-};
diff --git a/amd/src/summary_participants.js b/amd/src/summary_participants.js
deleted file mode 100644
index 8d036b68..00000000
--- a/amd/src/summary_participants.js
+++ /dev/null
@@ -1,75 +0,0 @@
-// 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 .
-
-/**
- * Javascript for summary participants graph.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define(
- ['core/fragment', 'core/templates', 'core/str', 'core/notification'],
- function (Fragment, Templates, Str, Notification) {
-
- /**
- * Module level variables.
- */
- var Summary = {};
-
- Summary.chart = function (assessids, contextid) {
- assessids.forEach((assessid) => {
- let chartElement = document.getElementById(assessid + '-summary-graph');
- let params = {'data': JSON.stringify({'quiz' : assessid, 'call': 'participant_summary'})};
-
- Fragment.loadFragment('local_assessfreq', 'get_quiz_chart', contextid, params)
- .done((response) => {
- let resObj = JSON.parse(response);
- if (resObj.hasdata == true) {
- let legend = {position: 'left'};
- let context = {
- 'withtable' : false,
- 'chartdata' : JSON.stringify(resObj.chart),
- 'aspect' : false,
- 'legend' : JSON.stringify(legend)
- };
- Templates.render('local_assessfreq/chart', context).done((html, js) => {
- // Load card body.
- Templates.replaceNodeContents(chartElement, html, js);
- }).fail(() => {
- Notification.exception(new Error('Failed to load chart template.'));
- return;
- });
- return;
- } else {
- Str.get_string('nodata', 'local_assessfreq').then((str) => {
- const noDatastr = document.createElement('h3');
- noDatastr.innerHTML = str;
- chartElement.innerHTML = noDatastr.outerHTML;
- return;
- }).catch(() => {
- Notification.exception(new Error('Failed to load string: nodata'));
- });
- }
- }).fail(() => {
- Notification.exception(new Error('Failed to load card.'));
- return;
- });
- });
- };
-
- return Summary;
- }
-);
diff --git a/amd/src/table_handler.js b/amd/src/table_handler.js
index 86a144c0..8005cd32 100644
--- a/amd/src/table_handler.js
+++ b/amd/src/table_handler.js
@@ -17,6 +17,7 @@
* Table handler JS module.
*
* @module local_assessfreq/table_handler
+ * @package
* @copyright 2020 Guillermo Gomez
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -29,330 +30,316 @@ import * as Debouncer from 'local_assessfreq/debouncer';
import OverrideModal from 'local_assessfreq/override_modal';
import * as UserPreference from 'local_assessfreq/user_preferences';
-/**
- * Module level variables.
- */
-let cardElement;
-let contextId;
-let elementId;
-let fragmentValue;
-let hoursFilter;
-let quizId = 0;
-let overridden = false;
-let rowPreference;
-let sortValue;
-let searchElement;
-
-/**
- * Table id variable.
- *
- * @type {string}
- */
-let id;
-
-/**
- * Table method name variable.
- *
- * @type {string}
- */
-let methodName;
-
-/**
- * Display the table that contains all the students in the exam as well as their attempts.
- *
- * @param {int} quiz The Quiz Id.
- * @param {array|null} hours Array with hour ahead or behind preference.
- * @param {string|null} sortValueTable Sort preference.
- * @param {int|string|null} page Page number.
- */
-export const getTable = (quiz, hours = null, sortValueTable = null, page) => {
- if (typeof page === "undefined" || overridden === true) {
- page = 0;
- }
-
- overridden = false;
-
- let search = document.getElementById(searchElement).value.trim();
- let tableElement = document.getElementById(elementId);
- let spinner = tableElement.getElementsByClassName('overlay-icon-container')[0];
- let tableBody = tableElement.getElementsByClassName('table-body')[0];
- let values = {'search': search, 'page': page};
-
- // Add values to Object depending on dashboard type.
- if (quiz > 0) {
- quizId = quiz;
- values.quiz = quizId;
- }
- if (hours) {
- hoursFilter = hours;
- values.hoursahead = hoursFilter[0];
- values.hoursbehind = hoursFilter[1];
- }
- if (sortValueTable) {
- sortValue = sortValueTable;
- let sortArray = sortValue.split('_');
- let sortOn = sortArray[0];
- let direction = sortArray[1];
- values.sorton = sortOn;
- values.direction = direction;
- }
-
- let params = {'data': JSON.stringify(values)};
-
- spinner.classList.remove('hide'); // Show spinner if not already shown.
- Fragment.loadFragment('local_assessfreq', fragmentValue, contextId, params)
- .done((response, js) => {
- tableBody.innerHTML = response;
- if (js) {
- Templates.runTemplateJS(js); // Magic call the initialises JS from template included in response template HTML.
- }
- spinner.classList.add('hide');
- tableEventListeners(); // Re-add table event listeners.
-
- }).fail(() => {
- Notification.exception(new Error('Failed to update table.'));
- });
-};
-
-/**
- * This stops the ajax method that updates the table from being updated
- * while the user is still checking options.
- *
- */
-const debounceTable = Debouncer.debouncer(() => {
- getTable(quizId, hoursFilter, sortValue);
-}, 750);
-
-/**
- * Process the sort click events from the student table.
- *
- * @param {Event} event The triggered event for the element.
- */
-const tableSort = (event) => {
- event.preventDefault();
-
- let sortArray = {};
- const linkUrl = new URL(event.target.closest('a').href);
- const targetSortBy = linkUrl.searchParams.get('tsort');
- let targetSortOrder = linkUrl.searchParams.get('tdir');
-
- // We want to flip the clicked column.
- if (targetSortOrder === '') {
- targetSortOrder = "4";
+export default class TableHandler {
+
+ constructor(activity,
+ context,
+ tableElementId,
+ tableFragmentComponent,
+ tableFragmentValue,
+ tableRowPreference,
+ tableSortPreference,
+ tableSearchElement,
+ tableId = null,
+ tableMethodName = 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 = false;
}
- sortArray[targetSortBy] = targetSortOrder;
+ /**
+ * Display the table that contains all the students in the exam as well as their attempts.
+ *
+ * @param {int|string|null} page Page number.
+ */
+ getTable = (page = 0) => {
- // Set option via ajax.
- Ajax.call([{
- methodname: methodName,
- args: {
- tableid: id,
- preference: 'sortby',
- values: JSON.stringify(sortArray)
- },
- }])[0].then(() => {
- getTable(quizId, hoursFilter, sortValue); // Reload the table.
- });
+ globalThis.reports++;
-};
+ this.overridden = false;
-/**
- * Process the sort click events from the student table.
- *
- * @param {Event} event The triggered event for the element.
- */
-const tableHide = (event) => {
- event.preventDefault();
-
- let hideArray = {};
- const linkUrl = new URL(event.target.closest('a').href);
- const tableElement = document.getElementById(elementId);
- const links = tableElement.querySelectorAll('a');
- let targetAction;
- let targetColumn;
- let action;
- let column;
-
- if (linkUrl.search.indexOf('thide') !== -1) {
- targetAction = 'hide';
- targetColumn = linkUrl.searchParams.get('thide');
- } else {
- targetAction = 'show';
- targetColumn = linkUrl.searchParams.get('tshow');
- }
+ let search = document.getElementById(this.searchElement).value.trim();
+ let tableElement = document.getElementById(this.elementId);
+ let spinner = tableElement.getElementsByClassName('overlay-icon-container')[0];
+ let tableBody = tableElement.getElementsByClassName('table-body')[0];
+ let values = {'search': search, 'page': page};
- for (let i = 0; i < links.length; i++) {
- let hideLinkUrl = new URL(links[i].href);
- if (hideLinkUrl.search.indexOf('thide') !== -1) {
- action = 'hide';
- column = hideLinkUrl.searchParams.get('thide');
- } else {
- action = 'show';
- column = hideLinkUrl.searchParams.get('tshow');
+ // Add values to Object depending on dashboard type.
+ if (this.activityId > 0) {
+ values.activityid = this.activityId;
}
- if (action === 'show') {
- hideArray[column] = 1;
+ let params = {'data': JSON.stringify(values)};
+
+ spinner.classList.remove('hide'); // Show spinner if not already shown.
+ Fragment.loadFragment(this.fragmentComponent, this.fragmentValue, this.contextId, params)
+ .done((response, js) => {
+ tableBody.innerHTML = response;
+ if (js) {
+ Templates.runTemplateJS(js); // Magic call the initialises JS from template included in response template HTML.
+ }
+ spinner.classList.add('hide');
+ this.tableEventListeners(); // Re-add table event listeners.
+ globalThis.reports--;
+ })
+ .fail(() => {
+ globalThis.reports--;
+ Notification.exception(new Error('Failed to update table.'));
+ });
+ };
+
+ /**
+ * This stops the ajax method that updates the table from being updated
+ * while the user is still checking options.
+ *
+ */
+ debounceTable = Debouncer.debouncer(() => {
+ this.getTable();
+ }, 750);
+
+ /**
+ * Process the sort click events from the student table.
+ *
+ * @param {Event} event The triggered event for the element.
+ */
+ tableSort = (event) => {
+ event.preventDefault();
+
+ let sortArray = {};
+ const linkUrl = new URL(event.target.closest('a').href);
+ const targetSortBy = linkUrl.searchParams.get('tsort');
+ let targetSortOrder = linkUrl.searchParams.get('tdir');
+
+ // We want to flip the clicked column.
+ if (targetSortOrder === '') {
+ targetSortOrder = "4";
}
- }
-
- hideArray[targetColumn] = (targetAction === 'hide') ? 1 : 0; // We want to flip the clicked column.
- // Set option via ajax.
- Ajax.call([{
- methodname: methodName,
- args: {
- tableid: id,
- preference: 'collapse',
- values: JSON.stringify(hideArray)
- },
- }])[0].then(() => {
- getTable(quizId, hoursFilter, sortValue); // Reload the table.
- });
+ sortArray[targetSortBy] = targetSortOrder;
+
+ // Set option via ajax.
+ // eslint-disable-next-line promise/catch-or-return
+ Ajax.call([{
+ methodname: this.methodName,
+ args: {
+ tableid: this.id,
+ preference: 'sortby',
+ values: JSON.stringify(sortArray)
+ },
+ // eslint-disable-next-line promise/always-return
+ }])[0].then(() => {
+ this.getTable(); // Reload the table.
+ });
-};
+ };
-/**
- * Process the reset click event from the table.
- *
- * @param {Event} event The triggered event for the element.
- */
-const tableReset = (event) => {
- event.preventDefault();
-
- // Set option via ajax.
- Ajax.call([{
- methodname: methodName,
- args: {
- tableid: id,
- preference: 'reset',
- values: JSON.stringify({})
- },
- }])[0].then(() => {
- getTable(quizId, hoursFilter, sortValue); // Reload the table.
- });
-
-};
+ /**
+ * Process the sort click events from the student table.
+ *
+ * @param {Event} event The triggered event for the element.
+ */
+ tableHide = (event) => {
+ event.preventDefault();
-/**
- * Process the search events from the student table.
- *
- * @param {Event} event The triggered event for the element.
- *
- */
-export const tableSearch = (event) => {
- if (event.key === 'Meta' || event.ctrlKey) {
- return false;
- }
+ let hideArray = {};
+ const linkUrl = new URL(event.target.closest('a').href);
+ const tableElement = document.getElementById(this.elementId);
+ const links = tableElement.querySelectorAll('a');
+ let targetAction;
+ let targetColumn;
+ let action;
+ let column;
+
+ if (linkUrl.search.indexOf('thide') !== -1) {
+ targetAction = 'hide';
+ targetColumn = linkUrl.searchParams.get('thide');
+ } else {
+ targetAction = 'show';
+ targetColumn = linkUrl.searchParams.get('tshow');
+ }
- if (event.target.value.length === 0 || event.target.value.length > 2) {
- debounceTable();
- }
-};
+ for (let i = 0; i < links.length; i++) {
+ let hideLinkUrl = new URL(links[i].href);
+ if (hideLinkUrl.search.indexOf('thide') !== -1) {
+ action = 'hide';
+ column = hideLinkUrl.searchParams.get('thide');
+ } else {
+ action = 'show';
+ column = hideLinkUrl.searchParams.get('tshow');
+ }
-/**
- * Process the search reset click event from the student table.
- *
- */
-export const tableSearchReset = () => {
- let tableSearchInputElement = document.getElementById(searchElement);
- tableSearchInputElement.value = '';
- tableSearchInputElement.focus();
- getTable(quizId, hoursFilter, sortValue);
-};
+ if (action === 'show') {
+ hideArray[column] = 1;
+ }
+ }
-/**
- * Process the row set event from the student table.
- *
- * @param {Event} event The triggered event for the element.
- */
-export const tableSearchRowSet = (event) => {
- event.preventDefault();
- if (event.target.tagName.toLowerCase() === 'a') {
- let rows = event.target.dataset.metric;
- UserPreference.setUserPreference(rowPreference, rows)
- .then(() => {
- getTable(quizId, hoursFilter, sortValue); // Reload the table.
- })
- .fail(() => {
- Notification.exception(new Error('Failed to update user preference: rows'));
- });
- }
-};
+ hideArray[targetColumn] = (targetAction === 'hide') ? 1 : 0; // We want to flip the clicked column.
+
+ // Set option via ajax.
+ // eslint-disable-next-line promise/catch-or-return
+ Ajax.call([{
+ methodname: this.methodName,
+ args: {
+ tableid: this.id,
+ preference: 'collapse',
+ values: JSON.stringify(hideArray)
+ },
+ // eslint-disable-next-line promise/always-return
+ }])[0].then(() => {
+ this.getTable(); // Reload the table.
+ });
-/**
- * Process the nav event from the student table.
- *
- * @param {Event} event The triggered event for the element.
- */
-const tableNav = (event) => {
- event.preventDefault();
+ };
+
+ /**
+ * Process the reset click event from the table.
+ *
+ * @param {Event} event The triggered event for the element.
+ */
+ tableReset = (event) => {
+ event.preventDefault();
+
+ // Set option via ajax.
+ // eslint-disable-next-line promise/catch-or-return
+ Ajax.call([{
+ methodname: this.methodName,
+ args: {
+ tableid: this.id,
+ preference: 'reset',
+ values: JSON.stringify({})
+ },
+ // eslint-disable-next-line promise/always-return
+ }])[0].then(() => {
+ this.getTable(); // Reload the table.
+ });
- const linkUrl = new URL(event.target.closest('a').href);
- const page = linkUrl.searchParams.get('page');
+ };
+
+ /**
+ * Process the search events from the student table.
+ *
+ * @param {Event} event
+ * @return {Boolean}
+ */
+ tableSearch = (event) => {
+ if (event.key === 'Meta' || event.ctrlKey) {
+ return false;
+ }
- if (page) {
- getTable(quizId, hoursFilter, sortValue, page);
- }
-};
+ if (event.target.value.length === 0 || event.target.value.length > 2) {
+ this.debounceTable();
+ }
+ return true;
+ };
+
+ /**
+ * Process the search reset click event from the student table.
+ *
+ */
+ tableSearchReset = () => {
+ let tableSearchInputElement = document.getElementById(this.searchElement);
+ tableSearchInputElement.value = '';
+ tableSearchInputElement.focus();
+ this.getTable();
+ };
+
+ /**
+ * Process the row set event from the student table.
+ *
+ * @param {Event} event The triggered event for the element.
+ */
+ tableSearchRowSet = (event) => {
+ event.preventDefault();
+ if (event.target.tagName.toLowerCase() === 'a') {
+ let rows = event.target.dataset.metric;
+ UserPreference.setUserPreference(this.rowPreference, rows)
+ // eslint-disable-next-line promise/always-return
+ .then(() => {
+ this.getTable(); // Reload the table.
+ })
+ .fail(() => {
+ Notification.exception(new Error('Failed to update user preference: rows'));
+ });
+ }
+ };
-/**
- * Get and process the selected assessment metric from the dropdown for the heatmap display,
- * and update the corresponding user preference.
- *
- * @param {Event} event The triggered event for the element.
- */
-export const tableSortButtonAction = (event) => {
- event.preventDefault();
- var element = event.target;
+ /**
+ * Process the nav event from the student table.
+ *
+ * @param {Event} event The triggered event for the element.
+ */
+ tableNav = (event) => {
+ event.preventDefault();
- if (element.tagName.toLowerCase() === 'a' && element.dataset.sort !== sortValue) {
- sortValue = element.dataset.sort;
+ const linkUrl = new URL(event.target.closest('a').href);
+ const page = linkUrl.searchParams.get('page');
- let links = element.parentNode.getElementsByTagName('a');
- for (let i = 0; i < links.length; i++) {
- links[i].classList.remove('active');
+ if (page) {
+ this.getTable(page);
}
+ };
+
+ /**
+ * Get and process the selected assessment metric from the dropdown for the heatmap display,
+ * and update the corresponding user preference.
+ *
+ * @param {Event} event The triggered event for the element.
+ */
+ tableSortButtonAction = (event) => {
+ event.preventDefault();
+ var element = event.target;
+
+ if (element.tagName.toLowerCase() === 'a' && element.dataset.sort !== this.sortValue) {
+ this.sortValue = element.dataset.sort;
+
+ let links = element.parentNode.getElementsByTagName('a');
+ for (let i = 0; i < links.length; i++) {
+ links[i].classList.remove('active');
+ }
- element.classList.add('active');
+ element.classList.add('active');
- // Save selection as a user preference.
- UserPreference.setUserPreference('local_assessfreq_quiz_table_inprogress_sort_preference', sortValue);
+ // Save selection as a user preference.
+ UserPreference.setUserPreference(this.sortPreference, this.sortValue);
- debounceTable(); // Call function to update table.
- }
-};
+ this.debounceTable(); // Call function to update table.
+ }
+ };
-/**
- * Re-add event listeners when the student table is updated.
- */
-const tableEventListeners = () => {
- const tableElement = document.getElementById(elementId);
- let tableNavElement;
- if (cardElement) {
- const tableCardElement = document.getElementById(cardElement);
+ /**
+ * Re-add event listeners when the student table is updated.
+ */
+ tableEventListeners = () => {
+ const tableElement = document.getElementById(this.elementId);
const links = tableElement.querySelectorAll('a');
const resetLink = tableElement.getElementsByClassName('resettable');
const overrideLinks = tableElement.getElementsByClassName('action-icon override');
const disabledLinks = tableElement.getElementsByClassName('action-icon disabled');
- tableNavElement = tableCardElement.querySelectorAll('nav'); // There are two nav paging elements per table.
+ const tableNavElement = tableElement.querySelectorAll('nav'); // There are two nav paging elements per table.
for (let i = 0; i < links.length; i++) {
let linkUrl = new URL(links[i].href);
if (linkUrl.search.indexOf('thide') !== -1 || linkUrl.search.indexOf('tshow') !== -1) {
- links[i].addEventListener('click', tableHide);
+ links[i].addEventListener('click', this.tableHide);
} else if (linkUrl.search.indexOf('tsort') !== -1) {
- links[i].addEventListener('click', tableSort);
+ links[i].addEventListener('click', this.tableSort);
}
}
if (resetLink.length > 0) {
- resetLink[0].addEventListener('click', tableReset);
+ resetLink[0].addEventListener('click', this.tableReset);
}
for (let i = 0; i < overrideLinks.length; i++) {
- overrideLinks[i].addEventListener('click', triggerOverrideModal);
+ overrideLinks[i].addEventListener('click', this.triggerOverrideModal);
}
for (let i = 0; i < disabledLinks.length; i++) {
@@ -360,61 +347,26 @@ const tableEventListeners = () => {
event.preventDefault();
});
}
- } else {
- tableNavElement = tableElement.querySelectorAll('nav');
- }
-
- tableNavElement.forEach((navElement) => {
- navElement.addEventListener('click', tableNav);
- });
-};
-
-/**
- * Trigger the override modal form. Thin wrapper to add extra data to click event.
- *
- * @param {Event} event The triggered event for the element.
- */
-const triggerOverrideModal = (event) => {
- event.preventDefault();
- let userid = event.target.closest('a').id.substring(25);
- if (userid.includes('-')) {
- let elements = userid.split('-');
- quizId = elements.pop();
- userid = elements.pop();
- }
- OverrideModal.displayModalForm(quizId, userid, hoursFilter);
-};
+ tableNavElement.forEach((navElement) => {
+ navElement.addEventListener('click', this.tableNav);
+ });
+ };
+
+ /**
+ * Trigger the override modal form. Thin wrapper to add extra data to click event.
+ *
+ * @param {Event} event The triggered event for the element.
+ */
+ 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();
+ }
-/**
- * Initialise method for table handler.
- *
- * @param {int} quiz The quiz id.
- * @param {int} context The context id.
- * @param {string} tableCardElement The table card element.
- * @param {string} tableElementId The table element id.
- * @param {string} tableFragmentValue The table fragment value.
- * @param {string} tableRowPreference The table row preference.
- * @param {string} tableSearchElement The table search element.
- * @param {string|null} tableId The table id.
- * @param {string|null} tableMethodName The table method name.
- */
-export const init = (quiz,
- context,
- tableCardElement,
- tableElementId,
- tableFragmentValue,
- tableRowPreference,
- tableSearchElement,
- tableId = null,
- tableMethodName = null) => {
- quizId = quiz;
- contextId = context;
- cardElement = tableCardElement;
- elementId = tableElementId;
- fragmentValue = tableFragmentValue;
- rowPreference = tableRowPreference;
- searchElement = tableSearchElement;
- id = tableId;
- methodName = tableMethodName;
- };
+ OverrideModal.displayModalForm(this.activityId, userid, this.hoursFilter);
+ };
+}
diff --git a/amd/src/user_preferences.js b/amd/src/user_preferences.js
index 7d18bd28..544408a0 100644
--- a/amd/src/user_preferences.js
+++ b/amd/src/user_preferences.js
@@ -17,6 +17,7 @@
* User preferences JS module.
*
* @module local_assessfreq/user_preferences
+ * @package
* @copyright 2020 Guillermo Gomez
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
diff --git a/amd/src/zoom_modal.js b/amd/src/zoom_modal.js
deleted file mode 100644
index c1ea2366..00000000
--- a/amd/src/zoom_modal.js
+++ /dev/null
@@ -1,111 +0,0 @@
-// 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 .
-
-/**
- * Javascript for report card display and processing.
- *
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define(
- ['core/str', 'core/modal', 'core/fragment', 'core/ajax', 'core/templates', 'local_assessfreq/modal_large',
- 'core/notification'],
- function (Str, Modal, Fragment, Ajax, Templates, ModalLarge, Notification) {
-
- /**
- * Module level variables.
- */
- var ZoomModal = {};
- var contextid;
- var modalObj;
- const spinner = '
'
- + ''
- + '
';
-
- /**
- * Provides zoom functionality for card graphs.
- *
- * @param {object} event The event object.
- * @param {object} params The parameters for the fragment call.
- * @param {string} method The method to call in the fragment.
- */
- ZoomModal.zoomGraph = function (event, params, method) {
- let title = event.target.parentElement.dataset.title;
-
- Fragment.loadFragment('local_assessfreq', method, contextid, params)
- .done((response) => {
- let resObj = JSON.parse(response);
- if (resObj.hasdata == true) {
- var context = { 'withtable' : false, 'chartdata' : JSON.stringify(resObj.chart), aspect: false};
- modalObj.setTitle(title);
- modalObj.setBody(Templates.render('local_assessfreq/chart', context));
- modalObj.show();
- return;
- } else {
- Str.get_string('nodata', 'local_assessfreq').then((str) => {
- const noDatastr = document.createElement('h3');
- noDatastr.innerHTML = str;
- modalObj.setTitle(title);
- modalObj.setBody(noDatastr.outerHTML);
- modalObj.show();
- return;
- }).catch(() => {
- Notification.exception(new Error('Failed to load string: nodata'));
- });
- }
- }).fail(() => {
- Notification.exception(new Error('Failed to load zoomed graph'));
- return;
- });
-
- };
-
- /**
- * Create the modal window for graph zooming.
- *
- * @private
- */
- const createModal = function () {
- return new Promise((resolve) => {
- Str.get_string('loading', 'core').then((title) => {
- // Create the Modal.
- Modal.create({
- type: ModalLarge.TYPE,
- title: title,
- body: spinner,
- large: true
- })
- .then((modal) => {
- modalObj = modal;
- resolve();
- });
- }).catch(Notification.exception);
- });
- };
-
- /**
- * Initialise method for quiz dashboard rendering.
- *
- * @param {int} context The context id for the dashboard.
- */
- ZoomModal.init = function (context) {
- contextid = context;
- createModal();
- };
-
- return ZoomModal;
- }
-);
diff --git a/ci.yml b/ci.yml
new file mode 100644
index 00000000..f1044690
--- /dev/null
+++ b/ci.yml
@@ -0,0 +1,13 @@
+# .github/workflows/ci.yml
+name: ci
+
+on: [push, pull_request]
+
+jobs:
+ ci:
+ uses: catalyst/catalyst-moodle-workflows/.github/workflows/ci.yml@main
+ # Required if you plan to publish (uncomment the below)
+ # secrets:
+ # moodle_org_token: ${{ secrets.MOODLE_ORG_TOKEN }}
+ with:
+ disable_phpcpd: true
diff --git a/classes/event/event_processed.php b/classes/event/event_processed.php
index e42cdeab..13f38253 100644
--- a/classes/event/event_processed.php
+++ b/classes/event/event_processed.php
@@ -24,6 +24,8 @@
namespace local_assessfreq\event;
+use core\event\base;
+
/**
* Event class.
*
@@ -31,7 +33,8 @@
* @copyright 2020 Matt Porritt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class event_processed extends \core\event\base {
+class event_processed extends base {
+
/**
* Init method.
*/
@@ -45,7 +48,7 @@ protected function init() {
*
* @return string
*/
- public static function get_name() {
+ public static function get_name() : string {
return get_string('eventeventprocessed', 'local_assessfreq');
}
@@ -54,7 +57,7 @@ public static function get_name() {
*
* @return string
*/
- public function get_description() {
+ public function get_description() : string {
return get_string('eventeven_processed_desc', 'local_assessfreq');
}
}
diff --git a/classes/external.php b/classes/external.php
index feebbc07..3938d2f0 100644
--- a/classes/external.php
+++ b/classes/external.php
@@ -21,9 +21,14 @@
* @copyright 2020 Matt Porritt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
+use core\session\manager;
+use local_assessfreq\source_base;
+
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . "/externallib.php");
+require_once(dirname(__FILE__, 2) . '/lib.php');
/**
* Local assessfreq Web Service.
@@ -36,194 +41,23 @@ class local_assessfreq_external extends external_api {
/**
* Returns description of method parameters.
*
- * @return void
- */
- public static function get_frequency_parameters() {
- return new external_function_parameters([
- 'jsondata' => new external_value(PARAM_RAW, 'The data encoded as a json array'),
- ]);
- }
-
- /**
- * Returns event frequency map for all users in site.
- *
- * @param string $jsondata JSON data.
- * @return string JSON response.
- */
- public static function get_frequency($jsondata) {
- \core\session\manager::write_close(); // Close session early this is a read op.
-
- // Parameter validation.
- self::validate_parameters(
- self::get_frequency_parameters(),
- ['jsondata' => $jsondata]
- );
-
- // Context validation and permission check.
- $context = context_system::instance();
- self::validate_context($context);
- has_capability('moodle/site:config', $context);
-
- // Execute API call.
- $data = json_decode($jsondata, true);
- $frequency = new \local_assessfreq\frequency();
- $freqarr = $frequency->get_frequency_array($data['year'], $data['metric'], $data['modules']);
-
- return json_encode($freqarr);
- }
-
- /**
- * Returns description of method result value
- * @return external_description
- */
- public static function get_frequency_returns() {
- return new external_value(PARAM_RAW, 'Event JSON');
- }
-
- /**
- * Returns description of method parameters.
- *
- * @return void
- */
- public static function get_heat_colors_parameters() {
- return new external_function_parameters([
- // If I had params they'd be here, but I don't, so they're not.
- ]);
- }
-
- /**
- * Returns heat map colors.
- * This method doesn't require login or user session update.
- * It also doesn't need any capability check.
- *
- * @return string JSON response.
- */
- public static function get_heat_colors() {
- \core\session\manager::write_close(); // Close session early this is a read op.
-
- // Execute API call.
- $frequency = new \local_assessfreq\frequency();
- $heatarray = $frequency->get_heat_colors();
-
- return json_encode($heatarray);
- }
-
- /**
- * Returns description of method result value
- * @return external_description
- */
- public static function get_heat_colors_returns() {
- return new external_value(PARAM_RAW, 'Event JSON');
- }
-
- /**
- * Returns description of method parameters.
- *
- * @return void
- */
- public static function get_process_modules_parameters() {
- return new external_function_parameters([
- // If I had params they'd be here, but I don't, so they're not.
- ]);
- }
-
- /**
- * Returns modules enabled for processing along with their module name string.
- *
- * @return string JSON response.
- */
- public static function get_process_modules() {
- \core\session\manager::write_close(); // Close session early this is a read op.
-
- $modulesandstrings = ['number' => get_string('numberevents', 'local_assessfreq')];
-
- // Execute API call.
- $frequency = new \local_assessfreq\frequency();
- $processmodules = $frequency->get_process_modules();
-
- foreach ($processmodules as $module) {
- $modulesandstrings[$module] = get_string('modulename', $module);
- }
-
- return json_encode($modulesandstrings);
- }
-
- /**
- * Returns description of method result value
- * @return external_description
- */
- public static function get_process_modules_returns() {
- return new external_value(PARAM_RAW, 'Event JSON');
- }
-
-
- /**
- * Returns description of method parameters.
- *
- * @return void
- */
- public static function get_day_events_parameters() {
- return new external_function_parameters([
- 'jsondata' => new external_value(PARAM_RAW, 'The data encoded as a json array'),
- ]);
- }
-
- /**
- * Returns event frequency map for all users in site.
- *
- * @param string $jsondata JSON data.
- * @return string JSON response.
- */
- public static function get_day_events($jsondata) {
- \core\session\manager::write_close(); // Close session early this is a read op.
-
- // Parameter validation.
- self::validate_parameters(
- self::get_day_events_parameters(),
- ['jsondata' => $jsondata]
- );
-
- // Context validation and permission check.
- $context = context_system::instance();
- self::validate_context($context);
- has_capability('moodle/site:config', $context);
-
- // Execute API call.
- $data = json_decode($jsondata, true);
- $frequency = new \local_assessfreq\frequency();
- $freqarr = $frequency->get_day_events($data['date'], $data['modules']);
-
- return json_encode($freqarr);
- }
-
- /**
- * Returns description of method result value
- * @return external_description
- */
- public static function get_day_events_returns() {
- return new external_value(PARAM_RAW, 'Event JSON');
- }
-
- /**
- * Returns description of method parameters.
- *
- * @return void
+ * @return external_function_parameters
*/
- public static function get_courses_parameters() {
+ public static function get_courses_parameters() : external_function_parameters {
return new external_function_parameters([
'query' => new external_value(PARAM_TEXT, 'The query to find'),
]);
}
/**
- * Returns courses and quizzes in that course that match search data.
+ * Returns courses that match search data.
*
* @param string $query The search query.
* @return string JSON response.
*/
- public static function get_courses($query) {
- global $DB;
- \core\session\manager::write_close(); // Close session early this is a read op.
+ public static function get_courses(string $query) : string {
+ global $DB, $SITE, $COURSE;
+ manager::write_close(); // Close session early this is a read op.
// Parameter validation.
self::validate_parameters(
@@ -231,23 +65,28 @@ public static function get_courses($query) {
['query' => $query]
);
- // Context validation and permission check.
- $context = context_system::instance();
- self::validate_context($context);
- has_capability('moodle/site:config', $context);
-
// Execute API call.
- $sql = 'SELECT id, fullname FROM {course} WHERE ' . $DB->sql_like('fullname', ':fullname', false) . ' AND id <> 1';
+ $sql = 'SELECT id, fullname, category FROM {course} WHERE ' . $DB->sql_like('fullname', ':fullname', false) . ' AND id <> 1';
$params = ['fullname' => '%' . $DB->sql_like_escape($query) . '%'];
- $courses = $DB->get_records_sql($sql, $params, 0, 11);
+ $courses = $DB->get_records_sql($sql, $params, 0, 30);
$data = [];
+ if (has_capability('local/assessfreq:view', context_system::instance())) {
+ $data[SITEID] = [
+ "id" => $SITE->id,
+ "fullname" => external_format_string($SITE->fullname, true, ["escape" => false])
+ ];
+ }
+ $categories = \core_course_category::make_categories_list();
foreach ($courses as $course) {
- $data[$course->id] = ["id" => $course->id, "fullname" => format_string(
- $course->fullname,
- true,
- ["context" => $context, "escape" => false]
- ), ];
+ $data[$course->id] = [
+ "id" => $course->id,
+ "fullname" => $categories[$course->category] . ' / ' . external_format_string($course->fullname, true, ["escape" => false])
+ ];
+ }
+
+ if (isset($data[$COURSE->id])) {
+ unset($data[$COURSE->id]);
}
return json_encode(array_values($data));
@@ -255,120 +94,80 @@ public static function get_courses($query) {
/**
* Returns description of method result value
- * @return external_description
+ * @return external_value
*/
- public static function get_courses_returns() {
+ public static function get_courses_returns() : external_value {
return new external_value(PARAM_RAW, 'Course result JSON');
}
/**
* Returns description of method parameters.
*
- * @return void
+ * @return external_function_parameters
*/
- public static function get_quizzes_parameters() {
+ public static function get_activities_parameters() : external_function_parameters {
return new external_function_parameters([
- 'query' => new external_value(PARAM_INT, 'The query to find'),
+ 'courseid' => new external_value(PARAM_INT, 'The courseid to find'),
]);
}
/**
- * Returns courses and quizzes in that course that match search data.
+ * Returns activities in the course that match search data.
*
- * @param string $query The search query.
+ * @param $courseid
* @return string JSON response.
*/
- public static function get_quizzes($query) {
+ public static function get_activities($courseid) : string {
global $DB;
- \core\session\manager::write_close(); // Close session early this is a read op.
+ manager::write_close(); // Close session early this is a read op.
// Parameter validation.
self::validate_parameters(
- self::get_quizzes_parameters(),
- ['query' => $query]
+ self::get_activities_parameters(),
+ ['courseid' => $courseid]
);
- // Context validation and permission check.
- $context = context_system::instance();
- self::validate_context($context);
- has_capability('moodle/site:config', $context);
-
// Execute API call.
- $params = ['course' => $query];
- $quizzes = $DB->get_records('quiz', $params, 'name ASC', 'id, name');
+ $modules = $DB->get_records('course_modules', ['course' => $courseid]);
+
+ $sources = get_sources();
$data = [];
- foreach ($quizzes as $quiz) {
- $data[$quiz->id] = ["id" => $quiz->id, "name" => format_string(
- $quiz->name,
- true,
- ["context" => $context, "escape" => false]
- ), ];
+ foreach ($modules as $module) {
+ $modinfo = get_fast_modinfo($courseid);
+ $cm = $modinfo->get_cm($module->id);
+ // Skip over if source is not enabled or if the source doesn't have an activity dashboard.
+ $moduletype = $cm->modname;
+ if (!isset($sources[$moduletype]) || !method_exists($sources[$moduletype], 'get_activity_dashboard')) {
+ continue;
+ }
+
+ $data[$module->id] = [
+ "id" => $module->id,
+ "name" => $cm->get_module_type_name() . " - " . $cm->get_name()
+ ];
}
+ usort($data, fn($a, $b) => $a['name'] <=> $b['name']);
+
return json_encode(array_values($data));
}
/**
* Returns description of method result value
- * @return external_description
+ * @return external_value
*/
- public static function get_quizzes_returns() {
- return new external_value(PARAM_RAW, 'Quiz result JSON');
+ public static function get_activities_returns() : external_value {
+ return new external_value(PARAM_RAW, 'Result JSON');
}
- /**
- * Returns description of method parameters.
- *
- * @return void
- */
- public static function get_quiz_data_parameters() {
- return new external_function_parameters([
- 'quizid' => new external_value(PARAM_INT, 'The quiz id to get data for'),
- ]);
- }
-
- /**
- * Returns quiz data.
- *
- * @param string $quizid The quiz id to get data for.
- * @return string JSON response.
- */
- public static function get_quiz_data($quizid) {
- \core\session\manager::write_close(); // Close session early this is a read op.
-
- // Parameter validation.
- self::validate_parameters(
- self::get_quiz_data_parameters(),
- ['quizid' => $quizid]
- );
-
- // Context validation and permission check.
- $context = context_system::instance();
- self::validate_context($context);
- has_capability('moodle/site:config', $context);
-
- // Execute API call.
- $quiz = new \local_assessfreq\quiz();
- $quizdata = $quiz->get_quiz_data($quizid);
-
- return json_encode($quizdata);
- }
-
- /**
- * Returns description of method result value
- * @return external_description
- */
- public static function get_quiz_data_returns() {
- return new external_value(PARAM_RAW, 'Quiz data result JSON');
- }
/**
* Returns description of method parameters.
*
- * @return void
+ * @return external_function_parameters
*/
- public static function set_table_preference_parameters() {
+ public static function set_table_preference_parameters() : external_function_parameters {
return new external_function_parameters([
'tableid' => new external_value(PARAM_ALPHANUMEXT, 'The table id to set the preference for'),
'preference' => new external_value(PARAM_ALPHAEXT, 'The table preference to set'),
@@ -377,15 +176,15 @@ public static function set_table_preference_parameters() {
}
/**
- * Returns quiz data.
+ * Set table preferences.
*
* @param string $tableid The table id to set the preference for.
* @param string $preference The name of the preference to set.
* @param string $values The values to set for the preference, encoded as JSON.
* @return string JSON response.
*/
- public static function set_table_preference($tableid, $preference, $values) {
- global $SESSION;
+ public static function set_table_preference(string $tableid, string $preference, string $values) : string {
+ global $SESSION, $PAGE;
// Parameter validation.
self::validate_parameters(
@@ -393,23 +192,14 @@ public static function set_table_preference($tableid, $preference, $values) {
['tableid' => $tableid, 'preference' => $preference, 'values' => $values]
);
- // Context validation and permission check.
- $context = context_system::instance();
- self::validate_context($context);
- has_capability('moodle/site:config', $context);
-
// Set up the initial preference template.
- if (isset($SESSION->flextable[$tableid])) {
- $prefs = $SESSION->flextable[$tableid];
- } else {
- $prefs = [
- 'collapse' => [],
- 'sortby' => [],
- 'i_first' => '',
- 'i_last' => '',
- 'textsort' => [],
- ];
- }
+ $prefs = $SESSION->flextable[$tableid] ?? [
+ 'collapse' => [],
+ 'sortby' => [],
+ 'i_first' => '',
+ 'i_last' => '',
+ 'textsort' => [],
+ ];
// Set or reset the preferences.
if ($preference == 'reset') {
@@ -437,38 +227,40 @@ public static function set_table_preference_returns() {
return new external_value(PARAM_ALPHAEXT, 'Name of the updated preference');
}
+
/**
* Returns description of method parameters
*
* @return external_function_parameters
*/
- public static function process_override_form_parameters() {
+ public static function process_override_form_parameters() : external_function_parameters {
return new external_function_parameters(
[
'jsonformdata' => new external_value(PARAM_RAW, 'The data from the create copy form, encoded as a json array'),
- 'quizid' => new external_value(PARAM_INT, 'The quiz id to processs the override for'),
+ 'activitytype' => new external_value(PARAM_ALPHANUMEXT, 'The activity to processs the override for'),
+ 'activityid' => new external_value(PARAM_INT, 'The activity id to processs the override for'),
]
);
}
/**
- * Submit the quiz override form.
+ * Submit the override form.
*
* @param string $jsonformdata The data from the form, encoded as a json array.
- * @param int $quizid The quiz id to add an override for.
- * @throws moodle_exception
+ * @param string $activitytype The activity to add an override for.
+ * @param int $activityid The activity id to add an override for.
* @return string
*/
- public static function process_override_form($jsonformdata, $quizid) {
+ public static function process_override_form(string $jsonformdata, string $activitytype, int $activityid) : string {
global $DB;
// Release session lock.
- \core\session\manager::write_close();
+ manager::write_close();
// We always must pass webservice params through validate_parameters.
$params = self::validate_parameters(
self::process_override_form_parameters(),
- ['jsonformdata' => $jsonformdata, 'quizid' => $quizid]
+ ['jsonformdata' => $jsonformdata, 'activitytype' => $activitytype, 'activityid' => $activityid]
);
$formdata = json_decode($params['jsonformdata']);
@@ -476,56 +268,15 @@ public static function process_override_form($jsonformdata, $quizid) {
$submitteddata = [];
parse_str($formdata, $submitteddata);
- // Check access.
- $quizdata = new \local_assessfreq\quiz();
- $context = $quizdata->get_quiz_context($quizid);
- self::validate_context($context);
- has_capability('mod/quiz:manageoverrides', $context);
-
- // Check if we have an existing override for this user.
- $override = $DB->get_record('quiz_overrides', ['quiz' => $quizid, 'userid' => $submitteddata['userid']]);
-
- // Submit the form data.
- $quiz = $DB->get_record('quiz', ['id' => $quizid], '*', MUST_EXIST);
- $cm = get_course_and_cm_from_cmid($context->instanceid, 'quiz')[1];
- $mform = new \local_assessfreq\form\quiz_override_form($cm, $quiz, $context, $override, $submitteddata);
-
- $mdata = $mform->get_data();
-
- if ($mdata) {
- $params = [
- 'context' => $context,
- 'other' => [
- 'quizid' => $quizid,
- ],
- 'relateduserid' => $mdata->userid,
- ];
- $mdata->quiz = $quizid;
-
- if (!empty($override->id)) {
- $mdata->id = $override->id;
- $DB->update_record('quiz_overrides', $mdata);
-
- // Determine which override updated event to fire.
- $params['objectid'] = $override->id;
- $event = \mod_quiz\event\user_override_updated::create($params);
- // Trigger the override updated event.
- $event->trigger();
- } else {
- unset($mdata->id);
- $mdata->id = $DB->insert_record('quiz_overrides', $mdata);
-
- // Determine which override created event to fire.
- $params['objectid'] = $mdata->id;
- $event = \mod_quiz\event\user_override_created::create($params);
- // Trigger the override created event.
- $event->trigger();
- }
- } else {
- throw new moodle_exception('submitoverridefail', 'local_assessfreq');
+ $processid = 0;
+ $sources = get_sources();
+ $source = $sources[$activitytype];
+ /* @var $source source_base */
+ if (method_exists($source, 'process_override_form')) {
+ $processid = $source->process_override_form($activityid, $submitteddata);
}
- return json_encode(['overrideid' => $mdata->id]);
+ return json_encode(['overrideid' => $processid]);
}
/**
@@ -536,82 +287,4 @@ public static function process_override_form($jsonformdata, $quizid) {
public static function process_override_form_returns() {
return new external_value(PARAM_RAW, 'JSON response.');
}
-
- /**
- * Returns description of method parameters.
- *
- * @return void
- */
- public static function get_system_timezone_parameters() {
- return new external_function_parameters([
- // If I had params they'd be here, but I don't, so they're not.
- ]);
- }
-
- /**
- * Returns system timezone.
- * This method doesn't require login or user session update.
- * It also doesn't need any capability check.
- *
- * @return string Timezone.
- */
- public static function get_system_timezone() {
- \core\session\manager::write_close(); // Close session early this is a read op.
- global $DB;
-
- // Execute API call.
- $timezone = $DB->get_field('config', 'value', ['name' => 'timezone'], MUST_EXIST);
-
- return $timezone;
- }
-
- /**
- * Returns description of method result value.
- *
- * @return external_description
- */
- public static function get_system_timezone_returns() {
- return new external_value(PARAM_TEXT, 'Timezone');
- }
-
- /**
- * Returns description of method parameters.
- *
- * @return void
- */
- public static function get_inprogress_counts_parameters() {
- return new external_function_parameters([
- // If I had params they'd be here, but I don't, so they're not.
- ]);
- }
-
- /**
- * Returns quiz summary data for upcomming and inprogress quizzes.
- *
- * @return string JSON response.
- */
- public static function get_inprogress_counts() {
- \core\session\manager::write_close(); // Close session early this is a read op.
-
- // Context validation and permission check.
- $context = context_system::instance();
- self::validate_context($context);
- has_capability('moodle/site:config', $context);
-
- // Execute API call.
- $quiz = new \local_assessfreq\quiz();
- $now = time();
- $quizdata = $quiz->get_inprogress_counts($now);
-
- return json_encode($quizdata);
- }
-
- /**
- * Returns description of method result value.
- *
- * @return external_description
- */
- public static function get_inprogress_counts_returns() {
- return new external_value(PARAM_RAW, 'JSON quiz count data');
- }
}
diff --git a/classes/form/quiz_search_form.php b/classes/form/quiz_search_form.php
deleted file mode 100644
index 385dc744..00000000
--- a/classes/form/quiz_search_form.php
+++ /dev/null
@@ -1,82 +0,0 @@
-.
-
-/**
- * Form to search for quizzes.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace local_assessfreq\form;
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once("$CFG->libdir/formslib.php");
-
-/**
- * Form to search for quizzes.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class quiz_search_form extends \moodleform {
- /**
- * Build form for the broadcast message.
- *
- * {@inheritDoc}
- * @see \moodleform::definition()
- */
- public function definition() {
- $mform = $this->_form;
- $mform->disable_form_change_checker();
-
- // Form heading.
- $mform->addElement(
- 'html',
- \html_writer::div(get_string('searchquizform', 'local_assessfreq'), 'form-description mb-3')
- );
-
- $courseoptions = [
- 'multiple' => false,
- 'placeholder' => get_string('entercourse', 'local_assessfreq'),
- 'noselectionstring' => get_string('nocourse', 'local_assessfreq'),
- 'ajax' => 'local_assessfreq/course_selector',
- 'casesensitive' => false,
- ];
- $mform->addElement('autocomplete', 'courses', get_string('course', 'local_assessfreq'), [], $courseoptions);
-
- $mform->addElement('hidden', 'coursechoice', '0');
- $mform->setType('coursechoice', PARAM_INT);
-
- $selectoptions = [
- 0 => get_string('selectcourse', 'local_assessfreq'),
- -1 => get_string('loadingquiz', 'local_assessfreq'),
- ];
- $mform->addElement(
- 'select',
- 'quiz',
- get_string('quiz', 'local_assessfreq'),
- $selectoptions
- );
- $mform->disabledIf('quiz', 'coursechoice', 'eq', '0');
-
- $btnstring = get_string('selectquiz', 'local_assessfreq');
- $this->add_action_buttons(true, $btnstring);
- }
-}
diff --git a/classes/form/scheduler.php b/classes/form/scheduler.php
deleted file mode 100644
index fa06bbf5..00000000
--- a/classes/form/scheduler.php
+++ /dev/null
@@ -1,55 +0,0 @@
-.
-
-
-/**
- * Text type form element
- *
- * Contains HTML class for a text type element
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->libdir . '/form/static.php');
-
-/**
- * Text type element
- *
- * HTML class for a text type element
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class scheduler_form_element extends MoodleQuickForm_static implements templatable {
- /**
- * Form element scheduler.
- *
- * @param string $elementname (optional) Name of the text field.
- * @param string $elementlabel (optional) text field label.
- * @param string $text (optional) Text to put in text field.
- */
- public function __construct($elementname = null, $elementlabel = null, $text = null) {
- global $OUTPUT;
- $text = $OUTPUT->render_from_template('local_assessfreq/scheduler_form_element', ['foo' => $text]);
-
- parent::__construct($elementname, $elementlabel, $text);
- }
-}
diff --git a/classes/frequency.php b/classes/frequency.php
index 97e9158f..9bcd2446 100644
--- a/classes/frequency.php
+++ b/classes/frequency.php
@@ -25,10 +25,19 @@
namespace local_assessfreq;
use cache;
+use context;
+use core\dml\sql_join;
+use core\oauth2\service\microsoft;
+use Exception;
+use moodle_recordset;
+use stdClass;
defined('MOODLE_INTERNAL') || die();
+global $CFG;
+
require_once($CFG->dirroot . '/calendar/lib.php');
+require_once($CFG->dirroot . '/local/assessfreq/lib.php');
/**
* Frequency class.
@@ -41,74 +50,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class frequency {
- /**
- * The due date databse field differs between module types.
- * This map provides the translation.
- *
- * @var array $modduefield
- */
- private $moduleendfield = [
- 'assign' => 'duedate',
- 'choice' => 'timeclose',
- 'data' => 'timeavailableto',
- 'feedback' => 'timeclose',
- 'forum' => 'duedate',
- 'lesson' => 'deadline',
- 'quiz' => 'timeclose',
- 'scorm' => 'timeclose',
- 'workshop' => 'submissionend',
- ];
-
- /**
- * The start date databse field differs between module types.
- * This map provides the translation.
- *
- * @var array $modduefield
- */
- private $modulestartfield = [
- 'assign' => 'allowsubmissionsfromdate',
- 'choice' => 'timeopen',
- 'data' => 'timeavailablefrom',
- 'feedback' => 'timeopen',
- 'forum' => null,
- 'lesson' => 'available',
- 'quiz' => 'timeopen',
- 'scorm' => 'timeopen',
- 'workshop' => 'submissionstart',
- ];
-
- /**
- * The time limit databse field differs between module types and only some support it.
- * This map provides the translation
- *
- * @var array $moduletimelimit
- */
- private $moduletimelimit = [
- 'leesson' => 'timelimit',
- 'quiz' => 'timelimit',
-
- ];
-
-
- /**
- * Map of capabilities that users must have
- * before that activity event applies to them.
- *
- * @var array $capabilitymap
- */
- private $capabilitymap = [
- 'assign' => ['mod/assign:submit', 'mod/assign:view'],
- 'choice' => ['mod/choice:choose', 'mod/choice:view'],
- 'data' => ['mod/data:writeentry', 'mod/data:viewentry', 'mod/data:view'],
- 'feedback' => ['mod/feedback:complete', 'mod/feedback:viewanalysepage', 'mod/feedback:view'],
- 'forum' => [
- 'mod/forum:startdiscussion', 'mod/forum:createattachment', 'mod/forum:replypost', 'mod/forum:viewdiscussion', ],
- 'lesson' => ['mod/lesson:view'],
- 'quiz' => ['mod/quiz:attempt', 'mod/quiz:view'],
- 'scorm' => ['mod/scorm:savetrack', 'mod/scorm:viewscores'],
- 'workshop' => ['mod/workshop:submit', 'mod/workshop:view'],
- ];
-
/**
* Expiry period for caches.
*
@@ -121,27 +62,12 @@ class frequency {
*
* @var integer $batchsize
*/
- private $batchsize = 100;
+ private int $batchsize = 100;
/**
- * Get the modules to use in data collection.
- * This is based on plugin configuration.
- *
- * @return array $modules The enabled modules.
+ * Cache of event users.
*/
- public function get_modules(): array {
- $version = get_config('moodle', 'version');
-
- // Start with a hardcoded list of modules. As there is not a good way to get a list of suppoerted modules.
- // Different versions of Moodle have different supported modules. This is an anti pattern, but yeah...
- if ($version < 2019052000) { // Versions less than 3.7 don't support forum due dates.
- $availablemodules = ['assign', 'choice', 'data', 'feedback', 'lesson', 'quiz', 'scorm', 'workshop'];
- } else {
- $availablemodules = ['assign', 'choice', 'data', 'feedback', 'forum', 'lesson', 'quiz', 'scorm', 'workshop'];
- }
-
- return $availablemodules;
- }
+ private array $eventuserscache = [];
/**
* Given a modle shortname get capabilities that users must have
@@ -151,20 +77,8 @@ public function get_modules(): array {
* @return array Capabilities relating to the module.
*/
public function get_module_capabilities(string $module): array {
- return $this->capabilitymap[$module];
- }
-
- /**
- * Get currently enabled modules from the Moodle DB.
- *
- * @return array $modules The enabled modules.
- */
- public function get_enabled_modules(): array {
- global $DB;
-
- $modules = $DB->get_records_menu('modules', [], '', 'name, visible');
-
- return $modules;
+ $sources = get_sources(true);
+ return $sources[$module]->get_user_capabilities();
}
/**
@@ -177,17 +91,13 @@ public function get_enabled_modules(): array {
* @return array $modules Lis of modules to process.
*/
public function get_process_modules(): array {
- $config = get_config('local_assessfreq');
- $modules = explode(',', $config->modules);
- $disabledmodules = $config->disabledmodules;
-
- if (!$disabledmodules) {
- $enabledmodules = $this->get_enabled_modules();
+ $sources = get_sources();
+ $modules = [];
- foreach ($modules as $index => $module) {
- if (empty($enabledmodules[$module])) {
- unset($modules[$index]);
- }
+ if (!empty($sources)) {
+ /* @var $source source_base */
+ foreach ($sources as $source) {
+ $modules[] = $source->get_module();
}
}
@@ -200,19 +110,15 @@ public function get_process_modules(): array {
* @param string $module Activity module to get data for.
* @return string $sql The generated SQL.
*/
- private function get_sql_query(string $module): string {
+ private function get_sql_query(string $module, $duedate, $startdate, $timelimit): string {
$includehiddencourses = get_config('local_assessfreq', 'hiddencourses');
-
- $duedate = $this->moduleendfield[$module];
$sql = 'SELECT cm.id, cm.course, m.name, cm.instance, c.id as contextid, a.' . $duedate . ' AS duedate ';
- if (!empty($this->modulestartfield[$module])) {
- $startdate = $this->modulestartfield[$module];
+ if ($startdate) {
$sql .= ', a.' . $startdate . ' AS startdate ';
}
- if (!empty($this->moduletimelimit[$module])) {
- $timelimit = $this->moduletimelimit[$module];
+ if ($timelimit) {
$sql .= ', a.' . $timelimit . ' AS timelimit ';
}
@@ -239,14 +145,12 @@ private function get_sql_query(string $module): string {
*
* @param string $sql
* @param array $params
- * @return \moodle_recordset
+ * @return moodle_recordset
*/
- private function get_module_events(string $sql, array $params): \moodle_recordset {
+ private function get_module_events(string $sql, array $params): moodle_recordset {
global $DB;
- $recordset = $DB->get_recordset_sql($sql, $params);
-
- return $recordset;
+ return $DB->get_recordset_sql($sql, $params);
}
/**
@@ -257,13 +161,11 @@ private function get_module_events(string $sql, array $params): \moodle_recordse
* @return array $timeelements Array of split time.
*/
private function format_time(int $timestamp): array {
- $timeelements = [
+ return [
'endyear' => date('Y', $timestamp),
'endmonth' => date('m', $timestamp),
'endday' => date('d', $timestamp),
];
-
- return $timeelements;
}
/**
@@ -271,9 +173,9 @@ private function format_time(int $timestamp): array {
* The event date may have been changed from in the past to in the future. In this case it may
* not have been picked up by the delete records process. This method removes it a processing time.
*
- * @param \stdClass $record The record to process.
+ * @param stdClass $record The record to process.
*/
- private function cleanup_record(\stdClass $record): void {
+ private function cleanup_record(stdClass $record): void {
global $DB;
$params = ['module' => $record->module, 'instanceid' => $record->instanceid];
@@ -289,10 +191,10 @@ private function cleanup_record(\stdClass $record): void {
* Take a recordest of events process
* and store in correct database table.
*
- * @param \moodle_recordset $recordset
- * @return array
+ * @param moodle_recordset $recordset
+ * @return int
*/
- private function process_module_events(\moodle_recordset $recordset): int {
+ private function process_module_events(moodle_recordset $recordset): int {
global $DB;
$recordsprocessed = 0;
$toinsert = [];
@@ -308,7 +210,7 @@ private function process_module_events(\moodle_recordset $recordset): int {
// Iterate through the records and insert to database in batches.
$timeelements = $this->format_time($record->duedate);
- $insertrecord = new \stdClass();
+ $insertrecord = new stdClass();
$insertrecord->module = $record->name;
$insertrecord->instanceid = $record->instance;
$insertrecord->courseid = $record->course;
@@ -328,7 +230,6 @@ private function process_module_events(\moodle_recordset $recordset): int {
// Insert in database.
$DB->insert_records('local_assessfreq_site', $toinsert);
$toinsert = []; // Reset array.
- $recordsprocessed += count($toinsert);
}
}
@@ -352,17 +253,24 @@ private function process_module_events(\moodle_recordset $recordset): int {
*/
public function process_site_events(int $duedate): int {
$recordsprocessed = 0;
- $enabledmods = $this->get_process_modules();
+ $sources = get_sources(true);
$includehiddencourses = get_config('local_assessfreq', 'hiddencourses');
- if (!empty($enabledmods[0])) {
- // Itterate through modules.
- foreach ($enabledmods as $module) {
- $sql = $this->get_sql_query($module);
+ if (!empty($sources)) {
+ // Itterate through sources.
+ foreach ($sources as $source) {
+
+ /* @var $source source_base */
+ $sql = $this->get_sql_query(
+ $source->get_module_table(),
+ $source->get_close_field(),
+ $source->get_open_field(),
+ $source->get_timelimit_field()
+ );
if ($includehiddencourses) {
- $params = [$module, CONTEXT_MODULE, $duedate, 1];
+ $params = [$source->get_module(), CONTEXT_MODULE, $duedate, 1];
} else {
- $params = [$module, CONTEXT_MODULE, $duedate, 1, 1];
+ $params = [$source->get_module(), CONTEXT_MODULE, $duedate, 1, 1];
}
$moduleevents = $this->get_module_events($sql, $params); // Get all events for module.
@@ -378,11 +286,11 @@ public function process_site_events(int $duedate): int {
* get the enrolled users with given capabilities for a given context.
* Used to generte SQL for getting users in assessments.
*
- * @param \context $context The context to get the enrolled users for.
+ * @param context $context The context to get the enrolled users for.
* @param array $capabilities The capabilities that users need to have.
* @return array
*/
- public function generate_enrolled_wheres_joins_params(\context $context, array $capabilities): array {
+ public function generate_enrolled_wheres_joins_params(context $context, array $capabilities): array {
$uid = 'u.id';
$joins = [];
$wheres = [];
@@ -401,33 +309,30 @@ public function generate_enrolled_wheres_joins_params(\context $context, array $
$wheres[] = "u.deleted = 0";
$wheres = implode(" AND ", $wheres);
- $wherejoin = [$joins, $wheres, $params];
-
- return $wherejoin;
+ return [$joins, $wheres, $params];
}
/**
* Our own implementation of get_enrolled_users. Allows us to check multiple capabilities
* in less database queries.
*
- * @param \context $context The context to get the enrolled users for.
+ * @param context $context The context to get the enrolled users for.
* @param array $capabilities The capabilities that users need to have.
* @return array Enrolled user records
*/
- private function get_enrolled_users(\context $context, array $capabilities): array {
+ private function get_enrolled_users(context $context, array $capabilities): array {
global $DB;
[$joins, $wheres, $params] = $this->generate_enrolled_wheres_joins_params($context, $capabilities);
- $finaljoin = new \core\dml\sql_join($joins, $wheres, $params);
+ $finaljoin = new sql_join($joins, $wheres, $params);
$sql = "SELECT DISTINCT u.id
- FROM {user} u
- $finaljoin->joins
- WHERE $finaljoin->wheres";
- $params = $finaljoin->params;
+ FROM {user} u
+ $finaljoin->joins
+ WHERE $finaljoin->wheres";
- return $DB->get_records_sql($sql, $params);
+ return $DB->get_records_sql($sql, $finaljoin->params);
}
/**
@@ -436,17 +341,33 @@ private function get_enrolled_users(\context $context, array $capabilities): arr
* this can take a long time. Consider using the get_event_users method
* if you don't need the most up to date data.
*
- * @param int $contextid The context ID in a course for the event to check.
+ * @param int $contextid The module context ID for the event to check.
* @param string $module The type of module the event is for.
* @return array $users An array of user IDs.
*/
public function get_event_users_raw(int $contextid, string $module): array {
- $context = \context::instance_by_id($contextid);
+
+ $context = context::instance_by_id($contextid);
+ $coursecontext = $context->get_parent_context();
+
+ $cachekey = "{$coursecontext->id}-{$module}";
+ if (isset($this->eventuserscache[$cachekey])) {
+ return $this->eventuserscache[$cachekey];
+ }
+
$capabilities = $this->get_module_capabilities($module);
- $users = $this->get_enrolled_users($context, $capabilities);
+ $roles = [];
+ foreach ($capabilities as $capability) {
+ $roles = $roles + get_roles_with_capability($capability, CAP_ALLOW, $context);
+ }
+ $users = [];
+ foreach ($roles as $role) {
+ $users = $users + get_users_from_role_on_context($role, $coursecontext);
+ }
- return $users;
+ $this->eventuserscache[$cachekey] = $users;
+ return $this->eventuserscache[$cachekey];
}
/**
@@ -460,8 +381,7 @@ public function get_event_users_raw(int $contextid, string $module): array {
*/
public function get_event_users(int $contextid, string $module, bool $cache = true): array {
global $DB;
- $users = [];
- $cachekey = (string)$contextid . '_' . $module;
+ $cachekey = $contextid . '_' . $module;
// Try to get value from cache.
$usercache = cache::make('local_assessfreq', 'eventusers');
@@ -472,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);
@@ -483,7 +404,7 @@ public function get_event_users(int $contextid, string $module, bool $cache = tr
// Update cache.
if (!empty($users)) {
$expiry = time() + $this->expiryperiod;
- $data = new \stdClass();
+ $data = new stdClass();
$data->expiry = $expiry;
$data->users = $users;
$usercache->set($cachekey, $data);
@@ -497,22 +418,20 @@ public function get_event_users(int $contextid, string $module, bool $cache = tr
* Get stored events from a specified date.
*
* @param int $duedate The duedate to get events from.
- * @return \moodle_recordset Recordset of event info.
+ * @return moodle_recordset Recordset of event info.
*/
- private function get_stored_events(int $duedate): \moodle_recordset {
+ private function get_stored_events(int $duedate): moodle_recordset {
global $DB;
$select = 'timeend >= ?';
$params = [$duedate];
- $recordset = $DB->get_recordset_select(
+ return $DB->get_recordset_select(
'local_assessfreq_site',
$select,
$params,
'timeend DESC',
'id, contextid, module'
);
-
- return $recordset;
}
/**
@@ -526,8 +445,8 @@ private function get_stored_events(int $duedate): \moodle_recordset {
private function prepare_user_event_records(array $users, int $eventid): array {
$userrecords = [];
foreach ($users as $user) {
- $record = new \stdClass();
- $record->userid = $user->id;
+ $record = new stdClass();
+ $record->userid = $user->userid;
$record->eventid = $eventid;
$userrecords[] = $record;
@@ -574,8 +493,8 @@ public function delete_events(int $duedate): void {
$select = 'timeend >= ?';
// We do the following in a transaction to maintain data consistency.
+ $transaction = $DB->start_delegated_transaction();
try {
- $transaction = $DB->start_delegated_transaction();
$userevents = $DB->get_fieldset_select('local_assessfreq_site', 'id', $select, [$duedate]);
// Delete site events.
@@ -591,8 +510,19 @@ public function delete_events(int $duedate): void {
}
}
+ // Clear the caches to prevent desync between caches and database.
+ cache::make('local_assessfreq', 'siteevents')->purge();
+ cache::make('local_assessfreq', 'userevents')->purge();
+ cache::make('local_assessfreq', 'courseevents')->purge();
+ cache::make('local_assessfreq', 'eventsduemonth')->purge();
+ cache::make('local_assessfreq', 'monthlyuser')->purge();
+ cache::make('local_assessfreq', 'eventsdueactivity')->purge();
+ cache::make('local_assessfreq', 'yearevents')->purge();
+ cache::make('local_assessfreq', 'usereventsallfrequencyarray')->purge();
+ cache::make('local_assessfreq', 'eventusers')->purge();
+
$transaction->allow_commit();
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$transaction->rollback($e);
}
}
@@ -600,21 +530,20 @@ public function delete_events(int $duedate): void {
/**
* Delete processed event.
*
- * @param \stdClass $event The event to delete.
+ * @param stdClass $event The event to delete.
*/
- public function delete_event(\stdClass $event): void {
+ public function delete_event(stdClass $event): void {
global $DB;
// We do the following in a transaction to maintain data consistency.
+ $transaction = $DB->start_delegated_transaction();
try {
- $transaction = $DB->start_delegated_transaction();
-
// Delete site events.
$DB->delete_records('local_assessfreq_site', ['id' => $event->id]);
$DB->delete_records('local_assessfreq_user', ['eventid' => $event->id]);
$transaction->allow_commit();
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$transaction->rollback($e);
}
}
@@ -628,7 +557,7 @@ public function delete_event(\stdClass $event): void {
* @param int $to Timestamp to fiter to.
* @return array $filteredevents The list of filtered events.
*/
- private function filter_event_data($events, int $from, int $to = 0): array {
+ private function filter_event_data(array $events, int $from, int $to = 0): array {
$filteredevents = [];
// If an explicit to date was not defined default to a year from now.
@@ -650,15 +579,15 @@ private function filter_event_data($events, int $from, int $to = 0): array {
* Get site events.
* This is events across all courses.
*
+ * @param int $courseid The course to get events for or all events. This is not used here but kept for function mapping.
* @param string $module The module to get events for or all events.
* @param int $from The timestamp to get events from.
* @param int $to The timestamp to get events to.
* @param bool $cache If false cache won't be used fresh data will be retrieved from DB.
* @return array $events An array of site events
*/
- public function get_site_events(string $module = 'all', int $from = 0, int $to = 0, bool $cache = true): array {
+ public function get_site_events(int $courseid, string $module = 'all', int $from = 0, int $to = 0, bool $cache = true): array {
global $DB;
- $events = [];
// Try to get value from cache.
$sitecache = cache::make('local_assessfreq', 'siteevents');
@@ -694,7 +623,7 @@ public function get_site_events(string $module = 'all', int $from = 0, int $to =
// Update cache.
if (!empty($rawevents)) {
$expiry = time() + $this->expiryperiod;
- $data = new \stdClass();
+ $data = new stdClass();
$data->expiry = $expiry;
$data->events = $rawevents;
$sitecache->set($module, $data);
@@ -708,14 +637,14 @@ public function get_site_events(string $module = 'all', int $from = 0, int $to =
/**
* Get all events that are ending on a given date.
*
+ * @param int $courseid The course to get events for.
* @param string $date The end date for the event.
* @param string $module The module to get events for or all events.
*
* @return array $events An array of site events
*/
- public function get_day_ending_events(string $date, string $module = 'all'): array {
+ public function get_day_ending_events(int $courseid , string $date, string $module = 'all'): array {
global $DB;
- $events = [];
// TODO: Think about some caching here.
// TODO: Improve unit test coverage for this.
@@ -755,9 +684,13 @@ public function get_day_ending_events(string $date, string $module = 'all'): arr
$params[] = $tostart;
$params[] = $toend;
- $events = $DB->get_records_sql($sql, $params);
+ // Add the courseid restrictions.
+ if ($courseid != SITEID) {
+ $params[] = $courseid;
+ $sql .= " AND c.id = ?";
+ }
- return $events;
+ return $DB->get_records_sql($sql, $params);
}
/**
@@ -778,8 +711,8 @@ public function get_course_events(
bool $cache = true
): array {
global $DB;
- $events = [];
- $cachekey = (string)$courseid . '_' . $module;
+
+ $cachekey = $courseid . '_' . $module . '_' . $from . '_' . $to;
// Try to get value from cache.
$coursecache = cache::make('local_assessfreq', 'courseevents');
@@ -801,7 +734,7 @@ public function get_course_events(
// Update cache.
if (!empty($rawevents)) {
$expiry = time() + $this->expiryperiod;
- $data = new \stdClass();
+ $data = new stdClass();
$data->expiry = $expiry;
$data->events = $rawevents;
$coursecache->set($cachekey, $data);
@@ -823,8 +756,8 @@ public function get_course_events(
*/
public function get_user_events(int $userid, string $module = 'all', int $from = 0, int $to = 0, bool $cache = true): array {
global $DB;
- $events = [];
- $cachekey = (string)$userid . '_' . $module;
+
+ $cachekey = $userid . '_' . $module;
// Try to get value from cache.
$usercache = cache::make('local_assessfreq', 'userevents');
@@ -859,7 +792,7 @@ public function get_user_events(int $userid, string $module = 'all', int $from =
// Update cache.
if (!empty($rawevents)) {
$expiry = time() + $this->expiryperiod;
- $data = new \stdClass();
+ $data = new stdClass();
$data->expiry = $expiry;
$data->events = $rawevents;
$usercache->set($cachekey, $data);
@@ -872,15 +805,16 @@ public function get_user_events(int $userid, string $module = 'all', int $from =
/**
* Return events for all users.
*
+ * @param int $courseid The course to get events from.
* @param string $module The module to get events for or all events.
* @param int $from The timestamp to get events from.
* @param int $to The timestamp to get events to.
* @return array $events An array of site events
*/
- public function get_user_events_all(string $module = 'all', int $from = 0, int $to = 0): iterable {
+ 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
@@ -896,12 +830,19 @@ public function get_user_events_all(string $module = 'all', int $from = 0, int $
$sql .= ' WHERE s.module = ?';
}
+ // Should we include hidden courses.
$includehiddencourses = get_config('local_assessfreq', 'hiddencourses');
if (!$includehiddencourses) {
$params[] = 1;
$sql .= " AND c.visible = ?";
}
+ // Add the courseid restriction.
+ if ($courseid != SITEID) {
+ $params[] = $courseid;
+ $sql .= " AND c.id = ?";
+ }
+
// If an explicit to date was not defined default to a year from now.
if ($to === 0) {
$to = time() + YEARSECS;
@@ -911,9 +852,7 @@ public function get_user_events_all(string $module = 'all', int $from = 0, int $
$params[] = $to;
$sql .= " AND s.timeend >= ? AND s.timeend < ?";
- $events = $DB->get_recordset_sql($sql, $params);
-
- return $events;
+ return $DB->get_records_sql($sql, $params);
}
/**
@@ -923,10 +862,15 @@ public function get_user_events_all(string $module = 'all', int $from = 0, int $
* @param bool $cache Fetch events from cache.
* @return array $events The events.
*/
- public function get_events_due_by_month(int $year, bool $cache = true): array {
- global $DB;
- $events = [];
- $cachekey = (string)$year;
+ public function get_events_due_by_month(int $year, int $month = 0, bool $cache = true): array {
+ global $DB, $PAGE;
+
+ // Adjust the cache key based on course.
+ if ($PAGE->course->id != SITEID) {
+ $cachekey = $PAGE->course->id . '_' . $year . '_' . $month;
+ } else {
+ $cachekey = $year . '_' . $month;
+ }
// Try to get value from cache.
$usercache = cache::make('local_assessfreq', 'eventsduemonth');
@@ -937,12 +881,10 @@ public function get_events_due_by_month(int $year, bool $cache = true): array {
} else { // Not valid cache data.
$modules = $this->get_process_modules();
[$insql, $params] = $DB->get_in_or_equal($modules);
- $params[] = $year;
$sql = "SELECT s.endmonth, COUNT(s.id) as count
FROM {local_assessfreq_site} s
LEFT JOIN {course} c ON s.courseid = c.id
- WHERE s.module $insql
- AND s.endyear = ? ";
+ WHERE s.module $insql ";
$includehiddencourses = get_config('local_assessfreq', 'hiddencourses');
if (!$includehiddencourses) {
@@ -950,7 +892,29 @@ public function get_events_due_by_month(int $year, bool $cache = true): array {
$sql .= " AND c.visible = ? ";
}
- $sql .= 'GROUP BY s.endmonth
+ // Add the courseid restriction.
+ if ($PAGE->course->id != SITEID) {
+ $params[] = $PAGE->course->id;
+ $sql .= " AND c.id = ? ";
+ }
+
+ // Add month restrictions.
+ if ($month && $month > 1) {
+ $params[] = $month;
+ $params[] = $year;
+ $params[] = $month;
+ $params[] = $year + 1;
+ $sql .= " AND (s.endmonth >= ? AND s.endyear = ? OR s.endmonth < ? AND s.endyear = ?) ";
+ } else if ($month == 1) {
+ $params[] = $month;
+ $params[] = $year;
+ $sql .= " AND s.endmonth >= ? AND s.endyear = ? ";
+ } else {
+ $params[] = $year;
+ $sql .= " AND s.endyear = ? ";
+ }
+
+ $sql .= ' GROUP BY s.endmonth
ORDER BY s.endmonth ASC';
$events = $DB->get_records_sql($sql, $params);
@@ -959,7 +923,7 @@ public function get_events_due_by_month(int $year, bool $cache = true): array {
// Update cache.
if (!empty($events)) {
$expiry = time() + $this->expiryperiod;
- $data = new \stdClass();
+ $data = new stdClass();
$data->expiry = $expiry;
$data->events = $events;
$usercache->set($cachekey, $data);
@@ -975,10 +939,15 @@ public function get_events_due_by_month(int $year, bool $cache = true): array {
* @param bool $cache Fetch events from cache.
* @return array $events The events.
*/
- public function get_events_due_monthly_by_user(int $year, bool $cache = true): array {
- global $DB;
- $events = [];
- $cachekey = (string)$year;
+ public function get_events_due_monthly_by_user(int $year, int $month = 0, bool $cache = true): array {
+ global $DB, $PAGE;
+
+ // Adjust the cache key based on course.
+ if ($PAGE->course->id != SITEID) {
+ $cachekey = $PAGE->course->id . '_' . $year . '_' . $month;
+ } else {
+ $cachekey = $year . '_' . $month;
+ }
// Try to get value from cache.
$usercache = cache::make('local_assessfreq', 'monthlyuser');
@@ -989,13 +958,11 @@ public function get_events_due_monthly_by_user(int $year, bool $cache = true): a
} else { // Not valid cache data.
$modules = $this->get_process_modules();
[$insql, $params] = $DB->get_in_or_equal($modules);
- $params[] = $year;
$sql = "SELECT s.endmonth, COUNT(u.id) as count
FROM {local_assessfreq_site} s
INNER JOIN {local_assessfreq_user} u ON s.id = u.eventid
INNER JOIN {course} c ON s.courseid = c.id
- WHERE s.module $insql
- AND s.endyear = ? ";
+ WHERE s.module $insql ";
$includehiddencourses = get_config('local_assessfreq', 'hiddencourses');
if (!$includehiddencourses) {
@@ -1003,7 +970,29 @@ public function get_events_due_monthly_by_user(int $year, bool $cache = true): a
$sql .= " AND c.visible = ? ";
}
- $sql .= 'GROUP BY s.endmonth
+ // Add the courseid restriction.
+ if ($PAGE->course->id != SITEID) {
+ $params[] = $PAGE->course->id;
+ $sql .= " AND c.id = ? ";
+ }
+
+ // Add month restrictions.
+ if ($month && $month > 1) {
+ $params[] = $month;
+ $params[] = $year;
+ $params[] = $month;
+ $params[] = $year + 1;
+ $sql .= " AND (s.endmonth >= ? AND s.endyear = ? OR s.endmonth < ? AND s.endyear = ?) ";
+ } else if ($month == 1) {
+ $params[] = $month;
+ $params[] = $year;
+ $sql .= " AND s.endmonth >= ? AND s.endyear = ? ";
+ } else {
+ $params[] = $year;
+ $sql .= " AND s.endyear = ? ";
+ }
+
+ $sql .= ' GROUP BY s.endmonth
ORDER BY s.endmonth ASC';
$events = $DB->get_records_sql($sql, $params);
@@ -1012,7 +1001,7 @@ public function get_events_due_monthly_by_user(int $year, bool $cache = true): a
// Update cache.
if (!empty($events)) {
$expiry = time() + $this->expiryperiod;
- $data = new \stdClass();
+ $data = new stdClass();
$data->expiry = $expiry;
$data->events = $events;
$usercache->set($cachekey, $data);
@@ -1028,10 +1017,15 @@ public function get_events_due_monthly_by_user(int $year, bool $cache = true): a
* @param bool $cache Fetch events from cache.
* @return array $events The events.
*/
- public function get_events_due_by_activity(int $year, bool $cache = true): array {
- global $DB;
- $events = [];
- $cachekey = (string)$year . '_activity';
+ public function get_events_due_by_activity(int $year, int $month = 0, bool $cache = true): array {
+ global $DB, $PAGE;
+
+ // Adjust the cache key based on course.
+ if ($PAGE->course->id != SITEID) {
+ $cachekey = $PAGE->course->id . '_' . $year . '_' . $month;
+ } else {
+ $cachekey = $year . '_' . $month;
+ }
// Try to get value from cache.
$usercache = cache::make('local_assessfreq', 'eventsdueactivity');
@@ -1040,11 +1034,11 @@ public function get_events_due_by_activity(int $year, bool $cache = true): array
if ($data && (time() < $data->expiry) && $cache) { // Valid cache data.
$events = $data->events;
} else { // Not valid cache data.
- $params = [$year];
+ $params = [];
$sql = 'SELECT s.module, COUNT(s.id) as count
FROM {local_assessfreq_site} s
- LEFT JOIN {course} c ON s.courseid = c.id
- WHERE s.endyear = ? ';
+ LEFT JOIN {course} c ON s.courseid = c.id
+ WHERE 1=1';
$includehiddencourses = get_config('local_assessfreq', 'hiddencourses');
if (!$includehiddencourses) {
@@ -1052,7 +1046,29 @@ public function get_events_due_by_activity(int $year, bool $cache = true): array
$sql .= " AND c.visible = ? ";
}
- $sql .= 'GROUP BY s.module
+ // Add the courseid restriction.
+ if ($PAGE->course->id != SITEID) {
+ $params[] = $PAGE->course->id;
+ $sql .= " AND c.id = ? ";
+ }
+
+ // Add month restrictions.
+ if ($month && $month > 1) {
+ $params[] = $month;
+ $params[] = $year;
+ $params[] = $month;
+ $params[] = $year + 1;
+ $sql .= " AND (s.endmonth >= ? AND s.endyear = ? OR s.endmonth < ? AND s.endyear = ?) ";
+ } else if ($month == 1) {
+ $params[] = $month;
+ $params[] = $year;
+ $sql .= " AND s.endmonth >= ? AND s.endyear = ? ";
+ } else {
+ $params[] = $year;
+ $sql .= " AND s.endyear = ? ";
+ }
+
+ $sql .= ' GROUP BY s.module
ORDER BY s.module ASC';
$events = $DB->get_records_sql($sql, $params);
@@ -1061,7 +1077,7 @@ public function get_events_due_by_activity(int $year, bool $cache = true): array
// Update cache.
if (!empty($events)) {
$expiry = time() + $this->expiryperiod;
- $data = new \stdClass();
+ $data = new stdClass();
$data->expiry = $expiry;
$data->events = $events;
$usercache->set($cachekey, $data);
@@ -1078,7 +1094,6 @@ public function get_events_due_by_activity(int $year, bool $cache = true): array
*/
public function get_years_has_events(bool $cache = true): array {
global $DB;
- $years = [];
$cachekey = 'yearevents';
// Try to get value from cache.
@@ -1097,7 +1112,7 @@ public function get_years_has_events(bool $cache = true): array {
// Update cache.
if (!empty($years)) {
$expiry = time() + $this->expiryperiod;
- $data = new \stdClass();
+ $data = new stdClass();
$data->expiry = $expiry;
$data->events = $years;
$usercache->set($cachekey, $data);
@@ -1109,12 +1124,14 @@ public function get_years_has_events(bool $cache = true): array {
/**
* Get all events on a particular day.
*
+ * @param int $courseid The course to get events for.
* @param string $date A string representations of the date to get events for.
* @param array $modules The modules to get events for.
* @return array $dayevents The list of events that day.
*/
- public function get_day_events(string $date, array $modules): array {
+ public function get_day_events(int $courseid, string $date, array $modules): array {
$dayevents = [];
+ $events = [];
if (empty($modules)) {
$modules = ['all'];
@@ -1122,21 +1139,23 @@ public function get_day_events(string $date, array $modules): array {
// Get the raw events.
if (in_array('all', $modules)) {
- $events = $this->get_day_ending_events($date, 'all');
+ $events = $this->get_day_ending_events($courseid, $date);
} else {
// Work through the event array.
foreach ($modules as $module) {
if ($module == 'all') {
continue;
} else {
- $events = array_merge($events, $this->get_day_ending_events($date, $module));
+ $events = array_merge($events, $this->get_day_ending_events($courseid, $date, $module));
}
}
}
+ $sources = get_sources();
+
// Get additional information and format the event data.
foreach ($events as $event) {
- $context = \context::instance_by_id($event->contextid, IGNORE_MISSING);
+ $context = context::instance_by_id($event->contextid, IGNORE_MISSING);
$course = get_course($event->courseid);
if ($context) {
@@ -1144,13 +1163,15 @@ public function get_day_events(string $date, array $modules): array {
$event->url = $context->get_url()->out();
$event->usercount = count($this->get_event_users($event->contextid, $event->module));
$event->timelimit =
- ($event->timelimit == 0) ? get_string('na', 'local_assessfreq') : round(($event->timelimit / 60));
+ ($event->timelimit == 0) ? '-' : round(($event->timelimit / 60));
+ $event->dashurl = '';
- if ($event->module == 'quiz') {
- $dashurl = new \moodle_url('/local/assessfreq/dashboard_quiz.php', ['id' => $event->instanceid]);
+ /* @var $source source_base */
+ $source = $sources[$event->module];
+ if (method_exists($source, 'get_activity_dashboard')) {
+ $dashurl = new \moodle_url('/local/assessfreq/', ['activityid' => $context->instanceid], 'activity_dashboard');
$event->dashurl = $dashurl->out();
}
-
$event->courseshortname = $course->shortname;
$dayevents[] = $event;
@@ -1186,24 +1207,28 @@ public function get_day_events(string $date, array $modules): array {
* @param array $modules List of modules to get events for.
* @return array $freqarray The array of even frequencies.
*/
- public function get_frequency_array(int $year, string $metric, array $modules): array {
+ public function get_frequency_array(int $year = 0, int $month = 0, string $metric = 'assess', array $modules = []): array {
+ global $PAGE;
+
$freqarray = [];
$events = [];
- $from = mktime(0, 0, 0, 1, 1, $year);
- $to = mktime(23, 59, 59, 12, 31, $year);
+ $from = mktime(0, 0, 0, $month, 1, $year);
+ $to = strtotime("+1 year", $from) - 1;
$userfreqarraycache = cache::make('local_assessfreq', 'usereventsallfrequencyarray');
sort($modules);
- $cachekey = implode("_", $modules) . '_' . (string)$from . '_' . (string)$to;
-
- if ($metric == 'assess') {
+ $cachekey = $PAGE->course->id . '_' . implode("_", $modules) . '_' . $from . '_' . $to;
+ if ($PAGE->course->id == SITEID) {
$functionname = 'get_site_events';
- } else if ($metric == 'students') {
+ } else {
+ $functionname = 'get_course_events';
+ }
+
+ if ($metric == 'students') {
+ $functionname = 'get_user_events_all';
$data = $userfreqarraycache->get($cachekey);
- if ($data && $metric == 'students' && (time() < $data->expiry)) {
+ if ($data && (time() < $data->expiry)) {
return $data->freqarray;
}
-
- $functionname = 'get_user_events_all';
}
if (empty($modules)) {
@@ -1211,20 +1236,21 @@ public function get_frequency_array(int $year, string $metric, array $modules):
}
// Get the raw events.
- if (in_array('all', $modules)) {
- $events = $this->$functionname('all', $from, $to);
- } else {
- // Work through the event array.
- foreach ($modules as $module) {
- $records = $this->$functionname($module, $from, $to);
- foreach ($records as $record) {
- $events[] = $record;
+ if (method_exists($this, $functionname)) {
+ if (in_array('all', $modules)) {
+ $events = $this->$functionname($PAGE->course->id, 'all', $from, $to);
+ } else {
+ // Work through the event array.
+ foreach ($modules as $module) {
+ $events = array_merge($events, $this->$functionname($PAGE->course->id, $module, $from, $to));
}
}
}
// Iterate through the events, building the frequency array.
+ raise_memory_limit(MEMORY_EXTRA);
foreach ($events as $event) {
+ $year = $event->endyear;
$month = $event->endmonth;
$day = $event->endday;
$module = $event->module;
@@ -1252,7 +1278,7 @@ public function get_frequency_array(int $year, string $metric, array $modules):
*/
if ($functionname == 'get_user_events_all') {
$expiry = time() + $this->expiryperiod;
- $data = new \stdClass();
+ $data = new stdClass();
$data->expiry = $expiry;
$data->freqarray = $freqarray;
$userfreqarraycache->set($cachekey, $data);
@@ -1269,17 +1295,21 @@ public function get_frequency_array(int $year, string $metric, array $modules):
* @param array $modules The modules to get.
* @return array $data The data for the download file.
*/
- public function get_download_data(int $year, string $metric, array $modules): array {
- global $DB;
+ public function get_download_data(int $year, int $month, string $metric, array $modules): array {
+ global $DB, $PAGE;
$data = [];
$events = [];
- $from = mktime(0, 0, 0, 1, 1, $year);
- $to = mktime(23, 59, 59, 12, 31, $year);
+ $from = mktime(0, 0, 0, $month, 1, $year);
+ $to = strtotime("+1 year", $from) - 1;
if ($metric == 'assess') {
- $functionname = 'get_site_events';
- } else if ($metric == 'students') {
+ if ($PAGE->course->id == SITEID) {
+ $functionname = 'get_site_events';
+ } else {
+ $functionname = 'get_course_events';
+ }
+ } else {
$functionname = 'get_user_events_all';
}
@@ -1289,28 +1319,29 @@ public function get_download_data(int $year, string $metric, array $modules): ar
// Get the raw events.
if (in_array('all', $modules)) {
- $events = $this->$functionname('all', $from, $to);
+ $events = $this->$functionname($PAGE->course->id, 'all', $from, $to);
} else {
// Work through the event array.
foreach ($modules as $module) {
if ($module == 'all') {
continue;
} else {
- $events = array_merge($events, $this->$functionname($module, $from, $to));
+ $events = array_merge($events, $this->$functionname($PAGE->course->id, $module, $from, $to));
}
}
}
+ // Soert the data by timeend.
+ usort($events, function($a, $b) {
+ return strcmp($a->timeend, $b->timeend);
+ });
+
// Format the data ready for download.
foreach ($events as $event) {
$row = [];
// Catch exception when context does not exist because assessfreq tables are out of sync.
- try {
- $context = \context::instance_by_id($event->contextid);
- } catch (\dml_missing_record_exception $ex) {
- continue;
- }
+ $context = context::instance_by_id($event->contextid);
$activity = get_string('modulename', $event->module);
$startdate = userdate($event->timestart, get_string('strftimedatetimeshort', 'langconfig'));
@@ -1339,116 +1370,6 @@ public function get_download_data(int $year, string $metric, array $modules): ar
}
$data[] = $row;
}
-
return $data;
}
-
- /**
- * Get heat colors to use id nheatmap display from plugin configuration.
- *
- * @return array
- */
- public function get_heat_colors(): array {
- $config = get_config('local_assessfreq');
-
- $heatcolors = [
- 1 => $config->heat1,
- 2 => $config->heat2,
- 3 => $config->heat3,
- 4 => $config->heat4,
- 5 => $config->heat5,
- 6 => $config->heat6,
- ];
-
- return $heatcolors;
- }
-
- /**
- * Purge all plugin caches.
- * This is invoked when a plugin setting is changed.
- *
- * @param string $name Name of the setting change that invoked the purge.
- */
- public static function purge_caches($name): void {
- global $CFG;
-
- // Get plugin cache definitions.
- $definitions = [];
- include($CFG->dirroot . '/local/assessfreq/db/caches.php');
- $definitionnames = array_keys($definitions);
-
- // Clear each cache.
- foreach ($definitionnames as $definitionname) {
- $cache = cache::make('local_assessfreq', $definitionname);
- $cache->purge();
- }
- }
-
- /**
- * Get assessment conflicts.
- *
- * @param int $now The timestamp to get the conflicts for.
- * @return array $conflicts The conflict data.
- */
- private function get_conflicts(int $now): array {
- global $DB;
- $conflicts = [];
-
- // A conflict is an overlapping date range for two or more quizzes where the quiz has at least one common student.
- $eventsql = 'SELECT lasa.id as eventid, lasb.id as conflictid
- FROM {local_assessfreq_site} lasa
- INNER JOIN {local_assessfreq_site} lasb ON (lasa.timestart > lasb.timestart AND lasa.timestart < lasb.timeend)
- OR (lasa.timeend > lasb.timestart AND lasa.timeend < lasb.timeend)
- OR (lasa.timeend > lasb.timeend AND lasa.timestart < lasb.timestart)
- WHERE lasa.module = ?
- AND lasb.module = ?
- AND lasa.timestart > ?';
- $eventparams = ['quiz', 'quiz', $now, $now];
- $recordset = $DB->get_recordset_sql($eventsql, $eventparams);
-
- foreach ($recordset as $record) {
- $usersql = 'SELECT DISTINCT laua.userid
- FROM {local_assessfreq_user} laua
- INNER JOIN {local_assessfreq_user} laub on laua.userid = laub.userid
- WHERE laua.eventid = ?
- AND laub.eventid = ?';
-
- $userparams = [$record->eventid, $record->conflictid];
- $users = $DB->get_fieldset_sql($usersql, $userparams);
-
- if (!empty($users)) {
- $conflict = new \stdClass();
- $conflict->eventid = $record->eventid;
- $conflict->conflictid = $record->conflictid;
- $conflict->users = $users;
-
- $conflicts[] = $conflict;
- }
- }
- $recordset->close();
-
- return $conflicts;
- }
-
- /**
- * Process the conflicts.
- *
- * @return array $conflicts Conflict data.
- */
- public function process_conflicts(): array {
-
- // Final result should look like this.
- $conflicts['eventid'] = [
- [
- 'conflicteventid' => 123,
- 'effecteduserids' => [1, 2, 3],
- ],
- [
- 'conflicteventid' => 456,
- 'effecteduserids' => [4, 5, 6],
- ],
- ];
-
- return $conflicts;
- }
}
diff --git a/classes/output/all_participants_inprogress.php b/classes/output/all_participants_inprogress.php
deleted file mode 100644
index 6c1d746f..00000000
--- a/classes/output/all_participants_inprogress.php
+++ /dev/null
@@ -1,124 +0,0 @@
-.
-
-/**
- * Renderable for all participant summary card.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace local_assessfreq\output;
-
-use local_assessfreq\quiz;
-
-/**
- * Renderable for all participant summary card.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class all_participants_inprogress {
- /**
- * Generate the markup for the summary chart,
- * used in the in progress quizzes dashboard.
- *
- * @param int $now Timestamp to get chart data for.
- * @param int $hoursahead Amount of time in hours to look ahead for quizzes starting.
- * @param int $hoursbehind Amount of time in hours to look behind for quizzes starting.
- * @return array With Generated chart object and chart data status.
- */
- public function get_all_participants_inprogress_chart(int $now, int $hoursahead = 0, int $hoursbehind = 0): array {
-
- // Get quizzes for the supplied timestamp.
- $quiz = new quiz($hoursahead, $hoursbehind);
- $quizzes = $quiz->get_quiz_summaries($now);
-
- $inprogressquizzes = $quizzes['inprogress'];
- $upcommingquizzes = $quizzes['upcomming'];
- $finishedquizzes = $quizzes['finished'];
-
- foreach ($upcommingquizzes as $timestamp => $upcommingquiz) {
- foreach ($upcommingquiz as $timestampupcomming => $upcomming) {
- $inprogressquizzes[$timestampupcomming] = $upcomming;
- }
- }
-
- foreach ($finishedquizzes as $timestamp => $finishedquiz) {
- foreach ($finishedquiz as $timestampfinished => $finished) {
- $inprogressquizzes[$timestampfinished] = $finished;
- }
- }
-
- $notloggedin = 0;
- $loggedin = 0;
- $inprogress = 0;
- $finished = 0;
-
- foreach ($inprogressquizzes as $quizobj) {
- if (!empty($quizobj->tracking)) {
- $notloggedin += $quizobj->tracking->notloggedin;
- $loggedin += $quizobj->tracking->loggedin;
- $inprogress += $quizobj->tracking->inprogress;
- $finished += $quizobj->tracking->finished;
- }
- }
-
- $result = [];
-
- if (($notloggedin == 0) && ($loggedin == 0) && ($inprogress == 0) && ($finished == 0)) {
- $result['hasdata'] = false;
- $result['chart'] = false;
- } else {
- $result['hasdata'] = true;
-
- $seriesdata = [
- $notloggedin,
- $loggedin,
- $inprogress,
- $finished,
- ];
-
- $labels = [
- get_string('notloggedin', 'local_assessfreq'),
- get_string('loggedin', 'local_assessfreq'),
- get_string('inprogress', 'local_assessfreq'),
- get_string('finished', 'local_assessfreq'),
- ];
-
- $colors = [
- get_config('local_assessfreq', 'notloggedincolor'),
- get_config('local_assessfreq', 'loggedincolor'),
- get_config('local_assessfreq', 'inprogresscolor'),
- get_config('local_assessfreq', 'finishedcolor'),
- ];
-
- // Create chart object.
- $chart = new \core\chart_pie();
- $chart->set_doughnut(true);
- $participants = new \core\chart_series(get_string('participants', 'local_assessfreq'), $seriesdata);
- $participants->set_colors($colors);
- $chart->add_series($participants);
- $chart->set_labels($labels);
-
- $result['chart'] = $chart;
- }
-
- return $result;
- }
-}
diff --git a/classes/output/dashboard_table.php b/classes/output/dashboard_table.php
deleted file mode 100644
index 4eaa7ffb..00000000
--- a/classes/output/dashboard_table.php
+++ /dev/null
@@ -1,201 +0,0 @@
-.
-namespace local_assessfreq\output;
-
-/**
- * Common code for outputting dashboard tables
- *
- * @package local_assessfreq
- * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
- * @author Mark Johnson
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-trait dashboard_table {
- /**
- * Get content for title column.
- *
- * @param \stdClass $row
- * @return string html used to display the video field.
- * @throws \moodle_exception
- */
- public function col_fullname($row): string {
- global $OUTPUT;
-
- return $OUTPUT->user_picture($row, ['size' => 35, 'includefullname' => true]);
- }
-
- /**
- * Get content for time start column.
- * Displays the user attempt start time.
- *
- * @param \stdClass $row
- * @return string html used to display the field.
- */
- public function col_timestart($row) {
- if ($row->timestart == 0) {
- $content = \html_writer::span(get_string('na', 'local_assessfreq'));
- } else {
- $datetime = userdate($row->timestart, get_string('trenddatetime', 'local_assessfreq'));
- $content = \html_writer::span($datetime);
- }
-
- return $content;
- }
-
- /**
- * Get content for time finish column.
- * Displays the user attempt finish time.
- *
- * @param \stdClass $row
- * @return string html used to display the field.
- */
- public function col_timefinish($row) {
- if ($row->timefinish == 0 && $row->timestart == 0) {
- $content = \html_writer::span(get_string('na', 'local_assessfreq'));
- } else if ($row->timefinish == 0 && $row->timestart > 0) {
- $time = $row->timestart + $row->timelimit;
- $datetime = userdate($time, get_string('trenddatetime', 'local_assessfreq'));
- $content = \html_writer::span($datetime, 'local-assessfreq-disabled');
- } else {
- $datetime = userdate($row->timefinish, get_string('trenddatetime', 'local_assessfreq'));
- $content = \html_writer::span($datetime);
- }
-
- return $content;
- }
-
- /**
- * Get content for state column.
- * Displays the users state in the quiz.
- *
- * @param \stdClass $row
- * @return string html used to display the field.
- */
- public function col_state($row) {
- if ($row->state == 'notloggedin') {
- $color = 'background: ' . get_config('local_assessfreq', 'notloggedincolor');
- } else if ($row->state == 'loggedin') {
- $color = 'background: ' . get_config('local_assessfreq', 'loggedincolor');
- } else if ($row->state == 'inprogress') {
- $color = 'background: ' . get_config('local_assessfreq', 'inprogresscolor');
- } else if ($row->state == 'uploadpending') {
- $color = 'background: ' . get_config('local_assessfreq', 'inprogresscolor');
- } else if ($row->state == 'finished') {
- $color = 'background: ' . get_config('local_assessfreq', 'finishedcolor');
- } else if ($row->state == 'abandoned') {
- $color = 'background: ' . get_config('local_assessfreq', 'finishedcolor');
- } else if ($row->state == 'overdue') {
- $color = 'background: ' . get_config('local_assessfreq', 'finishedcolor');
- }
-
- $content = \html_writer::span('', 'local-assessfreq-status-icon', ['style' => $color]);
- $content .= get_string($row->state, 'local_assessfreq');
-
- return $content;
- }
-
- /**
- * Return an array of headers common across dashboard tables.
- *
- * @return array
- */
- protected function get_common_headers(): array {
- return [
- get_string('quiztimeopen', 'local_assessfreq'),
- get_string('quiztimeclose', 'local_assessfreq'),
- get_string('quiztimelimit', 'local_assessfreq'),
- get_string('quiztimestart', 'local_assessfreq'),
- get_string('quiztimefinish', 'local_assessfreq'),
- get_string('status', 'local_assessfreq'),
- get_string('actions', 'local_assessfreq'),
- ];
- }
-
- /**
- * Return an array of columns common across dashboard tables.
- *
- * @return array
- */
- protected function get_common_columns(): array {
- return [
- 'timeopen',
- 'timeclose',
- 'timelimit',
- 'timestart',
- 'timefinish',
- 'state',
- 'actions',
- ];
- }
-
- /**
- * Return HTML for common column actions.
- *
- * @param \stdClass $row
- * @return string
- */
- protected function get_common_column_actions(\stdClass $row): string {
- global $OUTPUT;
- $actions = '';
- if (
- $row->state == 'finished'
- || $row->state == 'inprogress'
- || $row->state == 'uploadpending'
- || $row->state == 'abandoned'
- || $row->state == 'overdue'
- ) {
- $classes = 'action-icon';
- $attempturl = new \moodle_url('/mod/quiz/review.php', ['attempt' => $row->attemptid]);
- $attributes = [
- 'class' => $classes,
- 'id' => 'tool-assessfreq-attempt-' . $row->id,
- 'data-toggle' => 'tooltip',
- 'data-placement' => 'top',
- 'title' => get_string('userattempt', 'local_assessfreq'),
- ];
- } else {
- $classes = 'action-icon disabled';
- $attempturl = '#';
- $attributes = [
- 'class' => $classes,
- 'id' => 'tool-assessfreq-attempt-' . $row->id,
- ];
- }
- $icon = $OUTPUT->render(new \pix_icon('i/search', ''));
- $actions .= \html_writer::link($attempturl, $icon, $attributes);
-
- $profileurl = new \moodle_url('/user/profile.php', ['id' => $row->id]);
- $icon = $OUTPUT->render(new \pix_icon('i/completion_self', ''));
- $actions .= \html_writer::link($profileurl, $icon, [
- 'class' => 'action-icon',
- 'id' => 'tool-assessfreq-profile-' . $row->id,
- 'data-toggle' => 'tooltip',
- 'data-placement' => 'top',
- 'title' => get_string('userprofile', 'local_assessfreq'),
- ]);
-
- $logurl = new \moodle_url('/report/log/user.php', ['id' => $row->id, 'course' => 1, 'mode' => 'all']);
- $icon = $OUTPUT->render(new \pix_icon('i/report', ''));
- $actions .= \html_writer::link($logurl, $icon, [
- 'class' => 'action-icon',
- 'id' => 'tool-assessfreq-log-' . $row->id,
- 'data-toggle' => 'tooltip',
- 'data-placement' => 'top',
- 'title' => get_string('userlogs', 'local_assessfreq'),
- ]);
- return $actions;
- }
-}
diff --git a/classes/output/inprogress_participant_summary.php b/classes/output/inprogress_participant_summary.php
deleted file mode 100644
index c27ec8b1..00000000
--- a/classes/output/inprogress_participant_summary.php
+++ /dev/null
@@ -1,76 +0,0 @@
-.
-
-/**
- * Renderable for participant summary card.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace local_assessfreq\output;
-
-/**
- * Renderable for participant summary card.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class inprogress_participant_summary {
- /**
- * Generate the markup for the summary chart,
- * used in the quiz dashboard.
- *
- * @param \stdClass $participants The particpiant data.
- * @return \core\chart_pie $chart Generated chart object and chart data status.
- */
- public function get_inprogress_participant_summary_chart(\stdClass $participants): \core\chart_pie {
-
- $seriesdata = [
- $participants->notloggedin,
- $participants->loggedin,
- $participants->inprogress,
- $participants->finished,
- ];
-
- $labels = [
- get_string('notloggedin', 'local_assessfreq'),
- get_string('loggedin', 'local_assessfreq'),
- get_string('inprogress', 'local_assessfreq'),
- get_string('finished', 'local_assessfreq'),
- ];
-
- $colors = [
- get_config('local_assessfreq', 'notloggedincolor'),
- get_config('local_assessfreq', 'loggedincolor'),
- get_config('local_assessfreq', 'inprogresscolor'),
- get_config('local_assessfreq', 'finishedcolor'),
- ];
-
- // Create chart object.
- $chart = new \core\chart_pie();
- $chart->set_doughnut(true);
- $participants = new \core\chart_series(get_string('participants', 'local_assessfreq'), $seriesdata);
- $participants->set_colors($colors);
- $chart->add_series($participants);
- $chart->set_labels($labels);
- $chart->set_legend_options(['display' => false]);
-
- return $chart;
- }
-}
diff --git a/classes/output/quiz_user_table.php b/classes/output/quiz_user_table.php
deleted file mode 100644
index 9c8f44b5..00000000
--- a/classes/output/quiz_user_table.php
+++ /dev/null
@@ -1,329 +0,0 @@
-.
-
-/**
- * Renderable table for quiz dashboard users.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace local_assessfreq\output;
-
-defined('MOODLE_INTERNAL') || die;
-
-require_once($CFG->libdir . '/tablelib.php');
-
-use table_sql;
-use renderable;
-
-/**
- * Renderable table for quiz dashboard users.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class quiz_user_table extends table_sql implements renderable {
- use dashboard_table;
-
- /**
- * @var integer $quizid The ID of the braodcast to get the acknowledgements for.
- */
- private $quizid;
-
- /**
- *
- * @var integer $contextid The context id.
- */
- private $contextid;
-
- /**
- *
- * @var string $search The string to search for in the table data.
- */
- private $search;
-
- /**
- * @var string[] Extra fields to display.
- */
- protected $extrafields;
-
- /**
- * report_table constructor.
- *
- * @param string $baseurl Base URL of the page that contains the table.
- * @param int $quizid The id from the quiz table to get data for.
- * @param int $contextid The context id for the context the table is being displayed in.
- * @param string $search The string to search for in the table.
- * @param int $page the page number for pagination.
- *
- * @throws \coding_exception
- */
- public function __construct(string $baseurl, int $quizid, int $contextid, string $search, int $page = 0) {
- parent::__construct('local_assessfreq_student_table');
- global $DB;
-
- $this->quizid = $quizid;
- $this->contextid = $contextid;
- $this->search = $search;
- $this->set_attribute('id', 'local_assessfreq_ackreport_table');
- $this->set_attribute('class', 'generaltable generalbox');
- $this->downloadable = false;
- $this->define_baseurl($baseurl);
-
- $quizrecord = $DB->get_record('quiz', ['id' => $this->quizid], 'timeopen, timeclose, timelimit');
- $this->timeopen = $quizrecord->timeopen;
- $this->timeclose = $quizrecord->timeclose;
- $this->timelimit = $quizrecord->timelimit;
-
- $context = \context::instance_by_id($contextid);
-
- // Define the headers and columns.
- $headers = [];
- $columns = [];
-
- $headers[] = get_string('fullname');
- $columns[] = 'fullname';
-
- $extrafields = \core_user\fields::get_identity_fields($context, false);
- foreach ($extrafields as $field) {
- $headers[] = \core_user\fields::get_display_name($field);
- $columns[] = $field;
- }
-
- $this->define_columns(array_merge($columns, $this->get_common_columns()));
- $this->define_headers(array_merge($headers, $this->get_common_headers()));
- $this->extrafields = $extrafields;
-
- // Setup pagination.
- $this->currpage = $page;
- $this->sortable(true);
- $this->column_nosort = ['actions'];
- }
-
- /**
- * This function is used for the extra user fields.
- *
- * These are being dynamically added to the table so there are no functions 'col_' as
- * the list has the potential to increase in the future and we don't want to have to remember to add
- * a new method to this class. We also don't want to pollute this class with unnecessary methods.
- *
- * @param string $colname The column name
- * @param \stdClass $data
- * @return string
- */
- public function other_cols($colname, $data) {
- // Do not process if it is not a part of the extra fields.
- if (!in_array($colname, $this->extrafields)) {
- return '';
- }
-
- return s($data->{$colname});
- }
-
- /**
- * Get content for time open column.
- * Displays when the user attempt opens.
- *
- * @param \stdClass $row
- * @return string html used to display the field.
- */
- public function col_timeopen($row) {
- $datetime = userdate($row->timeopen, get_string('trenddatetime', 'local_assessfreq'));
-
- if ($row->timeopen != $this->timeopen) {
- $content = \html_writer::span($datetime, 'local-assessfreq-override-status');
- } else {
- $content = \html_writer::span($datetime);
- }
-
- return $content;
- }
-
- /**
- * Get content for time close column.
- * Displays when the user attempt closes.
- *
- * @param \stdClass $row
- * @return string html used to display the field.
- */
- public function col_timeclose($row) {
- $datetime = userdate($row->timeclose, get_string('trenddatetime', 'local_assessfreq'));
-
- if ($row->timeclose != $this->timeclose) {
- $content = \html_writer::span($datetime, 'local-assessfreq-override-status');
- } else {
- $content = \html_writer::span($datetime);
- }
-
- return $content;
- }
-
- /**
- * Get content for time limit column.
- * Displays the time the user has to finsih the quiz.
- *
- * @param \stdClass $row
- * @return string html used to display the field.
- */
- public function col_timelimit($row) {
- $timelimit = format_time($row->timelimit);
-
- if ($row->timelimit != $this->timelimit) {
- $content = \html_writer::span($timelimit, 'local-assessfreq-override-status');
- } else {
- $content = \html_writer::span($timelimit);
- }
-
- return $content;
- }
-
- /**
- * Get content for actions column.
- * Displays the actions for the user.
- *
- * @param \stdClass $row
- * @return string html used to display the field.
- */
- public function col_actions($row) {
- global $OUTPUT;
-
- $manage = '';
-
- $icon = $OUTPUT->render(new \pix_icon('i/duration', ''));
- $manage .= \html_writer::link('#', $icon, [
- 'class' => 'action-icon override',
- 'id' => 'tool-assessfreq-override-' . $row->id,
- 'data-toggle' => 'tooltip',
- 'data-placement' => 'top',
- 'title' => get_string('useroverride', 'local_assessfreq'),
- ]);
-
- $manage .= $this->get_common_column_actions($row);
-
- return $manage;
- }
-
-
- /**
- * Query the database for results to display in the table.
- *
- * @param int $pagesize size of page for paginated displayed table.
- * @param bool $useinitialsbar do you want to use the initials bar.
- */
- public function query_db($pagesize, $useinitialsbar = false) {
- global $CFG, $DB;
-
- $maxlifetime = $CFG->sessiontimeout;
- $timedout = time() - $maxlifetime;
- $sort = $this->get_sql_sort();
-
- // We never want initial bars. We are using a custom search.
- $this->initialbars(false);
-
- $frequency = new \local_assessfreq\frequency();
- $quiz = new \local_assessfreq\quiz();
- $capabilities = $frequency->get_module_capabilities('quiz');
- $context = $quiz->get_quiz_context($this->quizid);
-
- [$joins, $wheres, $params] = $frequency->generate_enrolled_wheres_joins_params($context, $capabilities);
- $attemptsql = 'SELECT qa_a.userid, qa_a.state, qa_a.quiz, qa_a.id as attemptid,
- qa_a.timestart as timestart, qa_a.timefinish as timefinish
- FROM {quiz_attempts} qa_a
- INNER JOIN (SELECT userid, MAX(timestart) as timestart
- FROM {quiz_attempts}
- GROUP BY userid) qa_b ON qa_a.userid = qa_b.userid
- AND qa_a.timestart = qa_b.timestart
- WHERE qa_a.quiz = :qaquiz';
-
- $sessionsql = 'SELECT DISTINCT (userid)
- FROM {sessions}
- 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 .= " LEFT JOIN ($sessionsql) us ON u.id = us.userid";
-
- $params['qaquiz'] = $this->quizid;
- $params['qoquiz'] = $this->quizid;
- $params['stm'] = $timedout;
-
- $finaljoin = new \core\dml\sql_join($joins, $wheres, $params);
- $params = $finaljoin->params;
-
- $sql = "SELECT u.*,
- COALESCE(qo.timeopen, $this->timeopen) AS timeopen,
- COALESCE(qo.timeclose, $this->timeclose) AS timeclose,
- COALESCE(qo.timelimit, $this->timelimit) AS timelimit,
- COALESCE(qa.state, (CASE
- WHEN us.userid > 0 THEN 'loggedin'
- ELSE 'notloggedin'
- END)) AS state,
- qa.attemptid,
- qa.timestart,
- qa.timefinish
- FROM {user} u
- $finaljoin->joins
- WHERE $finaljoin->wheres";
-
- $pagesize = get_user_preferences('local_assessfreq_quiz_table_rows_preference', 20);
-
- if (!empty($sort)) {
- $sql .= " ORDER BY $sort";
- }
-
- $records = $DB->get_recordset_sql($sql, $params);
- $data = [];
- $offset = $this->currpage * $pagesize;
- $offsetcount = 0;
- $recordcount = 0;
-
- foreach ($records as $record) {
- $searchcount = 0;
- if ($this->search != '') {
- // Because we are using COALESE and CASE for state we can't use SQL WHERE so we need to filter in PHP land.
- // Also because we need to do some filtering in PHP land, we'll do it all here.
- $searchcount = -1;
- $searchfields = array_merge($this->extrafields, ['firstname', 'lastname', 'state']);
-
- foreach ($searchfields as $searchfield) {
- if (stripos($record->{$searchfield}, $this->search) !== false) {
- $searchcount++;
- }
- }
- }
-
- if ($searchcount > -1 && $offsetcount >= $offset && $recordcount < $pagesize) {
- $data[$record->id] = $record;
- }
-
- if ($searchcount > -1 && $offsetcount >= $offset) {
- $recordcount++;
- }
-
- if ($searchcount > -1) {
- $offsetcount++;
- }
- }
-
- $records->close();
-
- $this->pagesize($pagesize, $offsetcount);
- $this->rawdata = $data;
- }
-}
diff --git a/classes/output/renderer.php b/classes/output/renderer.php
index a89d0365..d661d17b 100644
--- a/classes/output/renderer.php
+++ b/classes/output/renderer.php
@@ -15,463 +15,50 @@
// along with Moodle. If not, see .
/**
- * Assessment Frequency block rendrer.
+ * Renderer.
*
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package local_assessfreq
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
namespace local_assessfreq\output;
-use local_assessfreq\quiz;
+use local_assessfreq\form\course_search;
+use local_assessfreq\report_base;
use plugin_renderer_base;
-use local_assessfreq\frequency;
-/**
- * Assessment Frequency block rendrer.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
class renderer extends plugin_renderer_base {
/**
- * Render the html for the report cards.
- * Most content is loaded by ajax
+ * Render each of the assessfreqreport subplugins as tabs to display.
*
- * @return string html to display.
+ * @return void
*/
- public function render_report_cards(): string {
- $currentyear = date('Y');
- $preferenceyear = get_user_preferences('local_assessfreq_overview_year_preference', $currentyear);
- $frequency = new frequency();
-
- // Get years that have events and load into context.
- $years = $frequency->get_years_has_events();
-
- if (empty($years)) {
- $years = [$currentyear];
- }
-
- // Add current year to the selection of years if missing.
- if (!in_array($currentyear, $years)) {
- $years[] = $currentyear;
+ public function render_reports() : void {
+ $reports = get_reports();
+ $reportoutputs = [];
+ foreach ($reports as $report) {
+ /* @var $report report_base */
+ $reportoutputs[] = [
+ 'tablink' => $report->get_tablink(), // Plugin name.
+ 'tabname' => $report->get_name(), // Display name.
+ 'report' => $report->get_contents(),
+ 'weight' => $report->get_tab_weight(),
+ ];
}
-
- $context = ['years' => [], 'currentyear' => $preferenceyear];
-
- if (!empty($years)) {
- foreach ($years as $year) {
- if ($year == $preferenceyear) {
- $context['years'][] = ['year' => ['val' => $year, 'active' => 'true']];
- } else {
- $context['years'][] = ['year' => ['val' => $year]];
- }
- }
- } else {
- $context['years'][] = ['year' => ['val' => $preferenceyear, 'active' => 'true']];
- }
-
- return $this->render_from_template('local_assessfreq/report-cards', $context);
- }
-
- /**
- * Render the HTML for the student quiz table.
- *
- * @param string $baseurl the base url to render the table on.
- * @param int $quizid the id of the quiz in the quiz table.
- * @param int $contextid the id of the context the table is being called in.
- * @param string $search The string to search for.
- * @param int $page the page number for pagination.
- * @return string $output HTML for the table.
- */
- public function render_student_table(string $baseurl, int $quizid, int $contextid, string $search = '', int $page = 0): string {
- $renderable = new quiz_user_table($baseurl, $quizid, $contextid, $search, $page);
- $perpage = 50;
- ob_start();
- $renderable->out($perpage, true);
- $output = ob_get_contents();
- ob_end_clean();
-
- return $output;
- }
-
- /**
- * Render the HTML for the student search table.
- *
- * @param string $baseurl the base url to render the table on.
- * @param int $contextid the id of the context the table is being called in.
- * @param string $search The string to search for.
- * @param int $hoursahead Ammount of time in hours to look ahead for quizzes starting.
- * @param int $hoursbehind Ammount of time in hours to look behind for quizzes starting.
- * @param int $now The timestamp to use for the current time.
- * @param int $page the page number for pagination.
- * @return string $output HTML for the table.
- */
- public function render_student_search_table(
- string $baseurl,
- int $contextid,
- string $search,
- int $hoursahead,
- int $hoursbehind,
- int $now,
- int $page = 0
- ): string {
-
- $renderable = new student_search_table($baseurl, $contextid, $search, $hoursahead, $hoursbehind, $now, $page);
- $perpage = 50;
-
- ob_start();
- $renderable->out($perpage, true);
- $output = ob_get_contents();
- ob_end_clean();
-
- return $output;
- }
-
- /**
- * Renders the quizzes in progress "table" on the quiz dashboard screen.
- * We update the table via ajax.
- * The table isn't a real table it's a collection of divs.
- *
- * @param string $search The search string for the table.
- * @param int $page The page number of results.
- * @param string $sorton The value to sort the quizzes by.
- * @param string $direction The direction to sort the quizzes.
- * @param int $hoursahead Amount of time in hours to look ahead for quizzes starting.
- * @param int $hoursbehind Amount of time in hours to look behind for quizzes starting.
- * @return string $output HTML for the table.
- */
- public function render_quizzes_inprogress_table(
- string $search,
- int $page,
- string $sorton,
- string $direction,
- int $hoursahead = 0,
- int $hoursbehind = 0
- ): string {
- $context = \context_system::instance(); // TODO: pass the actual context in from the caller.
- $now = time();
- $quiz = new quiz($hoursahead, $hoursbehind);
- $quizzes = $quiz->get_quiz_summaries($now);
- $pagesize = get_user_preferences('local_assessfreq_quiz_table_inprogress_preference', 5);
-
- $inprogressquizzes = $quizzes['inprogress'];
- $upcommingquizzes = $quizzes['upcomming'];
- $finishedquizzes = $quizzes['finished'];
-
- foreach ($upcommingquizzes as $key => $upcommingquiz) {
- foreach ($upcommingquiz as $keyupcomming => $upcomming) {
- $inprogressquizzes[$keyupcomming] = $upcomming;
- }
- }
-
- foreach ($finishedquizzes as $key => $finishedquiz) {
- foreach ($finishedquiz as $keyfinished => $finished) {
- $inprogressquizzes[$keyfinished] = $finished;
- }
- }
-
- [$filtered, $totalrows] = $quiz->filter_quizzes($inprogressquizzes, $search, $page, $pagesize);
- $sortedquizzes = \local_assessfreq\utils::sort($filtered, $sorton, $direction);
-
- $pagingbar = new \paging_bar($totalrows, $page, $pagesize, '/');
- $pagingoutput = $this->render($pagingbar);
-
- $context = [
- 'quizzes' => array_values($sortedquizzes),
- 'quizids' => json_encode(array_keys($sortedquizzes)),
- 'context' => $context->id,
- 'pagingbar' => $pagingoutput,
- ];
-
- $output = $this->render_from_template('local_assessfreq/quiz-inprogress-summary', $context);
-
- return $output;
- }
-
- /**
- * Return heatmap HTML.
- *
- * @return string The heatmap HTML.
- */
- public function render_report_heatmap(): string {
- $currentyear = date('Y');
- $preferenceyear = get_user_preferences('local_assessfreq_heatmap_year_preference', $currentyear);
- $preferencemetric = get_user_preferences('local_assessfreq_heatmap_metric_preference', 'assess');
- $preferencemodules = json_decode(get_user_preferences('local_assessfreq_heatmap_modules_preference', '["all"]'), true);
-
- $frequency = new frequency();
-
- // Initial context setup.
- $context = [
- 'years' => [],
- 'currentyear' => $preferenceyear,
- 'modules' => [],
- 'metrics' => [],
- 'sesskey' => sesskey(),
- 'downloadmetric' => $preferencemetric,
- ];
-
- // Get years that have events and load into context.
- $years = $frequency->get_years_has_events();
-
- if (empty($years)) {
- $years = [$currentyear];
- }
-
- // Add current year to the selection of years if missing.
- if (!in_array($currentyear, $years)) {
- $years[] = $currentyear;
- }
-
- if (!empty($years)) {
- foreach ($years as $year) {
- if ($year == $preferenceyear) {
- $context['years'][] = ['year' => ['val' => $year, 'active' => 'true']];
- $context['downloadyear'] = $year;
- } else {
- $context['years'][] = ['year' => ['val' => $year]];
- }
- }
- } else {
- $context['years'][] = ['year' => ['val' => $preferenceyear, 'active' => 'true']];
- $context['downloadyear'] = $preferenceyear;
- }
-
- // Get modules for filters and load into context.
- $modules = $frequency->get_process_modules();
- if (empty($preferencemodules) || $preferencemodules === ['all']) {
- $context['modules'][] = ['module' => ['val' => 'all', 'name' => get_string('all'), 'active' => 'true']];
- } else {
- $context['modules'][] = ['module' => ['val' => 'all', 'name' => get_string('all')]];
- }
-
- if (!empty($modules[0])) {
- foreach ($modules as $module) {
- $modulename = get_string('modulename', $module);
- if (in_array($module, $preferencemodules)) {
- $context['modules'][] = ['module' => ['val' => $module, 'name' => $modulename, 'active' => 'true']];
- } else {
- $context['modules'][] = ['module' => ['val' => $module, 'name' => $modulename]];
- }
- }
- }
-
- // Get metric details and load into context.
- $context['metrics'] = [$preferencemetric => 'true'];
-
- return $this->render_from_template('local_assessfreq/report-heatmap', $context);
- }
-
- /**
- * Get the html to render the assessment dashboard.
- *
- * @param string $baseurl the base url to render this report on.
- * @return string $html the html to display.
- */
- public function render_dashboard_assessment(string $baseurl): string {
- $html = '';
- $html .= $this->header();
- $html .= $this->render_report_cards();
- $html .= $this->render_report_heatmap();
- $html .= $this->footer();
-
- return $html;
- }
-
- /**
- * Add HTML for quiz selection and quiz refresh buttons.
- *
- * @return string html for the button.
- */
- private function render_quiz_select_refresh_button(): string {
- $preferencerefresh = get_user_preferences('local_assessfreq_quiz_refresh_preference', 60);
- $refreshminutes = [
- 60 => 'minuteone',
- 120 => 'minutetwo',
- 300 => 'minutefive',
- 600 => 'minuteten',
- ];
-
- $context = [
- 'refreshinitial' => get_string($refreshminutes[$preferencerefresh], 'local_assessfreq'),
- 'refresh' => [$refreshminutes[$preferencerefresh] => 'true'],
- 'hide' => true,
- ];
-
- return $this->render_from_template('local_assessfreq/quiz-dashboard-controls', $context);
- }
-
- /**
- * Add HTML for quiz refresh button.
- *
- * @return string html for the button.
- */
- private function render_quiz_refresh_button(): string {
- $preferencerefresh = get_user_preferences('local_assessfreq_quiz_refresh_preference', 60);
- $preferencehoursahead = get_user_preferences('local_assessfreq_quizzes_inprogress_table_hoursahead_preference', 0);
- $preferencehoursbehind = get_user_preferences('local_assessfreq_quizzes_inprogress_table_hoursbehind_preference', 0);
-
- $refreshminutes = [
- 60 => 'minuteone',
- 120 => 'minutetwo',
- 300 => 'minutefive',
- 600 => 'minuteten',
- ];
-
- $hours = [
- 0 => 'hours0',
- 1 => 'hours1',
- 4 => 'hours4',
- 8 => 'hours8',
- ];
-
- $context = [
- 'refreshinitial' => get_string($refreshminutes[$preferencerefresh], 'local_assessfreq'),
- 'refresh' => [$refreshminutes[$preferencerefresh] => 'true'],
- 'hoursahead' => [$hours[$preferencehoursahead] => 'true'],
- 'hoursbehind' => [$hours[$preferencehoursbehind] => 'true'],
- ];
-
- return $this->render_from_template('local_assessfreq/quiz-dashboard-inprogress-controls', $context);
- }
-
- /**
- * Render the cards on the quiz dashboard.
- *
- * @return string
- */
- private function render_quiz_dashboard_cards(): string {
- $preferencerows = get_user_preferences('local_assessfreq_quiz_table_rows_preference', 20);
- $rows = [
- 20 => 'rows20',
- 50 => 'rows50',
- 100 => 'rows100',
- ];
-
- $context = [
- 'rows' => [$rows[$preferencerows] => 'true'],
- ];
-
- return $this->render_from_template('local_assessfreq/quiz-dashboard-cards', $context);
- }
-
- /**
- * Render the cards on the quiz dashboard.
- *
- * @return string
- */
- private function render_quiz_dashboard_inprogress_cards(): string {
- $preferencerows = get_user_preferences('local_assessfreq_quiz_table_inprogress_preference', 10);
- $preferencesort = get_user_preferences('local_assessfreq_quiz_table_inprogress_sort_preference', 'name_asc');
- $rows = [
- 5 => 'rows5',
- 10 => 'rows10',
- 20 => 'rows20',
- ];
-
- $context = [
- 'rows' => [$rows[$preferencerows] => 'true'],
- 'sort' => [$preferencesort => 'true'],
- ];
-
- return $this->render_from_template('local_assessfreq/quiz-dashboard-inprogress-cards', $context);
- }
-
- /**
- * Render the cards on the quiz dashboard.
- *
- * @return string
- */
- private function render_student_table_cards(): string {
- $preferencerows = get_user_preferences('local_assessfreq_student_search_table_rows_preference', 20);
- $preferencehoursahead = get_user_preferences('local_assessfreq_student_search_table_hoursahead_preference', 4);
- $preferencehoursbehind = get_user_preferences('local_assessfreq_student_search_table_hoursbehind_preference', 1);
- $preferencerefresh = get_user_preferences('local_assessfreq_quiz_refresh_preference', 60);
-
- $refreshminutes = [
- 60 => 'minuteone',
- 120 => 'minutetwo',
- 300 => 'minutefive',
- 600 => 'minuteten',
- ];
-
- $rows = [
- 20 => 'rows20',
- 50 => 'rows50',
- 100 => 'rows100',
- ];
-
- $hours = [
- 0 => 'hours0',
- 1 => 'hours1',
- 4 => 'hours4',
- 8 => 'hours8',
- ];
-
- $preferencerefresh = get_user_preferences('local_assessfreq_quiz_refresh_preference', 60);
- $refreshminutes = [
- 60 => 'minuteone',
- 120 => 'minutetwo',
- 300 => 'minutefive',
- 600 => 'minuteten',
- ];
-
- $context = [
- 'rows' => [$rows[$preferencerows] => 'true'],
- 'hoursahead' => [$hours[$preferencehoursahead] => 'true'],
- 'hoursbehind' => [$hours[$preferencehoursbehind] => 'true'],
- 'refreshinitial' => get_string($refreshminutes[$preferencerefresh], 'local_assessfreq'),
- 'refresh' => [$refreshminutes[$preferencerefresh] => 'true'],
- ];
-
- return $this->render_from_template('local_assessfreq/student-search', $context);
- }
-
- /**
- * Get the html to render the quiz dashboard.
- *
- * @param string $baseurl the base url to render this report on.
- * @return string $html the html to display.
- */
- public function render_dashboard_quiz(string $baseurl): string {
- $html = '';
- $html .= $this->header();
- $html .= $this->render_quiz_select_refresh_button();
- $html .= $this->render_quiz_dashboard_cards();
- $html .= $this->footer();
-
- return $html;
- }
-
- /**
- * Get the html to render the quizzes in porgress dashboard.
- *
- * @param string $baseurl the base url to render this report on.
- * @return string $html the html to display.
- */
- public function render_dashboard_quiz_inprogress(string $baseurl): string {
- $html = '';
- $html .= $this->header();
- $html .= $this->render_quiz_refresh_button();
- $html .= $this->render_quiz_dashboard_inprogress_cards();
- $html .= $this->footer();
-
- return $html;
- }
-
- /**
- * Get the html to render the student search.
- *
- * @return string $html the html to display.
- */
- public function render_student_search(): string {
- $html = '';
- $html .= $this->header();
- $html .= $this->render_student_table_cards();
- $html .= $this->footer();
-
- return $html;
+ usort($reportoutputs, function($a, $b) {
+ return $a['weight'] <=> $b['weight'];
+ });
+
+ $output = $this->output->header();
+ $output .= $this->render_from_template(
+ 'local_assessfreq/index',
+ [
+ 'reports' => $reportoutputs
+ ]
+ );
+ $output .= $this->output->footer();
+ echo $output;
}
}
diff --git a/classes/output/upcomming_quizzes.php b/classes/output/upcomming_quizzes.php
deleted file mode 100644
index d8925917..00000000
--- a/classes/output/upcomming_quizzes.php
+++ /dev/null
@@ -1,93 +0,0 @@
-.
-
-/**
- * Renderable for upcomming quizzes card.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace local_assessfreq\output;
-
-use local_assessfreq\quiz;
-
-/**
- * Renderable for upcomming quizzes card.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class upcomming_quizzes {
- /**
- * Generate the markup for the upcomming quizzes chart,
- * used in the in progress quizzes dashboard.
- *
- * @param int $now Timestamp to get chart data for.
- * @return array With Generated chart object and chart data status.
- */
- public function get_upcomming_quizzes_chart(int $now): array {
-
- // Get quizzes for the supplied timestamp.
- $quiz = new quiz();
- $quizzes = $quiz->get_quiz_summaries($now);
-
- $labels = [];
- $quizseriestitle = get_string('quizzes', 'local_assessfreq');
- $participantseries = get_string('students', 'local_assessfreq');
- $result = [];
- $result['hasdata'] = true;
-
- $quizseriesdata = [];
- $participantseriesdata = [];
-
- foreach ($quizzes['upcomming'] as $timestamp => $upcomming) {
- $quizcount = 0;
- $participantcount = 0;
-
- foreach ($upcomming as $quiz) {
- $quizcount++;
- $participantcount += $quiz->participants;
- }
-
- // Check if inprogress quizzes are upcomming quizzes with overrides.
- foreach ($quizzes['inprogress'] as $inprogress) {
- if ($inprogress->timestampopen >= $timestamp && $inprogress->timestampopen < $timestamp + HOURSECS) {
- $quizcount++;
- $participantcount += $inprogress->participants;
- }
- }
-
- $quizseriesdata[] = $quizcount;
- $participantseriesdata[] = $participantcount;
- $labels[] = userdate($timestamp + HOURSECS, get_string('inprogressdatetime', 'local_assessfreq'));
- }
-
- // Create chart object.
- $quizseries = new \core\chart_series($quizseriestitle, $quizseriesdata);
- $participantseries = new \core\chart_series($participantseries, $participantseriesdata);
-
- $chart = new \core\chart_bar();
- $chart->add_series($quizseries);
- $chart->add_series($participantseries);
- $chart->set_labels($labels);
- $result['chart'] = $chart;
-
- return $result;
- }
-}
diff --git a/classes/plugininfo/assessfreqreport.php b/classes/plugininfo/assessfreqreport.php
new file mode 100644
index 00000000..f61aaf31
--- /dev/null
+++ b/classes/plugininfo/assessfreqreport.php
@@ -0,0 +1,100 @@
+.
+
+/**
+ * Report plugininfo.
+ *
+ * @package local_assessfreq
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace local_assessfreq\plugininfo;
+
+use admin_settingpage;
+use core\plugininfo\base;
+use moodle_url;
+use part_of_admin_tree;
+
+class assessfreqreport extends base {
+
+ /**
+ * Finds all enabled plugin names, the result may include missing plugins.
+ * @return array of enabled plugins $pluginname=>$pluginname, null means unknown
+ */
+ public static function get_enabled_plugins() : array {
+ $pluginmanager = \core_plugin_manager::instance();
+ $plugins = $pluginmanager->get_plugins_of_type('assessfreqreport');
+
+ if (empty($plugins)) {
+ return array();
+ }
+
+ $enabled = [];
+ foreach ($plugins as $name => $plugin) {
+ if ($plugin->is_enabled()) {
+ $enabled[$name] = $name;
+ }
+ }
+ return $enabled;
+ }
+
+ /**
+ * Whether the subplugin is enabled.
+ *
+ * @return bool Whether enabled.
+ */
+ public function is_enabled() : bool {
+ return get_config('assessfreqreport_' . $this->name, 'enabled');
+ }
+
+ /**
+ * Returns the node name used in admin settings menu for this plugin settings (if applicable)
+ *
+ * @return string node name or null if plugin does not create settings node (default)
+ */
+ public function get_settings_section_name(): string {
+ return 'assessfreqreport_' . $this->name;
+ }
+
+ /**
+ * Include the settings.php file from sub plugins if they provide it.
+ * This is a copy of very similar implementations from various other subplugin areas.
+ */
+ public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
+ global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them.
+ $ADMIN = $adminroot; // May be used in settings.php.
+ $plugininfo = $this; // Also can be used inside settings.php.
+
+ if (!$this->is_installed_and_upgraded()) {
+ return;
+ }
+
+ if (!$hassiteconfig || !file_exists($this->full_path('settings.php'))) {
+ return;
+ }
+
+ $section = $this->get_settings_section_name();
+ $settings = new admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false);
+ include($this->full_path('settings.php')); // This may also set $settings to null.
+
+ if ($settings) {
+ $ADMIN->add($parentnodename, $settings);
+ }
+ }
+}
+
diff --git a/classes/plugininfo/assessfreqsource.php b/classes/plugininfo/assessfreqsource.php
new file mode 100644
index 00000000..f1c4e448
--- /dev/null
+++ b/classes/plugininfo/assessfreqsource.php
@@ -0,0 +1,99 @@
+.
+
+/**
+ * Source plugininfo.
+ *
+ * @package local_assessfreq
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace local_assessfreq\plugininfo;
+
+use admin_settingpage;
+use core\plugininfo\base;
+use part_of_admin_tree;
+
+class assessfreqsource extends base {
+
+ /**
+ * Finds all enabled plugin names, the result may include missing plugins.
+ * @return array of enabled plugins $pluginname=>$pluginname, null means unknown
+ */
+ public static function get_enabled_plugins() : array {
+ $pluginmanager = \core_plugin_manager::instance();
+ $plugins = $pluginmanager->get_plugins_of_type('assessfreqsource');
+
+ if (empty($plugins)) {
+ return array();
+ }
+
+ $enabled = [];
+ foreach ($plugins as $name => $plugin) {
+ if ($plugin->is_enabled()) {
+ $enabled[$name] = $name;
+ }
+ }
+ return $enabled;
+ }
+
+ /**
+ * Whether the subplugin is enabled.
+ *
+ * @return bool Whether enabled.
+ */
+ public function is_enabled() : bool {
+ return get_config('assessfreqsource_' . $this->name, 'enabled');
+ }
+
+ /**
+ * Returns the node name used in admin settings menu for this plugin settings (if applicable)
+ *
+ * @return string node name or null if plugin does not create settings node (default)
+ */
+ public function get_settings_section_name() : string {
+ return 'assessfreqsource_' . $this->name;
+ }
+
+ /**
+ * Include the settings.php file from sub plugins if they provide it.
+ * This is a copy of very similar implementations from various other subplugin areas.
+ */
+ public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
+ global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them.
+ $ADMIN = $adminroot; // May be used in settings.php.
+ $plugininfo = $this; // Also can be used inside settings.php.
+
+ if (!$this->is_installed_and_upgraded()) {
+ return;
+ }
+
+ if (!$hassiteconfig || !file_exists($this->full_path('settings.php'))) {
+ return;
+ }
+
+ $section = $this->get_settings_section_name();
+ $settings = new admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false);
+ include($this->full_path('settings.php')); // This may also set $settings to null.
+
+ if ($settings) {
+ $ADMIN->add($parentnodename, $settings);
+ }
+ }
+}
+
diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php
index 3d44b2cd..917da0ec 100644
--- a/classes/privacy/provider.php
+++ b/classes/privacy/provider.php
@@ -24,10 +24,12 @@
namespace local_assessfreq\privacy;
+use context;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
+use core_privacy\local\request\data_provider;
use core_privacy\local\request\writer;
use core_privacy\local\request\userlist;
@@ -38,7 +40,7 @@
* @copyright 2020 Matt Porritt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class provider implements \core_privacy\local\request\data_provider, \core_privacy\local\metadata\provider {
+class provider implements data_provider, \core_privacy\local\metadata\provider {
/**
* Returns metadata about this plugin's privacy policy.
*
@@ -75,7 +77,7 @@ public static function get_metadata(collection $collection): collection {
* @return contextlist the contexts in which data is contained.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
- $contextlist = new \core_privacy\local\request\contextlist();
+ $contextlist = new contextlist();
$contextlist->add_user_context($userid);
$contextlist->add_system_context();
return $contextlist;
@@ -118,8 +120,8 @@ public static function export_user_data(approved_contextlist $contextlist) {
// Get records for user ID.
$rows = $DB->get_records('local_assessfreq_user', ['userid' => $userid]);
+ $i = 0;
if (count($rows) > 0) {
- $i = 0;
foreach ($rows as $row) {
$parentclass[$i]['userid'] = $row->userid;
$parentclass[$i]['eventid'] = $row->eventid;
@@ -150,7 +152,7 @@ public static function export_user_data(approved_contextlist $contextlist) {
*
* @param context $context The context to delete for.
*/
- public static function delete_data_for_all_users_in_context(\context $context) {
+ public static function delete_data_for_all_users_in_context(context $context) {
global $DB;
// All data contained in system context.
if ($context->contextlevel == CONTEXT_SYSTEM) {
diff --git a/classes/quiz.php b/classes/quiz.php
deleted file mode 100644
index b85488a8..00000000
--- a/classes/quiz.php
+++ /dev/null
@@ -1,773 +0,0 @@
-.
-
-/**
- * Quiz data class.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace local_assessfreq;
-
-use mod_quiz\question\bank\qbank_helper;
-
-/**
- * Quiz data class.
- *
- * This class handles data processing to get quiz data.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class quiz {
- /**
- * Ammount of time in hours for lookahead values.
- * Defaults to 12.
- *
- * @var int $hoursahead.
- */
- private $hoursahead = 12;
-
- /**
- * Ammount of time in hours for lookbehind values.
- * Defaults to 1.
- *
- * @var int $hoursahead.
- */
- private $hoursbehind = 1;
-
- /**
- * The direction used in sorting.
- *
- * @var string $sortdirection
- */
- private $sortdirection;
-
- /**
- * The quiz details to sort by.
- *
- * @var string $sorton
- */
- private $sorton;
-
- /**
- * Class constructor.
- *
- * @param int $hoursahead
- * @param int $hoursbehind
- */
- public function __construct(int $hoursahead = 12, int $hoursbehind = 1) {
- $this->hoursahead = $hoursahead;
- $this->hoursbehind = $hoursbehind;
- }
-
- /**
- * Given a quiz id get the module context.
- *
- * @param int $quizid The quiz ID of the context to get.
- * @return \context_module $context The quiz module context.
- */
- public function get_quiz_context(int $quizid): \context_module {
- global $DB;
-
- $params = ['module' => 'quiz', 'quiz' => $quizid];
- $sql = 'SELECT cm.id
- FROM {course_modules} cm
- INNER JOIN {modules} m ON cm.module = m.id
- INNER JOIN {quiz} q ON cm.instance = q.id AND cm.course = q.course
- WHERE m.name = :module
- AND q.id = :quiz';
- $cmid = $DB->get_field_sql($sql, $params);
- $context = \context_module::instance($cmid);
-
- return $context;
- }
-
- /**
- * Get override info for a paricular quiz.
- * Data returned is:
- * Number of users with overrides in Quiz,
- * Ealiest override start,
- * Latest override end.
- *
- * @param int $quizid The ID of the quiz to get override data for.
- * @param \context_module $context The context object of the quiz.
- * @return \stdClass $overrideinfo Information about quiz overrides.
- */
- private function get_quiz_override_info(int $quizid, \context_module $context): \stdClass {
- global $DB;
-
- $capabilities = ['mod/quiz:attempt', 'mod/quiz:view'];
- $overrideinfo = new \stdClass();
- $users = [];
- $start = 0;
- $end = 0;
-
- $sql = 'SELECT id, userid, COALESCE(timeopen, 0) AS timeopen, COALESCE(timeclose, 0) AS timeclose
- FROM {quiz_overrides}
- WHERE quiz = ?';
- $params = [$quizid];
- $overrides = $DB->get_records_sql($sql, $params);
-
- foreach ($overrides as $override) {
- if (!has_all_capabilities($capabilities, $context, $override->userid)) {
- continue; // Don't count users who can't access the quiz.
- }
-
- $users[] = $override->userid;
-
- if ($override->timeclose > $end) {
- $end = $override->timeclose;
- }
-
- if ($start == 0) {
- $start = $override->timeopen;
- } else if ($override->timeopen < $start) {
- $start = $override->timeopen;
- }
- }
-
- $users = count(array_unique($users));
-
- $overrideinfo->start = $start;
- $overrideinfo->end = $end;
- $overrideinfo->users = $users;
-
- return $overrideinfo;
- }
-
- /**
- * Get quiz question infromation.
- * Data returned is:
- * List of individual question types,
- * Count of questions in quiz,
- * Count of question types.
- *
- * @param int $quizid The ID of the quiz to get override data for.
- * @return \stdClass $questions The question data for the quiz.
- */
- private function get_quiz_questions(int $quizid): \stdClass {
- global $DB;
- $questions = new \stdClass();
- $types = [];
- $questioncount = 0;
- $context = $this->get_quiz_context($quizid);
-
- $questionsrecords = qbank_helper::get_question_structure($quizid, $context);
-
- foreach ($questionsrecords as $questionrecord) {
- $types[] = get_string('pluginname', 'qtype_' . $questionrecord->qtype);
- $questioncount++;
- }
-
- $typeswithcounts = [];
- foreach (array_count_values($types) as $type => $count) {
- $typeswithcounts[] = ['type' => $type, 'count' => $count];
- }
-
- $questions->types = $typeswithcounts;
- $questions->typecount = count($typeswithcounts);
- $questions->questioncount = $questioncount;
-
- return $questions;
- }
-
- /**
- * Method returns data about a quiz.
- * Data returned is:
- * Quiz name,
- * Quiz start time,
- * Quiz end time,
- * Earliest participant start time (override),
- * Latest participant end time (override),
- * Total participants taking the quiz,
- * Number participants with overrides in quiz,
- * Quiz link,
- * Number of questions,
- * Number of question types,
- * List of question types.
- *
- * @param int $quizid ID of the quiz to get data for.
- * @return \stdClass $quizdata The retrieved quiz data.
- */
- public function get_quiz_data(int $quizid): \stdClass {
- global $DB;
- $quizdata = new \stdClass();
- $context = $this->get_quiz_context($quizid);
-
- $quizrecord = $DB->get_record('quiz', ['id' => $quizid], 'name, timeopen, timeclose, timelimit, course');
- $course = get_course($quizrecord->course);
- $courseurl = new \moodle_url('/course/view.php', ['id' => $quizrecord->course]);
-
- $overrideinfo = $this->get_quiz_override_info($quizid, $context);
- $questions = $this->get_quiz_questions($quizid);
- $frequency = new frequency();
- if (!empty($quizrecord->timeopen)) {
- $timesopen = userdate($quizrecord->timeopen, get_string('strftimedatetime', 'langconfig'));
- } else {
- $timesopen = get_string('na', 'local_assessfreq');
- }
- if (!empty($quizrecord->timeclose)) {
- $timeclose = userdate($quizrecord->timeclose, get_string('strftimedatetime', 'langconfig'));
- } else {
- $timeclose = get_string('na', 'local_assessfreq');
- }
- if (!empty($overrideinfo->start)) {
- $overrideinfostart = userdate($overrideinfo->start, get_string('strftimedatetime', 'langconfig'));
- } else {
- $overrideinfostart = get_string('na', 'local_assessfreq');
- }
- if (!empty($overrideinfo->end)) {
- $overrideinfoend = userdate($overrideinfo->end, get_string('strftimedatetime', 'langconfig'));
- } else {
- $overrideinfoend = get_string('na', 'local_assessfreq');
- }
-
- // Handle override start.
- if ($overrideinfo->start != 0 && $overrideinfo->start < $quizrecord->timeopen) {
- $earlyopen = $overrideinfostart;
- $earlyopenstamp = $overrideinfo->start;
- } else {
- $earlyopen = $timesopen;
- $earlyopenstamp = $quizrecord->timeopen;
- }
-
- // Handle override end.
- if ($overrideinfo->end != 0 && $overrideinfo->end > $quizrecord->timeclose) {
- $lateclose = $overrideinfoend;
- $lateclosestamp = $overrideinfo->end;
- } else {
- $lateclose = $timeclose;
- $lateclosestamp = $quizrecord->timeclose;
- }
-
- // Quiz result link.
- $resultlink = new \moodle_url('/mod/quiz/report.php', ['id' => $context->instanceid, 'mode' => 'overview']);
- // Override link.
- $overrridelink = new \moodle_url('/mod/quiz/overrides.php', ['cmid' => $context->instanceid, 'mode' => 'user']);
- // Participant link.
- $participantlink = new \moodle_url('/user/index.php', ['id' => $quizrecord->course]);
- // Dashboard link.
- $dashboardlink = new \moodle_url('/local/assessfreq/dashboard_quiz.php', ['id' => $quizid]);
-
- $quizdata->name = format_string($quizrecord->name, true, ["context" => $context, "escape" => true]);
- $quizdata->timeopen = $timesopen;
- $quizdata->timeclose = $timeclose;
- $quizdata->timelimit = format_time($quizrecord->timelimit);
- $quizdata->earlyopen = $earlyopen;
- $quizdata->earlyopenstamp = $earlyopenstamp;
- $quizdata->lateclose = $lateclose;
- $quizdata->lateclosestamp = $lateclosestamp;
- $quizdata->participants = count($frequency->get_event_users_raw($context->id, 'quiz'));
- $quizdata->overrideparticipants = $overrideinfo->users;
- $quizdata->url = $context->get_url()->out(false);
- $quizdata->types = $questions->types;
- $quizdata->typecount = $questions->typecount;
- $quizdata->questioncount = $questions->questioncount;
- $quizdata->resultlink = $resultlink->out(false);
- $quizdata->overridelink = $overrridelink->out(false);
- $quizdata->coursefullname = format_string($course->fullname, true, ["context" => $context, "escape" => true]);
- $quizdata->courseshortname = $course->shortname;
- $quizdata->courselink = $courseurl->out(false);
- $quizdata->participantlink = $participantlink->out(false);
- $quizdata->dashboardlink = $dashboardlink->out(false);
- $quizdata->assessid = $quizid;
-
- return $quizdata;
- }
-
- /**
- * Get a list of all quiz overrides that have a start date less than now + 1 hour
- * AND end date is in the future OR end date is less then 1 hour in the past.
- * And startdate != 0.
- *
- * @param int $now Timestamp to use for reference for time.
- * @param int $lookahead The number of seconds from the provided now value to look ahead when getting overrides.
- * @param int $lookbehind The number of seconds from the provided now value to look behind when getting overrides.
- * @return array $quizzes The quizzes with applicable overrides.
- */
- private function get_tracked_overrides(int $now, int $lookahead, int $lookbehind): array {
- global $DB;
-
- $starttime = $now + $lookahead;
- $endtime = $now - $lookbehind;
-
- $sql = 'SELECT id, quiz, userid, timeopen, timeclose
- FROM {quiz_overrides}
- WHERE (timeopen > 0 AND timeopen < :starttime)
- AND (timeclose > :endtime OR timeclose > :now)';
- $params = [
- 'starttime' => $starttime,
- 'endtime' => $endtime,
- 'now' => $now,
- ];
-
- $quizzes = $DB->get_records_sql($sql, $params);
-
- return $quizzes;
- }
-
- /**
- * Get a list of all quizzes that have a start date less than now + 1 hour
- * AND end date is in the future OR end date is less then 1 hour in the past.
- * And startdate != 0.
- *
- * @param int $now Timestamp to use for reference for time.
- * @param int $lookahead The number of seconds from the provided now value to look ahead when getting quizzes.
- * @param int $lookbehind The number of seconds from the provided now value to look behind when getting quizzes.
- * @return array $quizzes The quizzes.
- */
- private function get_tracked_quizzes(int $now, int $lookahead, int $lookbehind): array {
- global $DB;
-
- $starttime = $now + $lookahead;
- $endtime = $now - $lookbehind;
-
- $sql = 'SELECT id, timeopen, timeclose, timelimit, 0 AS isoverride
- FROM {quiz}
- WHERE (timeopen > 0 AND timeopen < :starttime)
- AND (timeclose > :endtime OR timeclose > :now)';
- $params = [
- 'starttime' => $starttime,
- 'endtime' => $endtime,
- 'now' => $now,
- ];
-
- $quizzes = $DB->get_records_sql($sql, $params);
-
- return $quizzes;
- }
-
- /**
- * Get a list of all quizzes that have a start date less than now + 1 hour
- * AND end date is in the future OR end date is less then 1 hour in the past.
- * And startdate != 0. With quiz start and end times adjusted to take into account users with overrides.
- *
- * @param int $now Timestamp to use for reference for time.
- * @param int $lookahead The number of seconds from the provided now value to look ahead when getting quizzes.
- * @param int $lookbehind The number of seconds from the provided now value to look behind when getting quizzes.
- * @return array $quizzes The quizzes.
- */
- private function get_tracked_quizzes_with_overrides(int $now, int $lookahead = HOURSECS, int $lookbehind = HOURSECS): array {
- global $DB;
-
- $quizzes = $this->get_tracked_quizzes($now, $lookahead, $lookbehind);
- $overrides = $this->get_tracked_overrides($now, $lookahead, $lookbehind);
-
- // Add override data to each quiz in the array.
- foreach ($overrides as $override) {
- $sql = 'SELECT id, timeopen, timeclose, timelimit
- FROM {quiz}
- WHERE id = :id';
- $params = [
- 'id' => $override->quiz,
- ];
-
- $quizzesoverride = $DB->get_record_sql($sql, $params);
-
- if ($quizzesoverride) {
- if (array_key_exists($quizzesoverride->id, $quizzes)) {
- $quizzesoverride->isoverride = $quizzes[$quizzesoverride->id]->isoverride;
- if (isset($quizzes[$quizzesoverride->id]->overrides)) {
- $quizzesoverride->overrides = $quizzes[$quizzesoverride->id]->overrides;
- }
- $quizzesoverride->overrides[] = $override;
- $quizzes[$quizzesoverride->id] = $quizzesoverride;
- } else {
- $quizzesoverride->isoverride = 1;
- $quizzesoverride->overrides[] = $override;
- $quizzes[$quizzesoverride->id] = $quizzesoverride;
- }
- }
- }
-
- return $quizzes;
- }
-
- /**
- * Get counts for inprogress assessments, both total in progress quiz activities
- * and total participants in progress.
- *
- * @param int $now Timestamp to use for reference for time.
- * @return array $quizzes Array of counts of inprogress assessments and participants.
- */
- public function get_inprogress_counts(int $now): array {
- // Get tracked quizzes.
- $trackedquizzes = $this->get_tracked_quizzes_with_overrides($now, 0, 0);
-
- $counts = [
- 'assessments' => 0,
- 'participants' => 0,
- ];
-
- foreach ($trackedquizzes as $quiz) {
- $counts['assessments']++;
-
- // Get tracked users for quiz.
- $trackedrecords = $this->get_quiz_tracking($quiz->id);
- if (!empty($trackedrecords)) {
- $tracking = array_pop($trackedrecords);
- $counts['participants'] += $tracking->inprogress;
- }
- }
-
- return $counts;
- }
-
- /**
- * Get finished, in progress and upcomming quizzes and their associated data.
- *
- * @param int $now Timestamp to use for reference for time.
- * @return array $quizzes Array of finished, inprogress and upcomming quizzes with associated data.
- */
- public function get_quiz_summaries(int $now): array {
- // Get tracked quizzes.
- $lookahead = HOURSECS * $this->hoursahead;
- $lookbehind = HOURSECS * $this->hoursbehind;
- $trackedquizzes = $this->get_tracked_quizzes_with_overrides($now, $lookahead, $lookbehind);
-
- // Set up array to hold quizzes and data.
- $quizzes = [
- 'finished' => [],
- 'inprogress' => [],
- 'upcomming' => [],
- ];
-
- // Itterate through the hours, processing in progress and upcomming quizzes.
- for ($hour = 0; $hour <= $this->hoursahead; $hour++) {
- $time = $now + (HOURSECS * $hour);
-
- if ($hour == 0) {
- $quizzes['inprogress'] = [];
- }
-
- $quizzes['upcomming'][$time] = [];
-
- // Seperate out inprogress and upcomming quizzes, then get data for each quiz.
- foreach ($trackedquizzes as $quiz) {
- if ($quiz->timeopen < $time && $quiz->timeclose > $time && $hour === 0) { // Get inprogress quizzes.
- $quizdata = $this->get_quiz_data($quiz->id);
- $quizdata->timestampopen = $quiz->timeopen;
- $quizdata->timestampclose = $quiz->timeclose;
- $quizdata->timestamplimit = $quiz->timelimit;
- $quizdata->isoverride = $quiz->isoverride;
-
- if (isset($quiz->overrides)) {
- $quizdata->overrides = $quiz->overrides;
- }
-
- // Get tracked users for quiz.
- $trackedrecords = $this->get_quiz_tracking($quiz->id);
- $quizdata->tracking = array_pop($trackedrecords);
-
- $quizzes['inprogress'][$quiz->id] = $quizdata;
- unset($trackedquizzes[$quiz->id]); // Remove quiz from array to help with performance.
- } else if (($quiz->timeopen >= $time) && ($quiz->timeopen < ($time + HOURSECS))) { // Get upcomming quizzes.
- $quizdata = $this->get_quiz_data($quiz->id);
- $quizdata->timestampopen = $quiz->timeopen;
- $quizdata->timestampclose = $quiz->timeclose;
- $quizdata->timestamplimit = $quiz->timelimit;
- $quizdata->isoverride = $quiz->isoverride;
-
- if (isset($quiz->overrides)) {
- $quizdata->overrides = $quiz->overrides;
- }
-
- // Get tracked users for quiz.
- $trackedrecords = $this->get_quiz_tracking($quiz->id);
- $quizdata->tracking = array_pop($trackedrecords);
-
- $quizzes['upcomming'][$time][$quiz->id] = $quizdata;
- unset($trackedquizzes[$quiz->id]);
- } else {
- if (isset($quiz->overrides)) {
- $quizdata = $this->get_quiz_data($quiz->id);
- $quizdata->timestampopen = $quiz->timeopen;
- $quizdata->timestampclose = $quiz->timeclose;
- $quizdata->timestamplimit = $quiz->timelimit;
- $quizdata->isoverride = $quiz->isoverride;
-
- if (isset($quiz->overrides)) {
- $quizdata->overrides = $quiz->overrides;
- }
-
- // Get tracked users for quiz.
- $trackedrecords = $this->get_quiz_tracking($quiz->id);
- $quizdata->tracking = array_pop($trackedrecords);
-
- $quizzes['inprogress'][$quiz->id] = $quizdata;
- unset($trackedquizzes[$quiz->id]);
- }
- }
- }
- }
-
- // Iterate through the hours, processing finished quizzes.
- for ($hour = 1; $hour <= $this->hoursbehind; $hour++) {
- $time = $now - (HOURSECS * $hour);
-
- $quizzes['finished'][$time] = [];
-
- // Get data for each finished quiz.
- foreach ($trackedquizzes as $quiz) {
- if (($quiz->timeclose >= $time) && ($quiz->timeclose < ($time + HOURSECS))) { // Get finished quizzes.
- $quizdata = $this->get_quiz_data($quiz->id);
- $quizdata->timestampopen = $quiz->timeopen;
- $quizdata->timestampclose = $quiz->timeclose;
- $quizdata->timestamplimit = $quiz->timelimit;
- $quizdata->isoverride = $quiz->isoverride;
-
- if (isset($quiz->overrides)) {
- $quizdata->overrides = $quiz->overrides;
- }
-
- // Get tracked users for quiz.
- $trackedrecords = $this->get_quiz_tracking($quiz->id);
- $quizdata->tracking = array_pop($trackedrecords);
-
- $quizzes['finished'][$time][$quiz->id] = $quizdata;
- unset($trackedquizzes[$quiz->id]);
- }
- }
- }
-
- return $quizzes;
- }
-
- /**
- * Given a list of user ids, check if the user is logged in our not
- * and return summary counts of logged in and not logged in users.
- *
- * @param array $userids User ids to get logged in status.
- * @return \stdClass $usercounts Object with coutns of users logged in and not logged in.
- */
- private function get_loggedin_users(array $userids): \stdClass {
- global $CFG, $DB;
-
- $maxlifetime = $CFG->sessiontimeout;
- $timedout = time() - $maxlifetime;
- $userchunks = array_chunk($userids, 250); // Break list of users into chunks so we don't exceed DB IN limits.
-
- $loggedin = 0; // Count of logged in users.
- $loggedout = 0; // Count of not loggedin users.
- $loggedinusers = [];
- $loggedoutusers = [];
-
- foreach ($userchunks as $userchunk) {
- [$insql, $inparams] = $DB->get_in_or_equal($userchunk);
- $inparams[] = $timedout;
-
- $sql = "SELECT DISTINCT(userid)
- FROM {sessions}
- WHERE userid $insql
- AND timemodified >= ?";
- $users = $DB->get_fieldset_sql($sql, $inparams);
- $loggedinusers = array_merge($loggedinusers, $users);
- }
-
- $loggedoutusers = array_diff($userids, $loggedinusers);
-
- $loggedin = count($loggedinusers);
- $loggedout = count($loggedoutusers);
-
- $usercounts = new \stdClass();
- $usercounts->loggedin = $loggedin;
- $usercounts->loggedout = $loggedout;
- $usercounts->loggedinusers = $loggedinusers;
- $usercounts->loggedoutusers = $loggedoutusers;
-
- return $usercounts;
- }
-
- /**
- * Get count of in porgress and finished attempts for a quiz.
- *
- * @param int $quizid The id of the quiz to get the counts for.
- * @return \stdClass $attemptcounts The found counts.
- */
- private function get_quiz_attempts(int $quizid): \stdClass {
- global $DB;
-
- $inprogress = 0;
- $finished = 0;
- $inprogressusers = [];
- $finishedusers = [];
-
- $sql = 'SELECT userid, state
- FROM {quiz_attempts} qa
- JOIN (
- SELECT MAX(id) id
- FROM {quiz_attempts}
- WHERE quiz = ?
- GROUP BY userid)
- AS qb
- ON qa.id = qb.id';
-
- $params = [$quizid];
-
- $usersattempts = $DB->get_records_sql($sql, $params);
-
- foreach ($usersattempts as $usersattempt) {
- if ($usersattempt->state == 'inprogress' || $usersattempt->state == 'overdue') {
- $inprogress++;
- $inprogressusers[] = $usersattempt->userid;
- } else if ($usersattempt->state == 'finished' || $usersattempt->state == 'abandoned') {
- $finished++;
- $finishedusers[] = $usersattempt->userid;
- }
- }
-
- $attemptcounts = new \stdClass();
- $attemptcounts->inprogress = $inprogress;
- $attemptcounts->finished = $finished;
- $attemptcounts->inprogressusers = $inprogressusers;
- $attemptcounts->finishedusers = $finishedusers;
-
- return $attemptcounts;
- }
-
- /**
- * Process and store user tracking information for a quiz.
- *
- * @param int $now Timestamp to use for reference for time.
- * @return int $count Count of processed quizzes
- */
- public function process_quiz_tracking(int $now): int {
- global $DB;
-
- $frequency = new frequency();
- $quizzes = $this->get_tracked_quizzes_with_overrides($now);
- $quizusersbyquizid = [];
- $contextsbyquizid = [];
- $count = 0;
-
- foreach ($quizzes as $quiz) {
- $contextid = $this->get_quiz_context($quiz->id)->id;
- $quizusersbyquizid[$quiz->id] = array_column($frequency->get_event_users_raw(
- $contextid,
- 'quiz'
- ), 'id');
-
- $contextsbyquizid[$quiz->id] = $contextid;
- }
-
- $loggedinusers = $this->get_loggedin_users(
- array_unique(array_reduce($quizusersbyquizid, 'array_merge', []))
- );
-
- // For each quiz get the list of users who are elligble to do the quiz.
- foreach ($quizzes as $quiz) {
- $context = $contextsbyquizid[$quiz->id];
- $quizusers = $quizusersbyquizid[$quiz->id];
- $attemptusers = $this->get_quiz_attempts($quiz->id);
- $loggedout = 0;
- $loggedin = 0;
- $inprogress = 0;
- $finished = 0;
-
- foreach ($quizusers as $user) {
- if (in_array($user, $attemptusers->finishedusers)) {
- $finished++;
- continue;
- } else if (in_array($user, $attemptusers->inprogressusers)) {
- $inprogress++;
- continue;
- } else if (in_array($user, $loggedinusers->loggedinusers)) {
- $loggedin++;
- continue;
- } else if (in_array($user, $loggedinusers->loggedoutusers)) {
- $loggedout++;
- continue;
- }
- }
-
- $record = new \stdClass();
- $record->assessid = $quiz->id;
- $record->notloggedin = $loggedout;
- $record->loggedin = $loggedin;
- $record->inprogress = $inprogress;
- $record->finished = $finished;
- $record->timecreated = time();
-
- $DB->insert_record('local_assessfreq_trend', $record);
- $count++;
- }
-
- return $count;
- }
-
- /**
- * Given a quiz ID get its tracking information.
- *
- * @param int $quizid The ID of the quiz.
- * @return array $tracking Tracking reocrds for the quiz.
- */
- public function get_quiz_tracking(int $quizid): array {
- global $DB;
-
- $tracking = $DB->get_records('local_assessfreq_trend', ['assessid' => $quizid], 'timecreated ASC');
-
- return $tracking;
- }
-
- /**
- * Given an array of quizzes, filter based on a provided search string and apply pagination.
- *
- * @param array $quizzes Array of quizzes to search.
- * @param string $search The string to search by.
- * @param int $page The page number of results.
- * @param int $pagesize The page size for results.
- * @return array $result Array containing list of filtered quizzes and total of how many quizzes matched the filter.
- */
- public function filter_quizzes(array $quizzes, string $search, int $page, int $pagesize): array {
- $filtered = [];
- $searchfields = ['name', 'coursefullname'];
- $offset = $page * $pagesize;
- $offsetcount = 0;
- $recordcount = 0;
-
- foreach ($quizzes as $id => $quiz) {
- $searchcount = 0;
- if ($search != '') {
- $searchcount = -1;
- foreach ($searchfields as $searchfield) {
- if (stripos($quiz->{$searchfield}, $search) !== false) {
- $searchcount++;
- }
- }
- }
-
- if ($searchcount > -1 && $offsetcount >= $offset && $recordcount < $pagesize) {
- $filtered[$id] = $quiz;
- }
-
- if ($searchcount > -1 && $offsetcount >= $offset) {
- $recordcount++;
- }
-
- if ($searchcount > -1) {
- $offsetcount++;
- }
- }
-
- $result = [$filtered, $offsetcount];
-
- return $result;
- }
-}
diff --git a/classes/report_base.php b/classes/report_base.php
new file mode 100644
index 00000000..c7327818
--- /dev/null
+++ b/classes/report_base.php
@@ -0,0 +1,103 @@
+.
+
+/**
+ * Base report class.
+ *
+ * @package local_assessfreq
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace local_assessfreq;
+
+/**
+ * Abstract class that each report subplugin primary class will extend from to determine consistent factors.
+ */
+abstract class report_base {
+
+ private static array $instances = [];
+
+ public function __construct() {
+ $this->get_required_js();
+ $this->get_required_css();
+ }
+
+ /**
+ * Get the instance of the report class.
+ *
+ * @return report_base
+ */
+ public static function get_instance() : report_base {
+ $class = static::class;
+ if (!isset(self::$instances[$class])) {
+ self::$instances[$class] = new static();
+ }
+
+ return self::$instances[$class];
+ }
+
+ /**
+ * Return the name of the tab being rendered.
+ * @return string
+ */
+ abstract public function get_name() : string;
+
+ /**
+ * Return the weight of the tab which is used to determine the loading order with the highest first.
+ * @return int
+ */
+ abstract public function get_tab_weight() : int;
+
+ /**
+ * Get the contents of the page as a string of HTML (template).
+ *
+ * @return object
+ */
+
+ abstract public function get_contents() : string;
+
+ /**
+ * Get the anchor link to use for the tabs.
+ *
+ * @return string
+ */
+ abstract public function get_tablink() : string;
+
+ /**
+ * Check if the report is visible to the user.
+ *
+ * @return bool
+ */
+ public function has_access() : bool {
+ return false;
+ }
+
+ /**
+ * Set up the required JS in the global $PAGE object.
+ * @return void
+ */
+ protected function get_required_js() {
+ }
+
+ /**
+ * Set up the required CSS in the global $PAGE object.
+ * @return void
+ */
+ protected function get_required_css() {
+ }
+}
diff --git a/classes/source_base.php b/classes/source_base.php
new file mode 100644
index 00000000..6bfdccf5
--- /dev/null
+++ b/classes/source_base.php
@@ -0,0 +1,197 @@
+.
+
+/**
+ * Base source class.
+ *
+ * @package local_assessfreq
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace local_assessfreq;
+
+/**
+ * Abstract class that each source subplugin primary class will extend from to determine consistent factors.
+ */
+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();
+ }
+
+ /**
+ * Get the instance of the source class.
+ *
+ * @return source_base
+ */
+ public static function get_instance() : source_base {
+ $class = static::class;
+ if (!isset(self::$instances[$class])) {
+ self::$instances[$class] = new static();
+ }
+
+ return self::$instances[$class];
+ }
+
+ /**
+ * Return the name of the module the source refers to.
+ * @return string
+ */
+ abstract public function get_module() : string;
+
+ /**
+ * Return the module table. By default, this is the module name, however some mods use a different table.
+ * @return string
+ */
+ public function get_module_table() : string {
+ return $this->get_module();
+ }
+
+ /**
+ * Return the timelimit field used in the module table.
+ * @return string
+ */
+ public function get_timelimit_field() : string {
+ return '';
+ }
+
+ /**
+ * Return the available/timeopen field used in the module table.
+ * @return string
+ */
+ public function get_open_field() : string {
+ return '';
+ }
+
+ /**
+ * Return the duedate/timeclose field used in the module table.
+ * @return string
+ */
+ public function get_close_field() : string {
+ return '';
+ }
+
+ /**
+ * Return the capability map for the module that users must have before the activity applies to them.
+ * @return array
+ */
+ public function get_user_capabilities() : array {
+ return [];
+ }
+
+ /**
+ * Return the name of the source being rendered.
+ * @return string
+ */
+ abstract public function get_name() : string;
+
+ /**
+ * Set up the required JS in the global $PAGE object.
+ * @return void
+ */
+ protected function get_required_js() {
+ }
+
+ /**
+ * Set up the required CSS in the global $PAGE object.
+ * @return void
+ */
+ protected function get_required_css() {
+ }
+
+ /**
+ * Given an assess ID and module get its tracking information.
+ *
+ * @param int $assessid The ID of the assessment.
+ * @param bool $limited If limited, only return a subset of data. Otherwise reports can try and render thousands of data points.
+ * @return array $tracking Tracking reocrds for the quiz.
+ */
+ protected function get_tracking(int $assessid, bool $limited = false) : array {
+ global $DB;
+
+ $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' => $module],
+ 'id DESC',
+ '*',
+ 0,
+ $trendlimit
+ );
+ if (!$limited) {
+ return $trends;
+ }
+ $modulus = round(count($trends) / $trendcount);
+ $i = 0;
+ if (count($trends) < $trendcount) {
+ return $trends;
+ }
+ foreach ($trends as $trend) {
+ if ($i % $modulus == 0) {
+ $return[] = $trend;
+ }
+ $i++;
+ }
+
+ self::$cache[$cachekey] = $return;
+
+ return $return;
+ }
+
+ /**
+ * Given an assess ID and module get its most recent tracking information.
+ *
+ * @param int $assessid The ID of the assessment.
+ * @return mixed $tracking Tracking record.
+ */
+ protected function get_recent_tracking(int $assessid) {
+ global $DB;
+
+ return $DB->get_record_sql("
+ SELECT *
+ FROM {local_assessfreq_trend}
+ WHERE assessid = ?
+ AND module = ?
+ ORDER BY id DESC
+ LIMIT 1
+ ",
+ [$assessid, $this->get_module()]
+ );
+ }
+}
diff --git a/classes/task/data_process.php b/classes/task/data_process.php
index 73eb05ab..0ac5fc71 100644
--- a/classes/task/data_process.php
+++ b/classes/task/data_process.php
@@ -21,9 +21,14 @@
* @copyright 2020 Matt Porritt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
namespace local_assessfreq\task;
+use context_system;
+use core\task\manager;
use core\task\scheduled_task;
+use local_assessfreq\event\event_processed;
+use local_assessfreq\frequency;
/**
* A scheduled task to generate data used in plugin reports.
@@ -38,7 +43,7 @@ class data_process extends scheduled_task {
*
* @return string
*/
- public function get_name() {
+ public function get_name() : string {
return get_string('task:dataprocess', 'local_assessfreq');
}
@@ -49,11 +54,11 @@ public function get_name() {
public function execute() {
mtrace('local_assessfreq: Processing event data');
$now = time();
- $frequency = new \local_assessfreq\frequency();
- $context = \context_system::instance();
+ $frequency = new frequency();
+ $context = context_system::instance();
// Only run scheduled task if there is not an ad-hoc task pending or processing historic data.
- $adhoctask = \core\task\manager::get_adhoc_tasks(\local_assessfreq\task\history_process::class);
+ $adhoctask = manager::get_adhoc_tasks(history_process::class);
if (!empty($adhoctask)) {
mtrace('local_assessfreq: Stopping early historic processing task pending');
return;
@@ -62,9 +67,9 @@ public function execute() {
// Due dates may have changed since we last ran report. So delete all events in DB later than now and replace them.
mtrace('local_assessfreq: Deleting old event data');
$actionstart = time();
- $frequency->delete_events($now); // Delete event records greaer than now.
+ $frequency->delete_events($now); // Delete event records greater than now.
$actionduration = time() - $actionstart;
- $event = \local_assessfreq\event\event_processed::create([
+ $event = event_processed::create([
'context' => $context,
'other' => ['action' => 'delete', 'duration' => $actionduration],
]);
@@ -75,7 +80,7 @@ public function execute() {
$actionstart = time();
$frequency->process_site_events($now); // Process records in the future.
$actionduration = time() - $actionstart;
- $event = \local_assessfreq\event\event_processed::create([
+ $event = event_processed::create([
'context' => $context,
'other' => ['action' => 'site', 'duration' => $actionduration],
]);
@@ -86,11 +91,21 @@ public function execute() {
$actionstart = time();
$frequency->process_user_events($now); // Process user events.
$actionduration = time() - $actionstart;
- $event = \local_assessfreq\event\event_processed::create([
+ $event = event_processed::create([
'context' => $context,
'other' => ['action' => 'user', 'duration' => $actionduration],
]);
$event->trigger();
mtrace('local_assessfreq: Processing user events finished in: ' . $actionduration . ' seconds');
+
+ //mtrace('local_assessfreq: Clearing legacy tracking data');
+ //$actionstart = time();
+ //$actionduration = time() - $actionstart;
+ //$event = event_processed::create([
+ // 'context' => $context,
+ // 'other' => ['action' => 'user', 'duration' => $actionduration],
+ //]);
+ //$event->trigger();
+ //mtrace('local_assessfreq: Processing user events finished in: ' . $actionduration . ' seconds');
}
}
diff --git a/classes/task/history_process.php b/classes/task/history_process.php
index 5cbcb513..7e2b797f 100644
--- a/classes/task/history_process.php
+++ b/classes/task/history_process.php
@@ -23,7 +23,12 @@
*/
namespace local_assessfreq\task;
+use context_system;
use core\task\adhoc_task;
+use core\task\manager;
+use local_assessfreq\event\event_processed;
+use local_assessfreq\frequency;
+use moodle_exception;
/**
* Adhoc task to process historical data used in plugin.
@@ -43,18 +48,18 @@ public function execute() {
// Only run if scheduled task is not running.
// Throw an error if it is and this task will be retried after a delay.
// The scheduled task won't start while this job is pending.
- $schedtask = \core\task\manager::get_scheduled_task(\local_assessfreq\task\data_process::class);
+ $schedtask = manager::get_scheduled_task(data_process::class);
if ($schedtask->get_lock()) {
- throw new \moodle_exception('local_assessfreq_scheduled_task_running');
+ throw new moodle_exception('local_assessfreq_scheduled_task_running');
}
- $frequency = new \local_assessfreq\frequency();
- $context = \context_system::instance();
+ $frequency = new frequency();
+ $context = context_system::instance();
$actionstart = time();
$frequency->delete_events(0); // Delete ALL event records.
$actionduration = time() - $actionstart;
- $event = \local_assessfreq\event\event_processed::create([
+ $event = event_processed::create([
'context' => $context,
'other' => ['action' => 'delete', 'duration' => $actionduration],
]);
@@ -65,7 +70,7 @@ public function execute() {
$actionstart = time();
$frequency->process_site_events(1); // Process ALL records.
$actionduration = time() - $actionstart;
- $event = \local_assessfreq\event\event_processed::create([
+ $event = event_processed::create([
'context' => $context,
'other' => ['action' => 'site', 'duration' => $actionduration],
]);
@@ -76,7 +81,7 @@ public function execute() {
$actionstart = time();
$frequency->process_user_events(1); // Process ALL user events.
$actionduration = time() - $actionstart;
- $event = \local_assessfreq\event\event_processed::create([
+ $event = event_processed::create([
'context' => $context,
'other' => ['action' => 'user', 'duration' => $actionduration],
]);
diff --git a/classes/task/quiz_tracking.php b/classes/task/quiz_tracking.php
deleted file mode 100644
index 0c7a21f3..00000000
--- a/classes/task/quiz_tracking.php
+++ /dev/null
@@ -1,59 +0,0 @@
-.
-
-/**
- * A scheduled task to track the process of quizzes in the system.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-namespace local_assessfreq\task;
-
-use core\task\scheduled_task;
-
-/**
- * A scheduled task to track the process of quizzes in the system.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class quiz_tracking extends scheduled_task {
- /**
- * Get a descriptive name for this task (shown to admins).
- *
- * @return string
- */
- public function get_name() {
- return get_string('task:quiztracking', 'local_assessfreq');
- }
-
- /**
- * Do the job.
- * Throw exceptions on errors (the job will be retried).
- */
- public function execute() {
- mtrace('local_assessfreq: Processing quiz trcking');
- $quiz = new \local_assessfreq\quiz();
-
- $actionstart = time();
- $quiz->process_quiz_tracking($actionstart); // Process user events.
- $actionduration = time() - $actionstart;
-
- mtrace('local_assessfreq: Processing quiz tracking finished in: ' . $actionduration . ' seconds');
- }
-}
diff --git a/classes/utils.php b/classes/utils.php
index 11d096ae..461f072a 100644
--- a/classes/utils.php
+++ b/classes/utils.php
@@ -70,7 +70,7 @@ public static function sort(array $inputarray, string $sorton, string $direction
/**
* Sort an array of arrays/objects by multiple values.
*
- * @param array $inputarray Array of quizzes to sort.
+ * @param array $inputarray Array of activities to sort.
* @param array $sorton Associative array to sort by in the format field => direction.
* @return array $inputarray the sorted array.
*/
diff --git a/dashboard_assessment.php b/dashboard_assessment.php
deleted file mode 100644
index 7c211437..00000000
--- a/dashboard_assessment.php
+++ /dev/null
@@ -1,50 +0,0 @@
-.
-
-/**
- * Assessment dashboard.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-require_once('../../config.php');
-require_once($CFG->libdir . '/adminlib.php');
-
-$baseurl = $CFG->wwwroot . "/local/assessfreq/dashboard_assessment.php";
-
-// Calls require_login and performs permissions checks for admin pages.
-admin_externalpage_setup(
- 'local_assessfreq_assessment',
- '',
- null,
- '',
- ['pagelayout' => 'admin']
-);
-
-$title = get_string('dashboard:assessment', 'local_assessfreq');
-$url = new moodle_url($baseurl);
-$context = context_system::instance();
-
-$PAGE->set_url($url);
-$PAGE->set_context($context);
-$PAGE->set_title($title);
-$PAGE->set_heading($title);
-$PAGE->requires->js_call_amd('local_assessfreq/dashboard_assessment', 'init', [$context->id]);
-
-$output = $PAGE->get_renderer('local_assessfreq');
-
-echo $output->render_dashboard_assessment($baseurl);
diff --git a/dashboard_quiz.php b/dashboard_quiz.php
deleted file mode 100644
index 66a62d2a..00000000
--- a/dashboard_quiz.php
+++ /dev/null
@@ -1,52 +0,0 @@
-.
-
-/**
- * Quiz dashboard.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-require_once('../../config.php');
-require_once($CFG->libdir . '/adminlib.php');
-
-$quizid = optional_param('id', 0, PARAM_INT);
-
-$baseurl = $CFG->wwwroot . "/local/assessfreq/dashboard_quiz.php";
-
-// Calls require_login and performs permissions checks for admin pages.
-admin_externalpage_setup(
- 'local_assessfreq_quiz',
- '',
- null,
- '',
- ['pagelayout' => 'admin']
-);
-
-$title = get_string('dashboard:quiz', 'local_assessfreq');
-$url = new moodle_url($baseurl);
-$context = context_system::instance();
-
-$PAGE->set_url($url);
-$PAGE->set_context($context);
-$PAGE->set_title($title);
-$PAGE->set_heading($title);
-$PAGE->requires->js_call_amd('local_assessfreq/dashboard_quiz', 'init', [$context->id, $quizid]);
-
-$output = $PAGE->get_renderer('local_assessfreq');
-
-echo $output->render_dashboard_quiz($baseurl);
diff --git a/dashboard_quiz_inprogress.php b/dashboard_quiz_inprogress.php
deleted file mode 100644
index 18cdacd4..00000000
--- a/dashboard_quiz_inprogress.php
+++ /dev/null
@@ -1,50 +0,0 @@
-.
-
-/**
- * Quiz dashboard.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-require_once('../../config.php');
-require_once($CFG->libdir . '/adminlib.php');
-
-$baseurl = $CFG->wwwroot . "/local/assessfreq/dashboard_quiz_inprogress.php";
-
-// Calls require_login and performs permissions checks for admin pages.
-admin_externalpage_setup(
- 'local_assessfreq_quiz',
- '',
- null,
- '',
- ['pagelayout' => 'admin']
-);
-
-$title = get_string('dashboard:quiz_inprogress', 'local_assessfreq');
-$url = new moodle_url($baseurl);
-$context = context_system::instance();
-
-$PAGE->set_url($url);
-$PAGE->set_context($context);
-$PAGE->set_title($title);
-$PAGE->set_heading($title);
-$PAGE->requires->js_call_amd('local_assessfreq/dashboard_quiz_inprogress', 'init', [$context->id]);
-
-$output = $PAGE->get_renderer('local_assessfreq');
-
-echo $output->render_dashboard_quiz_inprogress($baseurl);
diff --git a/db/access.php b/db/access.php
new file mode 100644
index 00000000..2359261f
--- /dev/null
+++ b/db/access.php
@@ -0,0 +1,34 @@
+.
+
+/**
+ * Access file.
+ *
+ * @package local_assessfreq
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = [
+ 'local/assessfreq:view' => [
+ 'captype' => 'read',
+ 'contextlevel' => CONTEXT_COURSE,
+ 'archetypes' => [],
+ ],
+];
diff --git a/db/install.php b/db/install.php
index a81c96ab..2ccdcc66 100644
--- a/db/install.php
+++ b/db/install.php
@@ -22,13 +22,16 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+use core\task\manager;
+use local_assessfreq\task\history_process;
+
/**
* Generate ad-hoc task on install.
*/
function xmldb_local_assessfreq_install() {
if (!PHPUNIT_TEST) { // I hate this anti-pattern.
// Create an adhoc task that will process all historical event data.
- $task = new \local_assessfreq\task\history_process();
- \core\task\manager::queue_adhoc_task($task, true);
+ $task = new history_process();
+ manager::queue_adhoc_task($task, true);
}
}
diff --git a/db/install.xml b/db/install.xml
index ca69cca6..a660c3ae 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -69,6 +69,7 @@
+
@@ -81,6 +82,7 @@
+
diff --git a/db/services.php b/db/services.php
index afac4f83..ce50c597 100644
--- a/db/services.php
+++ b/db/services.php
@@ -26,40 +26,6 @@
// Define the web service functions to install.
$functions = [
- 'local_assessfreq_get_frequency' => [
- 'classname' => 'local_assessfreq_external',
- 'methodname' => 'get_frequency',
- 'classpath' => '',
- 'description' => 'Returns event frequency map.',
- 'type' => 'read',
- 'ajax' => true,
- ],
- 'local_assessfreq_get_heat_colors' => [
- 'classname' => 'local_assessfreq_external',
- 'methodname' => 'get_heat_colors',
- 'classpath' => '',
- 'description' => 'Returns event heat map colors.',
- 'type' => 'read',
- 'loginrequired' => false,
- 'ajax' => true,
- ],
- 'local_assessfreq_get_process_modules' => [
- 'classname' => 'local_assessfreq_external',
- 'methodname' => 'get_process_modules',
- 'classpath' => '',
- 'description' => 'Returns modules we are processing .',
- 'type' => 'read',
- 'loginrequired' => false,
- 'ajax' => true,
- ],
- 'local_assessfreq_get_day_events' => [
- 'classname' => 'local_assessfreq_external',
- 'methodname' => 'get_day_events',
- 'classpath' => '',
- 'description' => 'Gets day event info for use in heatmap.',
- 'type' => 'read',
- 'ajax' => true,
- ],
'local_assessfreq_get_courses' => [
'classname' => 'local_assessfreq_external',
'methodname' => 'get_courses',
@@ -68,19 +34,11 @@
'type' => 'read',
'ajax' => true,
],
- 'local_assessfreq_get_quizzes' => [
+ 'local_assessfreq_get_activities' => [
'classname' => 'local_assessfreq_external',
- 'methodname' => 'get_quizzes',
+ 'methodname' => 'get_activities',
'classpath' => '',
- 'description' => 'Gets quizzes.',
- 'type' => 'read',
- 'ajax' => true,
- ],
- 'local_assessfreq_get_quiz_data' => [
- 'classname' => 'local_assessfreq_external',
- 'methodname' => 'get_quiz_data',
- 'classpath' => '',
- 'description' => 'Gets quiz data.',
+ 'description' => 'Gets activities.',
'type' => 'read',
'ajax' => true,
],
@@ -100,21 +58,4 @@
'type' => 'write',
'ajax' => true,
],
- 'local_assessfreq_get_system_timezone' => [
- 'classname' => 'local_assessfreq_external',
- 'methodname' => 'get_system_timezone',
- 'classpath' => '',
- 'description' => 'Returns system (not user) timezone.',
- 'type' => 'read',
- 'loginrequired' => false,
- 'ajax' => true,
- ],
- 'local_assessfreq_get_inprogress_counts' => [
- 'classname' => 'local_assessfreq_external',
- 'methodname' => 'get_inprogress_counts',
- 'classpath' => '',
- 'description' => 'Get counts for inprogress assessments.',
- 'type' => 'read',
- 'ajax' => true,
- ],
];
diff --git a/db/subplugins.json b/db/subplugins.json
new file mode 100644
index 00000000..e37298eb
--- /dev/null
+++ b/db/subplugins.json
@@ -0,0 +1,6 @@
+{
+ "plugintypes": {
+ "assessfreqreport": "local/assessfreq/report",
+ "assessfreqsource": "local/assessfreq/source"
+ }
+}
\ No newline at end of file
diff --git a/db/tasks.php b/db/tasks.php
index 6e9893c3..3648ed85 100644
--- a/db/tasks.php
+++ b/db/tasks.php
@@ -36,14 +36,5 @@
'day' => '*',
'dayofweek' => '*',
'month' => '*',
- ],
- [
- 'classname' => 'local_assessfreq\task\quiz_tracking',
- 'blocking' => 0,
- 'minute' => '*',
- 'hour' => '*',
- 'day' => '*',
- 'dayofweek' => '*',
- 'month' => '*',
- ],
+ ]
];
diff --git a/db/upgrade.php b/db/upgrade.php
new file mode 100644
index 00000000..daa35493
--- /dev/null
+++ b/db/upgrade.php
@@ -0,0 +1,57 @@
+.
+
+/**
+ * Upgrade file.
+ *
+ * @package local_assessfreq
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Function to upgrade local_assessfreq.
+ * @param int $oldversion the version we are upgrading from
+ * @return bool result
+ */
+function xmldb_local_assessfreq_upgrade($oldversion) {
+ global $DB;
+
+ $dbman = $DB->get_manager();
+
+ if ($oldversion < 2024040302) {
+
+ $table = new xmldb_table('local_assessfreq_trend');
+ /*
+ * Previously we only used this table for quiz, so all existing modules will be quiz modules, hence the default.
+ */
+ $field = new xmldb_field('module', XMLDB_TYPE_CHAR, '20', true, true, null, 'quiz');
+ $index = new xmldb_index('module', XMLDB_INDEX_NOTUNIQUE, ['assessid', 'module']);
+
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ upgrade_plugin_savepoint(true, 2024040302, 'local', 'assessfreq');
+ }
+
+ return true;
+}
diff --git a/history.php b/history.php
index d5f241a4..cb3cd9fb 100644
--- a/history.php
+++ b/history.php
@@ -34,22 +34,22 @@
// Build the page output.
echo $OUTPUT->header();
-echo $OUTPUT->heading(get_string('clearhistory', 'local_assessfreq'));
+echo $OUTPUT->heading(get_string('settings:clearhistory', 'local_assessfreq'));
// Page content. (This feels like the lazy way to do things).
$url = new \moodle_url('/local/assessfreq/history.php', ['action' => 'deleteall']);
if ($action === null) {
echo $OUTPUT->box_start();
- echo $OUTPUT->container(get_string('reprocessall_desc', 'local_assessfreq'));
- echo $OUTPUT->single_button($url, get_string('reprocessall', 'local_assessfreq'), 'get');
+ echo $OUTPUT->container(get_string('history:reprocessall_desc', 'local_assessfreq'));
+ echo $OUTPUT->single_button($url, get_string('history:reprocessall', 'local_assessfreq'), 'get');
echo $OUTPUT->box_end();
} else if ($action == 'deleteall') {
$actionurl = new moodle_url('/local/assessfreq/history.php', ['action' => 'confirmed']);
$cancelurl = new moodle_url('/local/assessfreq/history.php');
echo $OUTPUT->confirm(
- get_string('confirmreprocess', 'local_assessfreq'),
- new single_button($actionurl, get_string('continue'), 'post', single_button::BUTTON_SECONDARY),
+ get_string('history:confirmreprocess', 'local_assessfreq'),
+ new single_button($actionurl, get_string('continue'), 'post', true),
new single_button($cancelurl, get_string('cancel'), 'get')
);
} else if ($action == 'confirmed') {
@@ -57,8 +57,8 @@
$task = new \local_assessfreq\task\history_process();
\core\task\manager::queue_adhoc_task($task, true);
echo $OUTPUT->box_start();
- echo $OUTPUT->container(get_string('reprocessall_desc', 'local_assessfreq'));
- echo $OUTPUT->single_button($url, get_string('reprocessall', 'local_assessfreq'), 'get');
+ echo $OUTPUT->container(get_string('history:reprocessall_desc', 'local_assessfreq'));
+ echo $OUTPUT->single_button($url, get_string('history:reprocessall', 'local_assessfreq'), 'get');
echo $OUTPUT->box_end();
}
diff --git a/index.php b/index.php
new file mode 100644
index 00000000..1cc018e3
--- /dev/null
+++ b/index.php
@@ -0,0 +1,64 @@
+.
+
+/**
+ * Main landing page for the reports
+ *
+ * @package local_assessfreq
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(__FILE__, 3) . '/config.php');
+
+require_login();
+
+require_once('lib.php');
+
+// Capability requirements.
+$context = context_system::instance();
+$course = get_course(SITEID);
+
+// If we have a course selected, update the PAGE object accordinging.
+if ($courseid = optional_param('courseid', 0, PARAM_INT)) {
+ // If we've been given the side id redirect without the param.
+ if ($courseid == SITEID) {
+ redirect('/local/assessfreq/');
+ }
+ $context = context_course::instance($courseid);
+ $PAGE->set_pagelayout('incourse');
+ $course = get_course($courseid);
+}
+
+// Capability check.
+require_capability('local/assessfreq:view', $context);
+
+$PAGE->set_url('/local/assessfreq');
+$PAGE->set_context($context);
+// Set the course to use in subsequent checks.
+$PAGE->set_course($course);
+
+if ($course->id != SITEID) {
+ $PAGE->set_heading($course->fullname);
+}
+$PAGE->set_title(get_string('pluginname', 'local_assessfreq'));
+
+$output = $PAGE->get_renderer('local_assessfreq');
+$PAGE->requires->js_call_amd('local_assessfreq/dashboard', 'init');
+
+/* @var $output local_assessfreq\output\renderer */
+$output->render_reports();
diff --git a/lang/en/local_assessfreq.php b/lang/en/local_assessfreq.php
index 4ada491d..76ef1426 100644
--- a/lang/en/local_assessfreq.php
+++ b/lang/en/local_assessfreq.php
@@ -15,207 +15,50 @@
// along with Moodle. If not, see .
/**
- * Plugin strings are defined here.
+ * Lang file.
*
- * @package local_assessfreq
- * @category string
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package local_assessfreq
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
+$string['pluginname'] = 'Assessment Frequency Report';
+$string['subplugintype_assessfreqreport_plural'] = 'Assessment Frequency Reports';
+$string['subplugintype_assessfreqsource_plural'] = 'Assessment Frequency Sources';
-$string['pluginname'] = 'Assessment Frequency';
-$string['title'] = 'Assessment Frequency';
+$string['privacy:metadata'] = 'The assessment frequency reports only display data';
+
+$string['assessfreq:view'] = 'Ability to load the inital view. Report subplugins will also need to be allowed.';
-$string['abandoned'] = 'Abandoned';
-$string['activity'] = 'Activity';
-$string['actions'] = 'Actions';
-$string['assessbyactivity'] = 'Assessments by activity';
-$string['assessbymonth'] = 'Assessments due by month';
-$string['assessbymonthstudents'] = 'Students with assessments due by month';
-$string['assessheatmap'] = 'Assessment heatmap for year:';
-$string['assessoverview'] = 'Assessment overviews for year:';
-$string['cachedef_eventsdueactivity'] = 'Events due by activity cache';
-$string['cachedef_eventsduemonth'] = 'Events due by month cache';
-$string['cachedef_eventusers'] = 'Users for month cache';
-$string['cachedef_monthlyuser'] = 'User events due by month cache';
-$string['cachedef_courseevents'] = 'Assessment frequency course event cache';
-$string['cachedef_siteevents'] = 'Assessment frequency site event cache';
-$string['cachedef_userevents'] = 'Assessment frequency user event cache';
-$string['cachedef_usereventsallfrequencyarray'] = 'Assessment frequency all user event cache';
-$string['cachedef_yearevents'] = 'Years that have events';
-$string['clearhistory'] = 'Clear history';
-$string['close'] = 'Close';
-$string['closeapply'] = 'Close and apply';
-$string['confirmreprocess'] = 'Delete ALL history and reprocess?';
-$string['course'] = 'Course';
-$string['courseasc'] = 'Course Asc';
-$string['coursedesc'] = 'Course Desc';
-$string['dashboard'] = 'View activity dashboard';
-$string['dashboard:assessment'] = 'Assessment dashboard';
-$string['dashboard:quiz'] = 'Quiz dashboard';
-$string['dashboard:quiz_inprogress'] = 'Quizzes in progress dashboard';
-$string['dashboard:quiztitle'] = '{$a->quiz} - {$a->course} - Dashboard';
-$string['duedate'] = 'Due date';
-$string['eventeventprocessed'] = 'event_processed';
-$string['eventeven_processed_desc'] = 'local assessfreq task event processing';
-$string['entercourse'] = 'Enter course name';
-$string['entersearch'] = 'Enter search text';
-$string['entersearchquiz'] = 'Search by quiz or course name';
-$string['findcourse'] = 'Find course';
-$string['finished'] = 'Finished';
-$string['hours0'] = 'Now';
-$string['hours1'] = '1 Hour';
-$string['hours4'] = '4 Hours';
-$string['hours8'] = '8 Hours';
-$string['hoursahead'] = 'Hours ahead';
-$string['hoursbehind'] = 'Hours behind';
-$string['inprogress'] = 'In progress';
-$string['inprogressdatetime'] = '%H:00';
-$string['inprogressparticpants'] = 'Participants in progress: {$a}';
-$string['inprogressquiz'] = 'Quizzes in progress: {$a}';
-$string['loading'] = 'Loading...';
-$string['loadingquiz'] = 'Loading quizzes';
-$string['loadingquiztitle'] = 'Loading quiz';
-$string['loggedin'] = 'Logged in';
-$string['na'] = 'N/A';
-$string['minuteone'] = '1 Minute';
-$string['minutetwo'] = '2 Minutes';
-$string['minutefive'] = '5 Minutes';
-$string['minuteten'] = '10 Minutes';
-$string['nocourse'] = 'No course selected';
-$string['nodata'] = 'No data found';
-$string['noquiz'] = 'No quiz selected...';
-$string['noquizselected'] = 'No quiz selected. Select quiz or cancel';
-$string['notloggedin'] = 'Not logged in';
-$string['numberassessments'] = 'By number of assessments';
-$string['numberevents'] = 'Event Count';
-$string['numberstudents'] = 'By number of students with assessments';
-$string['open'] = 'Open';
-$string['overdue'] = 'Overdue';
-$string['overrides'] = 'Overrides';
-$string['participantsummary'] = 'Participant summary';
-$string['participanttrend'] = 'Participant trend';
-$string['participants'] = 'Participants';
-$string['period'] = 'Period';
-$string['privacy:metadata:local_assessfreq'] = 'Data relating users for the local assessfreq plugin';
-$string['privacy:metadata:local_assessfreq_user'] = 'Data relating users with assessment events';
-$string['privacy:metadata:local_assessfreq_user:id'] = 'Record ID';
-$string['privacy:metadata:local_assessfreq_user:userid'] = 'The ID of the user that is effected by the assessment event';
-$string['privacy:metadata:local_assessfreq_user:eventid'] = 'The ID that relates to the assessment event';
-$string['privacy:metadata:local_assessfreq_conf_user'] = 'Data relating users with assessment conflicts';
-$string['privacy:metadata:local_assessfreq_conf_user:id'] = 'Record ID';
-$string['privacy:metadata:local_assessfreq_conf_user:userid'] = 'The ID of the user that is effected by the assessment conflict';
-$string['privacy:metadata:local_assessfreq_conf_user:conflictid'] = 'The ID that relates to the assessment conflict';
-$string['pluginsettings'] = 'Plugin settings';
-$string['quiz'] = 'Quiz';
-$string['quizasc'] = 'Quiz Asc';
-$string['quizdesc'] = 'Quiz Desc';
-$string['quizdetails'] = 'Quiz details';
-$string['quiztparticipantsoverride'] = 'Participants with an override:';
-$string['quiztquestionnumber'] = 'Questions in quiz:';
-$string['quizquestiontypes'] = 'Question types in quiz:';
-$string['quiztimeclose'] = 'Close time';
-$string['quiztimeearlyopen'] = 'First participant starts:';
-$string['quiztimefinish'] = 'Finish';
-$string['quiztimelateclose'] = 'Last participant finishes:';
-$string['quiztimelimit'] = 'Time limit';
-$string['quiztimeopen'] = 'Open time';
-$string['quiztimestart'] = 'Start';
-$string['quizparticipants'] = 'Participant count:';
-$string['quizresults'] = 'Quiz results:';
-$string['quizresultsview'] = 'View quiz results';
-$string['quizzes'] = 'Quizzes';
-$string['quizzesinprogress'] = 'Quizzes in progress';
-$string['reports'] = 'Assessment reports';
-$string['reset'] = 'Clear search';
-$string['reprocessall'] = 'Reprocess all events';
-$string['reprocessall_desc'] = 'This will delete ALL existing event records from the database and start a process to reprocess all events. This will happen in the background.';
-$string['rows5'] = '5 Rows';
-$string['rows10'] = '10 Rows';
-$string['rows20'] = '20 Rows';
-$string['rows50'] = '50 Rows';
-$string['rows100'] = '100 Rows';
-$string['scale'] = 'Scale:';
-$string['schedule'] = 'Daily schedule';
-$string['selectassessment'] = 'Select assessment type';
-$string['selectcourse'] = 'Select course first';
-$string['selectquiz'] = 'Select quiz';
-$string['searchquiz'] = 'Search for quiz';
-$string['searchquizform'] = 'Search and select the quiz to display on the dashboard';
-$string['selectmetric'] = 'Select metric';
-$string['selectyear'] = 'Select year';
-$string['settings:chartheading'] = 'Chart colors';
-$string['settings:chartheading_desc'] = 'These settings allow you to configure the colors used in the charts and graphs';
-$string['settings:finishedcolor'] = 'Finished color';
-$string['settings:finishedcolor_desc'] = 'Select color to display for finished users in charts';
-$string['settings:heat1'] = 'First heat color';
-$string['settings:heat1_desc'] = 'Select color for the first level of the frequency heatmap';
-$string['settings:heat1'] = 'First heat color';
-$string['settings:heat1_desc'] = 'Select color for the first level of the frequency heatmap';
-$string['settings:heat2'] = 'Second heat color';
-$string['settings:heat2_desc'] = 'Select color for the second level of the frequency heatmap';
-$string['settings:heat3'] = 'Third heat color';
-$string['settings:heat3_desc'] = 'Select color for the third level of the frequency heatmap';
-$string['settings:heat4'] = 'Fourth heat color';
-$string['settings:heat4_desc'] = 'Select color for the fourth level of the frequency heatmap';
-$string['settings:heat5'] = 'Fifth heat color';
-$string['settings:heat5_desc'] = 'Select color for the fifth level of the frequency heatmap';
-$string['settings:heat6'] = 'Sixth heat color';
-$string['settings:heat6_desc'] = 'Select color for the sixth level of the frequency heatmap';
-$string['settings:heatheading'] = 'Heatmap colors';
-$string['settings:heatheading_desc'] = 'These settings allow you to configure the colors used in the heatmap';
-$string['settings:hiddencourses'] = 'Include hidden courses';
-$string['settings:hiddencourses_desc'] = 'Included hidden courses in the heatmap calculations';
-$string['settings:inprogresscolor'] = 'In progress color';
-$string['settings:inprogresscolor_desc'] = 'Select color to display for in progress users in charts';
-$string['settings:loggedincolor'] = 'Logged in color';
-$string['settings:loggedincolor_desc'] = 'Select color to display for logged in users in charts';
-$string['settings:modules'] = 'Enabled modules';
-$string['settings:modules_desc'] = 'Select the modules that you want to appear in the heatmap calculations';
-$string['settings:moduleheading'] = 'Modules and courses';
-$string['settings:moduleheading_desc'] = 'These settings control how modules and courses are used in processing';
-$string['settings:notloggedincolor'] = 'Not logged in color';
-$string['settings:notloggedincolor_desc'] = 'Select color to display for not logged in users in charts';
-$string['settings:disabledmodules'] = 'Include disabled modules';
-$string['settings:disabledmodules_desc'] = 'Include modules that have been disabled in calculations';
-$string['showrows'] = 'Show rows';
-$string['sorttable'] = 'Sort table';
-$string['status'] = 'Status';
-$string['student_search'] = 'Student Search';
-$string['students'] = 'Students';
-$string['studenttable'] = 'Student attempt status';
-$string['submitoverridefail'] = 'Ajax override form submission failed';
-$string['systemdisabled'] = ' (module disabled)';
$string['task:dataprocess'] = 'Data collection task';
$string['task:quiztracking'] = 'Quiz tracking task';
-$string['time'] = 'Time';
-$string['timelimit'] = 'Time limit (minutes)';
-$string['timeendasc'] = 'End time Asc';
-$string['timeenddesc'] = 'End time Desc';
-$string['timestartasc'] = 'Start time Asc';
-$string['timestartdesc'] = 'Start time Desc';
-$string['title'] = 'Title';
-$string['toggleoverview'] = 'Toggle overview graphs';
-$string['trenddatetime'] = '%H:%M, %d-%m-%y';
-$string['userattempt'] = 'View user attempt';
-$string['upcommingquizes'] = 'Upcomming quizzes starting';
-$string['uploadpending'] = 'Upload pending';
-$string['userlogs'] = 'View user logs';
-$string['useroverride'] = 'Add user override';
-$string['userprofile'] = 'View user profile';
-$string['url'] = 'URL';
-$string['zoom'] = 'Zoom in';
-$string['jan'] = 'January';
-$string['feb'] = 'February';
-$string['mar'] = 'March';
-$string['apr'] = 'April';
-$string['may'] = 'May';
-$string['jun'] = 'June';
-$string['jul'] = 'July';
-$string['aug'] = 'August';
-$string['sep'] = 'September';
-$string['oct'] = 'October';
-$string['nov'] = 'November';
-$string['dec'] = 'December';
+
+$string['courseselect'] = 'Select course...';
+$string['noreports'] = 'No reports have been configured for you.
+If you believe this is an error please contact your site administrator.';
+
+$string['history:confirmreprocess'] = 'Delete ALL history and reprocess?';
+$string['history:reprocessall'] = 'Reprocess all events';
+$string['history:reprocessall_desc'] = 'This will delete ALL existing event records from the database and start a process to reprocess all events. This will happen in the background.';
+
+$string['settings:clearhistory'] = 'Assessment Frequency Clear History';
+$string['settings:head'] = 'Assessment Frequency Reports';
+$string['settings:local_assessfreq'] = 'Global Settings';
+$string['settings:start_month'] = 'Start month';
+$string['settings:start_month_desc'] = 'Specify the month that the heatmap year should start from.';
+$string['settings:hiddencourses'] = 'Include hidden courses';
+$string['settings:hiddencourses_desc'] = 'Included hidden courses in the reports';
+$string['settings:enablesource'] = 'Enable: {$a}';
+$string['settings:enablesource_help'] = 'Check this control to allow the source to be used for the dashboard.';
+$string['settings:enablereport'] = 'Enable: {$a}';
+$string['settings:enablereport_help'] = 'Check this control to allow the report to be used for the dashboard.';
+
+$string['filter:entersearch'] = 'Enter search';
+$string['filter:reset'] = 'Reset';
+$string['filter:showrows'] = 'Show rows';
+$string['filter:rows20'] = '20 rows';
+$string['filter:rows50'] = '50 rows';
+$string['filter:rows100'] = '100 rows';
+
+$string['modal:useroverride'] = 'User override';
diff --git a/lib.php b/lib.php
index 1258ad3a..484ef139 100644
--- a/lib.php
+++ b/lib.php
@@ -13,6 +13,9 @@
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
+use local_assessfreq\frequency;
+use local_assessfreq\source_base;
+use local_assessfreq\report_base;
/**
* This page contains callbacks.
@@ -23,297 +26,214 @@
*/
/**
- * Returns the name of the user preferences as well as the details this plugin uses.
+ * This function extends the navigation with the report link.
*
- * @return array
+ * @param navigation_node $navigation The navigation node to extend
+ * @param stdClass $course The course to object for the report
+ * @param context $context The context of the course
*/
-function local_assessfreq_user_preferences() {
-
- $preferences['local_assessfreq_overview_year_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => date('Y'),
- 'type' => PARAM_INT,
- ];
-
- $preferences['local_assessfreq_heatmap_year_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => date('Y'),
- 'type' => PARAM_INT,
- ];
-
- $preferences['local_assessfreq_heatmap_metric_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => 'assess',
- 'type' => PARAM_ALPHA,
- ];
-
- $preferences['local_assessfreq_heatmap_modules_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => '[]',
- 'type' => PARAM_RAW,
- ];
-
- $preferences['local_assessfreq_quiz_refresh_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => 60,
- 'type' => PARAM_INT,
- ];
-
- $preferences['local_assessfreq_quiz_table_rows_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => 20,
- 'type' => PARAM_INT,
- ];
-
- $preferences['local_assessfreq_student_search_table_rows_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => 20,
- 'type' => PARAM_INT,
- ];
-
- $preferences['local_assessfreq_student_search_table_hoursahead_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => 4,
- 'type' => PARAM_INT,
- ];
-
- $preferences['local_assessfreq_student_search_table_hoursbehind_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => 1,
- 'type' => PARAM_INT,
- ];
-
- $preferences['local_assessfreq_quizzes_inprogress_table_hoursahead_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => 0,
- 'type' => PARAM_INT,
- ];
-
- $preferences['local_assessfreq_quizzes_inprogress_table_hoursbehind_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => 0,
- 'type' => PARAM_INT,
- ];
-
- $preferences['local_assessfreq_quiz_table_inprogress_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => 20,
- 'type' => PARAM_INT,
- ];
-
- $preferences['local_assessfreq_quiz_table_inprogress_sort_preference'] = [
- 'null' => NULL_NOT_ALLOWED,
- 'default' => 'name_asc',
- 'type' => PARAM_ALPHAEXT,
- ];
-
- return $preferences;
+function local_assessfreq_extend_navigation_course(navigation_node $navigation, stdClass $course, context $context) {
+ if (has_capability('local/assessfreq:view', $context)) {
+ $url = new moodle_url('/local/assessfreq/', ['courseid' => $course->id]);
+ $settingsnode = navigation_node::create(get_string('pluginname', 'local_assessfreq'), $url);
+ $reportnode = $navigation->get('coursereports');
+ if (isset($settingsnode) && !empty($reportnode)) {
+ $reportnode->add_node($settingsnode);
+ }
+ }
}
/**
- * Return the HTML for the given chart.
+ * Get all of the subplugin reports that are enabled and instantiate the class.
*
- * @param string $args JSON from the calling AJAX function.
- * @return string $chartdata The generated chart.
+ * @param $ignoreenabled
+ * @return array
*/
-function local_assessfreq_output_fragment_get_chart($args): string {
- $allowedcalls = [
- 'assess_by_month',
- 'assess_by_activity',
- 'assess_by_month_student',
- ];
-
- $context = $args['context'];
- has_capability('moodle/site:config', $context);
- $data = json_decode($args['data']);
-
- if (in_array($data->call, $allowedcalls)) {
- $classname = '\\local_assessfreq\\output\\' . $data->call;
- $methodname = 'get_' . $data->call . '_chart';
- } else {
- throw new moodle_exception('Call not allowed');
+function get_reports($ignoreenabled = false) : array {
+ $reports = [];
+ $pluginmanager = core_plugin_manager::instance();
+ foreach ($pluginmanager->get_plugins_of_type('assessfreqreport') as $subplugin) {
+ /* @var $class report_base */
+ if ($subplugin->is_enabled() || $ignoreenabled) {
+ $class = "assessfreqreport_{$subplugin->name}\\report";
+ $report = $class::get_instance();
+ if ($report->has_access()) {
+ $reports[$subplugin->name] = $report;
+ }
+ }
}
-
- $assesschart = new $classname();
- $chart = $assesschart->$methodname($data->year);
-
- $chartdata = json_encode($chart);
- return $chartdata;
+ return $reports;
}
/**
- * Return the HTML for the given chart.
+ * Get all of the subplugin sources that are enabled and instantiate the class.
*
- * @param string $args JSON from the calling AJAX function.
- * @return string $chartdata The generated chart.
+ * @param $ignoreenabled
+ * @return array
*/
-function local_assessfreq_output_fragment_get_quiz_chart($args): string {
- $allowedcalls = [
- 'participant_summary',
- 'participant_trend',
- ];
-
- $context = $args['context'];
- has_capability('moodle/site:config', $context);
- $data = json_decode($args['data']);
-
- if (in_array($data->call, $allowedcalls)) {
- $classname = '\\local_assessfreq\\output\\' . $data->call;
- $methodname = 'get_' . $data->call . '_chart';
- } else {
- throw new moodle_exception('Call not allowed');
+function get_sources($ignoreenabled = false, $requiredmethod = '') : array {
+ $sources = [];
+ $pluginmanager = core_plugin_manager::instance();
+ foreach ($pluginmanager->get_plugins_of_type('assessfreqsource') as $subplugin) {
+ if ($subplugin->is_enabled() || $ignoreenabled) {
+ /* @var $class source_base */
+ $class = "assessfreqsource_{$subplugin->name}\\source";
+ $source = $class::get_instance();
+ if (!empty($requiredmethod)) {
+ if (!method_exists($source, $requiredmethod)) {
+ continue;
+ }
+ }
+ $sources[$subplugin->name] = $source;
+ }
}
-
- $assesschart = new $classname();
- $chart = $assesschart->$methodname($data->quiz);
-
- $chartdata = json_encode($chart);
- return $chartdata;
+ return $sources;
}
/**
- * Return the HTML for the given chart.
+ * Using the start month defined in config get an ordered year of month names.
*
- * @param string $args JSON from the calling AJAX function.
- * @return string $chartdata The generated chart.
+ * @return array
*/
-function local_assessfreq_output_fragment_get_quiz_inprogress_chart($args): string {
- $allowedcalls = [
- 'upcomming_quizzes',
- 'all_participants_inprogress',
- ];
-
- $context = $args['context'];
- has_capability('moodle/site:config', $context);
- $data = json_decode($args['data']);
-
- if (in_array($data->call, $allowedcalls)) {
- $classname = '\\local_assessfreq\\output\\' . $data->call;
- $methodname = 'get_' . $data->call . '_chart';
- } else {
- throw new moodle_exception('Call not allowed');
- }
+function get_months_ordered() : array {
- $assesschart = new $classname();
- $now = time();
+ $months = [];
+ $startmonth = get_config('local_assessfreq', 'start_month');
- if ($methodname == 'get_all_participants_inprogress_chart') {
- $chart = $assesschart->$methodname($now, $data->hoursahead, $data->hoursbehind);
- } else {
- $chart = $assesschart->$methodname($now);
+ for ($i = $startmonth; $i < $startmonth + 12; $i++) {
+ $month = $i - 12 > 0 ? $i - 12 : $i;
+
+ $date = DateTime::createFromFormat('!m', $month);
+ $monthname = $date->format('F');
+
+ $months[$month] = $monthname;
}
- $chartdata = json_encode($chart);
- return $chartdata;
+ return $months;
}
/**
- * Renders the quiz search form for the modal on the quiz dashboard.
+ * Get the years that have events with the preferred year active.
*
- * @param array $args
- * @return string $o Form HTML.
+ * @param $preference
+ * @return array
*/
-function local_assessfreq_output_fragment_new_base_form($args): string {
+function get_years($preference) : array {
- $context = $args['context'];
- has_capability('moodle/site:config', $context);
+ $currentyear = date('Y');
- $mform = new \local_assessfreq\form\quiz_search_form(null, null, 'post', '', ['class' => 'ignoredirty']);
+ // Get years that have events and load into context.
+ $frequency = new frequency();
+ $yearlist = $frequency->get_years_has_events();
- ob_start();
- $mform->display();
- $o = ob_get_contents();
- ob_end_clean();
+ if (empty($yearlist)) {
+ $yearlist = [$currentyear];
+ }
- return $o;
-}
+ // Add current year to the selection of years if missing.
+ if (!in_array($currentyear, $yearlist)) {
+ $yearlist[] = $currentyear;
+ }
-/**
- * Renders the student table on the quiz dashboard screen.
- * We update the table via ajax.
- *
- * @param array $args
- * @return string $o Form HTML.
- */
-function local_assessfreq_output_fragment_get_student_table($args): string {
- global $CFG, $PAGE;
+ $years = [];
- $context = $args['context'];
- has_capability('moodle/site:config', $context);
- $data = json_decode($args['data']);
+ foreach ($yearlist as $year) {
+ $years[$year] = ['year' => ['val' => $year]];
+ }
- $baseurl = $CFG->wwwroot . '/local/assessfreq/dashboard_quiz.php';
- $output = $PAGE->get_renderer('local_assessfreq');
+ if (!$preference) {
+ $preference = date('Y');
+ }
- $o = $output->render_student_table($baseurl, $data->quiz, $context->id, $data->search, $data->page);
+ $years[$preference]['year']['active'] = true;
- return $o;
+ return array_values($years);
}
/**
- * Renders the student table on the student search screen.
- * We update the table via ajax.
+ * Get the modules to use in data collection.
+ * This is based on which sources have been enabled.
*
- * @param array $args
- * @return string $o Form HTML.
+ * @return array $modules The enabled modules.
*/
-function local_assessfreq_output_fragment_get_student_search_table($args): string {
- global $CFG, $PAGE;
+function get_modules($preferences, $requiredmethod= '') : array {
- $context = $args['context'];
- has_capability('moodle/site:config', $context);
- $data = json_decode($args['data']);
- $search = is_null($data->search) ? '' : $data->search;
- $now = time();
- $hoursahead = (int)$data->hoursahead;
- $hoursbehind = (int)$data->hoursbehind;
+ $sources = get_sources(false, $requiredmethod);
- $baseurl = $CFG->wwwroot . '/local/assessfreq/student_search.php';
- $output = $PAGE->get_renderer('local_assessfreq');
+ // Get modules for filters and load into context.
+ $modules = [];
+ $modules['all'] = ['module' => ['val' => 'all', 'name' => get_string('all')]];
- $o = $output->render_student_search_table($baseurl, $context->id, $search, $hoursahead, $hoursbehind, $now, $data->page);
+ foreach ($sources as $source) {
+ $modulename = get_string('modulename', $source->get_module());
+ $modules[$source->get_module()] = ['module' => ['val' => $source->get_module(), 'name' => $modulename]];
+ }
- return $o;
+ if (!$preferences) {
+ $preferences = ["all"];
+ }
+
+ foreach ($preferences as $preference) {
+ if (isset($modules[$preference])) {
+ $modules[$preference]['module']['active'] = true;
+ }
+ }
+
+ return array_values($modules);
}
/**
- * Renders the quizzes in progress "table" on the quiz dashboard screen.
- * We update the table via ajax.
- * The table isn't a real table it's a collection of divs.
+ * Given a list of user ids, check if the user is logged in our not
+ * and return summary counts of logged in and not logged in users.
*
- * @param array $args
- * @return string $o Form HTML.
+ * @param array $userids User ids to get logged in status.
+ * @return stdClass $usercounts Object with coutns of users logged in and not logged in.
*/
-function local_assessfreq_output_fragment_get_quizzes_inprogress_table($args): string {
- global $PAGE;
+function get_loggedin_users(array $userids): stdClass {
+ global $CFG, $DB;
- $context = $args['context'];
- has_capability('moodle/site:config', $context);
+ $maxlifetime = $CFG->sessiontimeout;
+ $timedout = time() - $maxlifetime;
+ $userchunks = array_chunk($userids, 250); // Break list of users into chunks so we don't exceed DB IN limits.
- $data = json_decode($args['data']);
- $search = is_null($data->search) ? '' : $data->search;
- $sorton = is_null($data->sorton) ? 'name' : $data->sorton;
- $direction = is_null($data->direction) ? 'asc' : $data->direction;
- $hoursahead = (int)$data->hoursahead;
- $hoursbehind = (int)$data->hoursbehind;
+ $loggedinusers = [];
- $output = $PAGE->get_renderer('local_assessfreq');
- $o = $output->render_quizzes_inprogress_table($search, $data->page, $sorton, $direction, $hoursahead, $hoursbehind);
+ foreach ($userchunks as $userchunk) {
+ [$insql, $inparams] = $DB->get_in_or_equal($userchunk);
+ $inparams[] = $timedout;
- return $o;
+ $sql = "SELECT DISTINCT(userid)
+ FROM {sessions}
+ WHERE userid $insql
+ AND timemodified >= ?";
+ $users = $DB->get_fieldset_sql($sql, $inparams);
+ $loggedinusers = array_merge($loggedinusers, $users);
+ }
+
+ $loggedoutusers = array_diff($userids, $loggedinusers);
+
+ $loggedin = count($loggedinusers);
+ $loggedout = count($loggedoutusers);
+
+ $usercounts = new stdClass();
+ $usercounts->loggedin = $loggedin;
+ $usercounts->loggedout = $loggedout;
+ $usercounts->loggedinusers = $loggedinusers;
+ $usercounts->loggedoutusers = $loggedoutusers;
+
+ return $usercounts;
}
/**
- * Renders the quiz user override form for the modal on the quiz dashboard.
+ * Renders the user override form for the modal.
*
* @param array $args
* @return string $o Form HTML.
*/
function local_assessfreq_output_fragment_new_override_form($args): string {
- global $DB;
+ global $DB, $CFG;
- $context = $args['context'];
- has_capability('mod/quiz:manageoverrides', $context);
+ $module = $args['activitytype'];
$serialiseddata = json_decode($args['jsonformdata'], true);
@@ -323,36 +243,17 @@ function local_assessfreq_output_fragment_new_override_form($args): string {
parse_str($serialiseddata, $formdata);
}
- // Get some data needed to generate the form.
- $quizid = $args['quizid'];
- $quizdata = new \local_assessfreq\quiz();
- $quizcontext = $quizdata->get_quiz_context($quizid);
- $quiz = $DB->get_record('quiz', ['id' => $quizid], '*', MUST_EXIST);
-
- $cm = get_course_and_cm_from_cmid($quizcontext->instanceid, 'quiz')[1];
-
- // Check if we have an existing override for this user.
- $override = $DB->get_record('quiz_overrides', ['quiz' => $quiz->id, 'userid' => $args['userid']]);
-
- if ($override) {
- $data = clone $override;
- } else {
- $data = new \stdClass();
- $data->userid = $args['userid'];
- }
-
- $mform = new \local_assessfreq\form\quiz_override_form($cm, $quiz, $quizcontext, $override, $formdata);
- $mform->set_data($data);
-
- if (!empty($serialiseddata)) {
- // If we were passed non-empty form data we want the mform to call validation functions and show errors.
- $mform->is_validated();
+ $sources = get_sources();
+ $source = $sources[$module];
+ $o = '';
+ /* @var $source source_base */
+ if (method_exists($source, 'get_override_form')) {
+ $mform = $source->get_override_form($args['activityid'], $args['context'], $args['userid'], $serialiseddata);
+ ob_start();
+ $mform->display();
+ $o = ob_get_contents();
+ ob_end_clean();
}
- ob_start();
- $mform->display();
- $o = ob_get_contents();
- ob_end_clean();
-
return $o;
}
diff --git a/report/activities_in_progress/amd/build/activities_in_progress.min.js b/report/activities_in_progress/amd/build/activities_in_progress.min.js
new file mode 100644
index 00000000..4bbce616
--- /dev/null
+++ b/report/activities_in_progress/amd/build/activities_in_progress.min.js
@@ -0,0 +1,11 @@
+define("assessfreqreport_activities_in_progress/activities_in_progress",["exports","local_assessfreq/table_handler","local_assessfreq/user_preferences"],(function(_exports,_table_handler,UserPreference){var obj;
+/**
+ * Chart data JS module.
+ *
+ * @module assessfreqreport/activities_in_progress
+ * @package
+ * @copyright Simon Thornett
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */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)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_table_handler=(obj=_table_handler)&&obj.__esModule?obj:{default:obj},UserPreference=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}(UserPreference);_exports.init=context=>{moduleDropdown();let table=new _table_handler.default(0,context,"assessfreqreport-activities-in-progress-table","assessfreqreport_activities_in_progress","get_in_progress_table","assessfreqreport_activities_in_progress_table_rows_preference","assessfreqreport_activities_in_progress_table_sort_preference","assessfreqreport-activities-in-progress-table-search","assessfreqreport-activities-in-progress-table","local_assessfreq_set_table_preference");table.getTable();let tableSearchInputElement=document.getElementById("assessfreqreport-activities-in-progress-table-search"),tableSearchResetElement=document.getElementById("assessfreqreport-activities-in-progress-table-search-reset"),tableSearchRowsElement=document.getElementById("assessfreqreport-activities-in-progress-table-rows"),tableSearchAheadElement=document.getElementById("assessfreqreport-activities-in-progress-hoursahead"),tableSearchBehindElement=document.getElementById("assessfreqreport-activities-in-progress-hoursbehind");tableSearchInputElement.addEventListener("keyup",table.tableSearch),tableSearchInputElement.addEventListener("paste",table.tableSearch),tableSearchResetElement.addEventListener("click",table.tableSearchReset),tableSearchRowsElement.addEventListener("click",table.tableSearchRowSet),tableSearchAheadElement.addEventListener("click",tableSearchAheadSet),tableSearchBehindElement.addEventListener("click",tableSearchBehindSet)};const moduleDropdown=()=>{let links=document.getElementById("local-assessfreq-report-activities-in-progress-filter-type").getElementsByTagName("a"),all=links[0],modules=[];for(let i=0;i{event.preventDefault(),event.stopPropagation();for(let j=0;j{event.preventDefault(),event.stopPropagation();document.getElementById("local-assessfreq-report-activities-in-progress-filter-type-filters").classList.remove("show");for(let i=0;i{event.preventDefault(),event.stopPropagation(),all.classList.remove("active"),event.target.classList.toggle("active")}))}},tableSearchAheadSet=event=>{if(event.preventDefault(),"a"===event.target.tagName.toLowerCase()){let hours=event.target.dataset.metric;UserPreference.setUserPreference("assessfreqreport_activities_in_progress_hoursahead_preference",hours),location.reload()}},tableSearchBehindSet=event=>{if(event.preventDefault(),"a"===event.target.tagName.toLowerCase()){let hours=event.target.dataset.metric;UserPreference.setUserPreference("assessfreqreport_activities_in_progress_hoursbehind_preference",hours),location.reload()}}}));
+
+//# sourceMappingURL=activities_in_progress.min.js.map
\ No newline at end of file
diff --git a/report/activities_in_progress/amd/build/activities_in_progress.min.js.map b/report/activities_in_progress/amd/build/activities_in_progress.min.js.map
new file mode 100644
index 00000000..e325c844
--- /dev/null
+++ b/report/activities_in_progress/amd/build/activities_in_progress.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"activities_in_progress.min.js","sources":["../src/activities_in_progress.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 assessfreqreport/activities_in_progress\n * @package\n * @copyright Simon Thornett \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport TableHandler from 'local_assessfreq/table_handler';\nimport * as UserPreference from 'local_assessfreq/user_preferences';\n\n/**\n * Init function.\n * @param {Integer} context\n */\nexport const init = (context) => {\n\n // Set up event listener and related actions for module dropdown on heatmp.\n moduleDropdown();\n\n let table = new TableHandler(\n 0,\n context,\n 'assessfreqreport-activities-in-progress-table',\n 'assessfreqreport_activities_in_progress',\n 'get_in_progress_table',\n 'assessfreqreport_activities_in_progress_table_rows_preference',\n 'assessfreqreport_activities_in_progress_table_sort_preference',\n 'assessfreqreport-activities-in-progress-table-search',\n 'assessfreqreport-activities-in-progress-table',\n 'local_assessfreq_set_table_preference'\n );\n\n table.getTable();\n\n let tableSearchInputElement = document.getElementById('assessfreqreport-activities-in-progress-table-search');\n let tableSearchResetElement = document.getElementById('assessfreqreport-activities-in-progress-table-search-reset');\n let tableSearchRowsElement = document.getElementById('assessfreqreport-activities-in-progress-table-rows');\n let tableSearchAheadElement = document.getElementById('assessfreqreport-activities-in-progress-hoursahead');\n let tableSearchBehindElement = document.getElementById('assessfreqreport-activities-in-progress-hoursbehind');\n\n tableSearchInputElement.addEventListener('keyup', table.tableSearch);\n tableSearchInputElement.addEventListener('paste', table.tableSearch);\n tableSearchResetElement.addEventListener('click', table.tableSearchReset);\n tableSearchRowsElement.addEventListener('click', table.tableSearchRowSet);\n tableSearchAheadElement.addEventListener('click', tableSearchAheadSet);\n tableSearchBehindElement.addEventListener('click', tableSearchBehindSet);\n\n};\n\n/**\n * Add the event listeners to the modules in the module select dropdown.\n */\nconst moduleDropdown = () => {\n let links = document.getElementById('local-assessfreq-report-activities-in-progress-filter-type').getElementsByTagName('a');\n let all = links[0];\n let modules = [];\n\n for (let i = 0; i < links.length; i++) {\n let module = links[i].dataset.module;\n\n if (module.toLowerCase() === 'all') {\n links[i].addEventListener('click', event => {\n event.preventDefault();\n event.stopPropagation();\n // Remove active class from all other links.\n for (let j = 0; j < links.length; j++) {\n links[j].classList.remove('active');\n }\n event.target.classList.toggle('active');\n });\n } else if (module.toLowerCase() === 'close') {\n links[i].addEventListener('click', event => {\n event.preventDefault();\n event.stopPropagation();\n\n const dropdownmenu = document.getElementById('local-assessfreq-report-activities-in-progress-filter-type-filters');\n dropdownmenu.classList.remove('show');\n\n for (let i = 0; i < links.length; i++) {\n if (links[i].classList.contains('active')) {\n let module = links[i].dataset.module;\n modules.push(module);\n }\n }\n\n // Save selection as a user preference.\n UserPreference.setUserPreference(\n 'assessfreqreport_activities_in_progress_modules_preference',\n JSON.stringify(modules)\n );\n\n // Reload based on selected year.\n location.reload();\n });\n } else {\n links[i].addEventListener('click', event => {\n event.preventDefault();\n event.stopPropagation();\n\n all.classList.remove('active');\n\n event.target.classList.toggle('active');\n });\n }\n }\n};\n\n/**\n * Process the hours ahead event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst tableSearchAheadSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let hours = event.target.dataset.metric;\n UserPreference.setUserPreference('assessfreqreport_activities_in_progress_hoursahead_preference', hours);\n // Reload based on selected year.\n location.reload();\n }\n};\n\n/**\n * Process the hours behind event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\nconst tableSearchBehindSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let hours = event.target.dataset.metric;\n UserPreference.setUserPreference('assessfreqreport_activities_in_progress_hoursbehind_preference', hours);\n // Reload based on selected year.\n location.reload();\n }\n};\n"],"names":["context","moduleDropdown","table","TableHandler","getTable","tableSearchInputElement","document","getElementById","tableSearchResetElement","tableSearchRowsElement","tableSearchAheadElement","tableSearchBehindElement","addEventListener","tableSearch","tableSearchReset","tableSearchRowSet","tableSearchAheadSet","tableSearchBehindSet","links","getElementsByTagName","all","modules","i","length","module","dataset","toLowerCase","event","preventDefault","stopPropagation","j","classList","remove","target","toggle","contains","push","UserPreference","setUserPreference","JSON","stringify","location","reload","tagName","hours","metric"],"mappings":";;;;;;;;qmCA+BqBA,UAGjBC,qBAEIC,MAAQ,IAAIC,uBACZ,EACAH,QACA,gDACA,0CACA,wBACA,gEACA,gEACA,uDACA,gDACA,yCAGJE,MAAME,eAEFC,wBAA0BC,SAASC,eAAe,wDAClDC,wBAA0BF,SAASC,eAAe,8DAClDE,uBAAyBH,SAASC,eAAe,sDACjDG,wBAA0BJ,SAASC,eAAe,sDAClDI,yBAA2BL,SAASC,eAAe,uDAEvDF,wBAAwBO,iBAAiB,QAASV,MAAMW,aACxDR,wBAAwBO,iBAAiB,QAASV,MAAMW,aACxDL,wBAAwBI,iBAAiB,QAASV,MAAMY,kBACxDL,uBAAuBG,iBAAiB,QAASV,MAAMa,mBACvDL,wBAAwBE,iBAAiB,QAASI,qBAClDL,yBAAyBC,iBAAiB,QAASK,6BAOjDhB,eAAiB,SACfiB,MAAQZ,SAASC,eAAe,8DAA8DY,qBAAqB,KACnHC,IAAMF,MAAM,GACZG,QAAU,OAET,IAAIC,EAAI,EAAGA,EAAIJ,MAAMK,OAAQD,IAAK,KAC/BE,OAASN,MAAMI,GAAGG,QAAQD,OAED,QAAzBA,OAAOE,cACPR,MAAMI,GAAGV,iBAAiB,SAASe,QAC/BA,MAAMC,iBACND,MAAME,sBAED,IAAIC,EAAI,EAAGA,EAAIZ,MAAMK,OAAQO,IAC9BZ,MAAMY,GAAGC,UAAUC,OAAO,UAE9BL,MAAMM,OAAOF,UAAUG,OAAO,aAEF,UAAzBV,OAAOE,cACdR,MAAMI,GAAGV,iBAAiB,SAASe,QAC/BA,MAAMC,iBACND,MAAME,kBAEevB,SAASC,eAAe,sEAChCwB,UAAUC,OAAO,YAEzB,IAAIV,EAAI,EAAGA,EAAIJ,MAAMK,OAAQD,OAC1BJ,MAAMI,GAAGS,UAAUI,SAAS,UAAW,KACnCX,OAASN,MAAMI,GAAGG,QAAQD,OAC9BH,QAAQe,KAAKZ,QAKrBa,eAAeC,kBACX,6DACAC,KAAKC,UAAUnB,UAInBoB,SAASC,YAGbxB,MAAMI,GAAGV,iBAAiB,SAASe,QAC/BA,MAAMC,iBACND,MAAME,kBAENT,IAAIW,UAAUC,OAAO,UAErBL,MAAMM,OAAOF,UAAUG,OAAO,eAWxClB,oBAAuBW,WACzBA,MAAMC,iBACqC,MAAvCD,MAAMM,OAAOU,QAAQjB,cAAuB,KACxCkB,MAAQjB,MAAMM,OAAOR,QAAQoB,OACjCR,eAAeC,kBAAkB,gEAAiEM,OAElGH,SAASC,WASXzB,qBAAwBU,WAC1BA,MAAMC,iBACqC,MAAvCD,MAAMM,OAAOU,QAAQjB,cAAuB,KACxCkB,MAAQjB,MAAMM,OAAOR,QAAQoB,OACjCR,eAAeC,kBAAkB,iEAAkEM,OAEnGH,SAASC"}
\ No newline at end of file
diff --git a/report/activities_in_progress/amd/src/activities_in_progress.js b/report/activities_in_progress/amd/src/activities_in_progress.js
new file mode 100644
index 00000000..cea58fd8
--- /dev/null
+++ b/report/activities_in_progress/amd/src/activities_in_progress.js
@@ -0,0 +1,153 @@
+// 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 .
+
+/**
+ * Chart data JS module.
+ *
+ * @module assessfreqreport/activities_in_progress
+ * @package
+ * @copyright Simon Thornett
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import TableHandler from 'local_assessfreq/table_handler';
+import * as UserPreference from 'local_assessfreq/user_preferences';
+
+/**
+ * Init function.
+ * @param {Integer} context
+ */
+export const init = (context) => {
+
+ // Set up event listener and related actions for module dropdown on heatmp.
+ moduleDropdown();
+
+ let table = new TableHandler(
+ 0,
+ context,
+ 'assessfreqreport-activities-in-progress-table',
+ 'assessfreqreport_activities_in_progress',
+ 'get_in_progress_table',
+ 'assessfreqreport_activities_in_progress_table_rows_preference',
+ 'assessfreqreport_activities_in_progress_table_sort_preference',
+ 'assessfreqreport-activities-in-progress-table-search',
+ 'assessfreqreport-activities-in-progress-table',
+ 'local_assessfreq_set_table_preference'
+ );
+
+ table.getTable();
+
+ let tableSearchInputElement = document.getElementById('assessfreqreport-activities-in-progress-table-search');
+ let tableSearchResetElement = document.getElementById('assessfreqreport-activities-in-progress-table-search-reset');
+ let tableSearchRowsElement = document.getElementById('assessfreqreport-activities-in-progress-table-rows');
+ let tableSearchAheadElement = document.getElementById('assessfreqreport-activities-in-progress-hoursahead');
+ let tableSearchBehindElement = document.getElementById('assessfreqreport-activities-in-progress-hoursbehind');
+
+ tableSearchInputElement.addEventListener('keyup', table.tableSearch);
+ tableSearchInputElement.addEventListener('paste', table.tableSearch);
+ tableSearchResetElement.addEventListener('click', table.tableSearchReset);
+ tableSearchRowsElement.addEventListener('click', table.tableSearchRowSet);
+ tableSearchAheadElement.addEventListener('click', tableSearchAheadSet);
+ tableSearchBehindElement.addEventListener('click', tableSearchBehindSet);
+
+};
+
+/**
+ * Add the event listeners to the modules in the module select dropdown.
+ */
+const moduleDropdown = () => {
+ let links = document.getElementById('local-assessfreq-report-activities-in-progress-filter-type').getElementsByTagName('a');
+ let all = links[0];
+ let modules = [];
+
+ for (let i = 0; i < links.length; i++) {
+ let module = links[i].dataset.module;
+
+ if (module.toLowerCase() === 'all') {
+ links[i].addEventListener('click', event => {
+ event.preventDefault();
+ event.stopPropagation();
+ // Remove active class from all other links.
+ for (let j = 0; j < links.length; j++) {
+ links[j].classList.remove('active');
+ }
+ event.target.classList.toggle('active');
+ });
+ } else if (module.toLowerCase() === 'close') {
+ links[i].addEventListener('click', event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const dropdownmenu = document.getElementById('local-assessfreq-report-activities-in-progress-filter-type-filters');
+ dropdownmenu.classList.remove('show');
+
+ for (let i = 0; i < links.length; i++) {
+ if (links[i].classList.contains('active')) {
+ let module = links[i].dataset.module;
+ modules.push(module);
+ }
+ }
+
+ // Save selection as a user preference.
+ UserPreference.setUserPreference(
+ 'assessfreqreport_activities_in_progress_modules_preference',
+ JSON.stringify(modules)
+ );
+
+ // Reload based on selected year.
+ location.reload();
+ });
+ } else {
+ links[i].addEventListener('click', event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ all.classList.remove('active');
+
+ event.target.classList.toggle('active');
+ });
+ }
+ }
+};
+
+/**
+ * Process the hours ahead event from the student table.
+ *
+ * @param {Event} event The triggered event for the element.
+ */
+const tableSearchAheadSet = (event) => {
+ event.preventDefault();
+ if (event.target.tagName.toLowerCase() === 'a') {
+ let hours = event.target.dataset.metric;
+ UserPreference.setUserPreference('assessfreqreport_activities_in_progress_hoursahead_preference', hours);
+ // Reload based on selected year.
+ location.reload();
+ }
+};
+
+/**
+ * Process the hours behind event from the student table.
+ *
+ * @param {Event} event The triggered event for the element.
+ */
+const tableSearchBehindSet = (event) => {
+ event.preventDefault();
+ if (event.target.tagName.toLowerCase() === 'a') {
+ let hours = event.target.dataset.metric;
+ UserPreference.setUserPreference('assessfreqreport_activities_in_progress_hoursbehind_preference', hours);
+ // Reload based on selected year.
+ location.reload();
+ }
+};
diff --git a/report/activities_in_progress/classes/output/renderer.php b/report/activities_in_progress/classes/output/renderer.php
new file mode 100644
index 00000000..21e3ea0e
--- /dev/null
+++ b/report/activities_in_progress/classes/output/renderer.php
@@ -0,0 +1,346 @@
+.
+
+/**
+ * Renderer.
+ *
+ * @package assessfreqreport_activities_in_progress
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assessfreqreport_activities_in_progress\output;
+
+use context_system;
+use core\chart_bar;
+use core\chart_pie;
+use core\chart_series;
+use html_writer;
+use local_assessfreq\source_base;
+use local_assessfreq\utils;
+use paging_bar;
+use plugin_renderer_base;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/local/assessfreq/lib.php');
+
+class renderer extends plugin_renderer_base {
+
+ public function render_report($data) {
+
+ // In progress counts.
+ $contents = '';
+ foreach ($data['inprogress'] as $count) {
+ $contents .= html_writer::div($count);
+ }
+
+ $progresssummarycontainer = $this->render_from_template(
+ 'local_assessfreq/card',
+ [
+ 'header' => get_string('inprogress:head', 'assessfreqreport_activities_in_progress'),
+ 'contents' => $contents
+ ]
+ );
+
+ // Upcoming activities starting.
+ $labels = [];
+ $seriestitle = get_string('upcomingchart:activities', 'assessfreqreport_activities_in_progress');
+ $participantseries = get_string('upcomingchart:participants', 'assessfreqreport_activities_in_progress');
+
+ $seriesdata = [];
+ $participantseriesdata = [];
+
+ foreach ($data['upcoming'] as $sourceupcoming) {
+ foreach ($sourceupcoming['upcoming'] as $timestamp => $upcoming) {
+ $count = 0;
+ $participantcount = 0;
+
+ foreach ($upcoming as $activity) {
+ $count++;
+ $participantcount += $activity->participants;
+ }
+
+ foreach ($sourceupcoming['inprogress'] as $inprogress) {
+ if ($inprogress->timestampopen >= $timestamp && $inprogress->timestampopen < $timestamp + HOURSECS) {
+ $count++;
+ $participantcount += $inprogress->participants;
+ }
+ }
+
+ if (!isset($seriesdata[$timestamp])) {
+ $seriesdata[$timestamp] = 0;
+ }
+ $seriesdata[$timestamp] += $count;
+ if (!isset($participantseriesdata[$timestamp])) {
+ $participantseriesdata[$timestamp] = 0;
+ }
+ $participantseriesdata[$timestamp] += $participantcount;
+ $labels[$timestamp] = userdate(
+ $timestamp + HOURSECS,
+ get_string('upcomingchart:inprogressdatetime', 'assessfreqreport_activities_in_progress')
+ );
+ }
+ }
+ $seriesdata = array_values($seriesdata);
+ $participantseriesdata = array_values($participantseriesdata);
+ $labels = array_values($labels);
+
+ if ($seriesdata) {
+ $series = new chart_series($seriestitle, $seriesdata);
+ $participantseries = new chart_series($participantseries, $participantseriesdata);
+
+ $chart = new chart_bar();
+ $chart->add_series($series);
+ $chart->add_series($participantseries);
+ $chart->set_labels($labels);
+
+ $contents = $this->render($chart);
+ } else {
+ $contents = '';
+ }
+ $upcomingcontainer = $this->render_from_template(
+ 'local_assessfreq/card',
+ [
+ 'header' => get_string('upcomingchart:head', 'assessfreqreport_activities_in_progress'),
+ 'contents' => $contents,
+ ]
+ );
+
+ // Participant summary container.
+ $seriesdata = [
+ 'notloggedin' => 0,
+ 'loggedin' => 0,
+ 'inprogress' => 0,
+ 'finished' => 0,
+ ];
+
+ foreach ($data['participants'] as $sourceparticipants) {
+ foreach ($sourceparticipants as $status => $value) {
+ $seriesdata[$status] = $value + ($seriesdata[$status] ?? 0);
+ }
+ }
+
+ $seriesdata = array_values($seriesdata);
+
+ $labels = [
+ get_string('summarychart:notloggedin', 'assessfreqreport_activities_in_progress'),
+ get_string('summarychart:loggedin', 'assessfreqreport_activities_in_progress'),
+ get_string('summarychart:inprogress', 'assessfreqreport_activities_in_progress'),
+ get_string('summarychart:finished', 'assessfreqreport_activities_in_progress'),
+ ];
+
+ $colors = [
+ get_config('assessfreqreport_activities_in_progress', 'notloggedincolor'),
+ get_config('assessfreqreport_activities_in_progress', 'loggedincolor'),
+ get_config('assessfreqreport_activities_in_progress', 'inprogresscolor'),
+ get_config('assessfreqreport_activities_in_progress', 'finishedcolor'),
+ ];
+
+ if ($participantseriesdata) {
+ $chart = new chart_pie();
+ $chart->set_doughnut(true);
+ $participants = new chart_series(
+ get_string('summarychart:participants', 'assessfreqreport_activities_in_progress'),
+ $seriesdata
+ );
+ $participants->set_colors($colors);
+ $chart->add_series($participants);
+ $chart->set_labels($labels);
+
+ $contents = $this->render($chart);
+ } else {
+ $contents = '';
+ }
+
+ $summarycontainer = $this->render_from_template(
+ 'local_assessfreq/card',
+ [
+ 'header' => get_string('summarychart:head', 'assessfreqreport_activities_in_progress'),
+ 'contents' => $contents
+ ]
+ );
+
+ // Activies in progress container.
+ $progresscontainer = $this->render_from_template(
+ 'local_assessfreq/card',
+ [
+ 'header' => get_string('inprogresstable:head', 'assessfreqreport_activities_in_progress'),
+ 'contents' => 'No data'
+ ]
+ );
+
+ $preferencerows = get_user_preferences('assessfreqreport_activities_in_progress_table_rows_preference', 20);
+ $rows = [
+ 20 => 'rows20',
+ 50 => 'rows50',
+ 100 => 'rows100',
+ ];
+
+ $preferencehoursahead = (int)get_user_preferences('assessfreqreport_activities_in_progress_hoursahead_preference', 8);
+ $preferencehoursbehind = (int)get_user_preferences('assessfreqreport_activities_in_progress_hoursbehind_preference', 1);
+
+ $hours = [
+ 0 => 'hours0',
+ 1 => 'hours1',
+ 4 => 'hours4',
+ 8 => 'hours8',
+ ];
+
+ $preferencemodule = json_decode(
+ get_user_preferences('assessfreqreport_activities_in_progress_modules_preference', '["all"]'),
+ true
+ );
+ // Only get modules with the "get_inprogress_count" method as only these display on the report.
+ $modules = get_modules($preferencemodule, 'get_inprogress_count');
+
+ return $this->render_from_template(
+ 'assessfreqreport_activities_in_progress/activities-in-progress',
+ [
+ 'filters' => [
+ 'modules' => $modules,
+ 'hoursahead' => [$hours[$preferencehoursahead] => 'true'],
+ 'hoursbehind' => [$hours[$preferencehoursbehind] => 'true'],
+ ],
+ 'progresssummary' => $progresssummarycontainer,
+ 'upcoming' => $upcomingcontainer,
+ 'summary' => $summarycontainer,
+ 'progress' => $progresscontainer,
+ 'table' => [
+ 'id' => 'assessfreqreport-activities-in-progress',
+ 'name' => get_string('inprogresstable:head', 'assessfreqreport_activities_in_progress'),
+ 'rows' => [$rows[$preferencerows] => 'true'],
+ ]
+ ]
+ );
+ }
+
+ /**
+ * Renders the activities in progress "table" on the dashboard screen.
+ * We update the table via ajax.
+ * The table isn't a real table it's a collection of divs.
+ *
+ * @param string $search The search string for the table.
+ * @param int $page The page number of results.
+ * @param string $sorton The value to sort by.
+ * @param string $direction The direction to sort.
+ * @param int $hoursahead Amount of time in hours to look ahead for activity starting.
+ * @param int $hoursbehind Amount of time in hours to look behind for activity starting.
+ * @return string $output HTML for the table.
+ */
+ public function render_activities_inprogress_table(
+ string $search,
+ int $page,
+ string $sorton,
+ string $direction
+ ): string {
+ $now = time();
+ $hoursahead = (int)get_user_preferences('assessfreqreport_activities_in_progress_hoursahead_preference', 8);
+ $hoursbehind = (int)get_user_preferences('assessfreqreport_activities_in_progress_hoursbehind_preference', 1);
+ $sources = get_sources();
+ $inprogress = [];
+ /* @var $source source_base */
+ foreach ($sources as $source) {
+ if (method_exists($source, 'get_inprogress_data')) {
+ $inprogress[] = $source->get_inprogress_data($now, $hoursahead, $hoursbehind);
+ }
+ }
+ $pagesize = get_user_preferences('assessfreqreport_activities_in_progress_table_rows_preference', 20);
+
+ $activities = [];
+ foreach ($inprogress as $activity) {
+ array_push($activities, ...$activity['inprogress']);
+ $upcomingactivities = $activity['upcoming'];
+ $finishedactivities = $activity['finished'];
+
+ foreach ($upcomingactivities as $upcomingactivity) {
+ foreach ($upcomingactivity as $key => $upcoming) {
+ $activities[$key] = $upcoming;
+ }
+ }
+
+ foreach ($finishedactivities as $finishedactivity) {
+ foreach ($finishedactivity as $key => $finished) {
+ $activities[$key] = $finished;
+ }
+ }
+ }
+
+ if (empty($activities)) {
+ return '';
+ }
+
+ [$filtered, $totalrows] = $this->filter($activities, $search, $page, $pagesize);
+ $sortedactivities = utils::sort($filtered, $sorton, $direction);
+
+ $pagingbar = new paging_bar($totalrows, $page, $pagesize, '/');
+ $pagingoutput = $this->render($pagingbar);
+
+ $context = [
+ 'activities' => array_values($sortedactivities),
+ 'pagingbar' => $pagingoutput,
+ 'iscourse' => $this->page->course->id !== SITEID,
+ ];
+
+ return $this->render_from_template('assessfreqreport_activities_in_progress/activities-in-progress-table', $context);
+ }
+
+
+ /**
+ * Given an array of activities, filter based on a provided search string and apply pagination.
+ *
+ * @param array $activities Array of activities to search.
+ * @param string $search The string to search by.
+ * @param int $page The page number of results.
+ * @param int $pagesize The page size for results.
+ * @return array $result Array containing list of filtered activities and total of how many activities matched the filter.
+ */
+ private function filter(array $activities, string $search, int $page, int $pagesize): array {
+ $filtered = [];
+ $searchfields = ['name', 'coursefullname'];
+ $offset = $page * $pagesize;
+ $offsetcount = 0;
+ $recordcount = 0;
+
+ foreach ($activities as $id => $activity) {
+ $searchcount = 0;
+ if ($search != '') {
+ $searchcount = -1;
+ foreach ($searchfields as $searchfield) {
+ if (stripos($activity->{$searchfield}, $search) !== false) {
+ $searchcount++;
+ }
+ }
+ }
+
+ if ($searchcount > -1 && $offsetcount >= $offset && $recordcount < $pagesize) {
+ $filtered[$id] = $activity;
+ }
+
+ if ($searchcount > -1 && $offsetcount >= $offset) {
+ $recordcount++;
+ }
+
+ if ($searchcount > -1) {
+ $offsetcount++;
+ }
+ }
+
+ return [$filtered, $offsetcount];
+ }
+}
diff --git a/report/activities_in_progress/classes/report.php b/report/activities_in_progress/classes/report.php
new file mode 100644
index 00000000..74c330b6
--- /dev/null
+++ b/report/activities_in_progress/classes/report.php
@@ -0,0 +1,127 @@
+.
+
+/**
+ * Main report class.
+ *
+ * @package assessfreqreport_activities_in_progress
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assessfreqreport_activities_in_progress;
+
+use local_assessfreq\report_base;
+use local_assessfreq\source_base;
+
+class report extends report_base {
+ const WEIGHT = 30;
+
+ /**
+ * @inheritDoc
+ */
+ public function get_name() : string {
+ return get_string("tab:name", "assessfreqreport_activities_in_progress");
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get_tab_weight() : int {
+ return self::WEIGHT;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get_tablink() : string {
+ return 'activities_in_progress';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function has_access() : bool {
+ global $PAGE;
+
+ return has_capability('assessfreqreport/activities_in_progress:view', $PAGE->context);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get_contents() : string {
+ global $PAGE;
+
+ $data = [];
+ $inprogress = [];
+ $upcoming = [];
+ $participants = [];
+ $now = time();
+ $modulepreference = json_decode(
+ get_user_preferences('assessfreqreport_activities_in_progress_modules_preference', '["all"]')
+ );
+ $sources = get_sources();
+ $hoursahead = (int)get_user_preferences('assessfreqreport_activities_in_progress_hoursahead_preference', 8);
+ $hoursbehind = (int)get_user_preferences('assessfreqreport_activities_in_progress_hoursbehind_preference', 1);
+
+ foreach ($sources as $source) {
+ /* @var $source source_base */
+ if (!in_array('all', $modulepreference) && !in_array($source->get_module(), $modulepreference)) {
+ continue;
+ }
+ if (method_exists($source, 'get_inprogress_count')) {
+ $inprogress[] = $source->get_inprogress_count($now, $hoursahead, $hoursbehind);
+ }
+ if (method_exists($source, 'get_upcoming_data')) {
+ $upcoming[] = $source->get_upcoming_data($now, $hoursahead, $hoursbehind);
+ }
+ if (method_exists($source, 'get_all_participants_inprogress_data')) {
+ $participants[] = $source->get_all_participants_inprogress_data($now, $hoursahead, $hoursbehind);
+ }
+ }
+ $data['inprogress'] = $inprogress;
+ $data['upcoming'] = $upcoming;
+ $data['participants'] = $participants;
+
+ $renderer = $PAGE->get_renderer("assessfreqreport_activities_in_progress");
+
+ return $renderer->render_report($data);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function get_required_js() : void {
+ global $PAGE;
+
+ $PAGE->requires->js_call_amd(
+ 'assessfreqreport_activities_in_progress/activities_in_progress',
+ 'init',
+ [$PAGE->context->id]
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function get_required_css(): void {
+ global $PAGE;
+
+ $PAGE->requires->css('/local/assessfreq/report/activities_in_progress/styles.css');
+ }
+}
diff --git a/report/activities_in_progress/db/access.php b/report/activities_in_progress/db/access.php
new file mode 100644
index 00000000..7e3d2ed6
--- /dev/null
+++ b/report/activities_in_progress/db/access.php
@@ -0,0 +1,34 @@
+.
+
+/**
+ * Access file.
+ *
+ * @package assessfreqreport_activities_in_progress
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = [
+ 'assessfreqreport/activities_in_progress:view' => [
+ 'captype' => 'read',
+ 'contextlevel' => CONTEXT_COURSE,
+ 'archetypes' => [],
+ ],
+];
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
new file mode 100644
index 00000000..4c5857bb
--- /dev/null
+++ b/report/activities_in_progress/lang/en/assessfreqreport_activities_in_progress.php
@@ -0,0 +1,80 @@
+.
+
+/**
+ * Lang file.
+ *
+ * @package assessfreqreport_activities_in_progress
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'Report - Activities in Progress';
+
+$string['tab:name'] = 'Activities in Progress';
+
+$string['activities_in_progress:view'] = 'Ability to view the activities in progress report.';
+
+$string['settings:chartheading'] = 'Chart settings';
+$string['settings:chartheading_desc'] = 'These settings allow you to configure the the settings used in the charts and graphs';
+$string['settings:notloggedincolor'] = 'Not logged in color';
+$string['settings:notloggedincolor_desc'] = 'Select color to display for not logged in users in charts';
+$string['settings:loggedincolor'] = 'Logged in color';
+$string['settings:loggedincolor_desc'] = 'Select color to display for logged in users in charts';
+$string['settings:inprogresscolor'] = 'In progress color';
+$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:graphsheading'] = 'Graph settings';
+$string['settings:graphsheading_desc'] = 'Specify the graph settings for each graph report';
+
+$string['filter:selectassessment'] = 'Select assessment type';
+$string['filter:closeapply'] = 'Close and apply';
+$string['filter:header'] = 'Filters';
+$string['filter:submit'] = 'Filter';
+$string['filter:hours0'] = 'Now';
+$string['filter:hours1'] = '1 Hour';
+$string['filter:hours4'] = '4 Hours';
+$string['filter:hours8'] = '8 Hours';
+$string['filter:hoursahead'] = 'Hours ahead';
+$string['filter:hoursbehind'] = 'Hours behind';
+
+$string['inprogress'] = 'In progress';
+$string['inprogress:head'] = 'In progress';
+
+$string['upcomingchart:head'] = 'Upcoming activities starting';
+$string['upcomingchart:inprogressdatetime'] = '%H:00';
+$string['upcomingchart:activities'] = 'Activities';
+$string['upcomingchart:participants'] = 'Students';
+
+$string['summarychart:head'] = 'Participant summary';
+$string['summarychart:participants'] = 'Students';
+$string['summarychart:notloggedin'] = 'Not logged in';
+$string['summarychart:loggedin'] = 'Logged in';
+$string['summarychart:inprogress'] = 'In progress';
+$string['summarychart:finished'] = 'Finished';
+
+$string['inprogresstable:head'] = 'Activies in progress';
+$string['inprogresstable:activity'] = 'Activity';
+$string['inprogresstable:course'] = 'Course';
+$string['inprogresstable:timelimit'] = 'Time limit';
+$string['inprogresstable:timeopen'] = 'Time open';
+$string['inprogresstable:timeclose'] = 'Time close';
+$string['inprogresstable:participants'] = 'Participants (Overrides)';
+$string['inprogresstable:dashboard'] = 'Dashboard';
+
+$string['report:usage_guidlines'] = '';
diff --git a/report/activities_in_progress/lib.php b/report/activities_in_progress/lib.php
new file mode 100644
index 00000000..373555de
--- /dev/null
+++ b/report/activities_in_progress/lib.php
@@ -0,0 +1,86 @@
+.
+
+/**
+ * @package assessfreqreport_activities_in_progress
+ * @copyright 2024 Simon Thornett
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Returns the name of the user preferences as well as the details this plugin uses.
+ *
+ * @return array
+ */
+function assessfreqreport_activities_in_progress_user_preferences() : array {
+
+ $preferences['assessfreqreport_activities_in_progress_modules_preference'] = [
+ 'null' => NULL_NOT_ALLOWED,
+ 'default' => '[]',
+ 'type' => PARAM_RAW,
+ ];
+
+ $preferences['assessfreqreport_activities_in_progress_table_rows_preference'] = [
+ 'null' => NULL_NOT_ALLOWED,
+ 'default' => 20,
+ 'type' => PARAM_INT,
+ ];
+
+ $preferences['assessfreqreport_activities_in_progress_table_sort_preference'] = [
+ 'null' => NULL_NOT_ALLOWED,
+ 'default' => 'name_asc',
+ 'type' => PARAM_ALPHAEXT,
+ ];
+
+ $preferences['assessfreqreport_activities_in_progress_hoursahead_preference'] = [
+ 'null' => NULL_NOT_ALLOWED,
+ 'default' => 8,
+ 'type' => PARAM_INT,
+ ];
+
+ $preferences['assessfreqreport_activities_in_progress_hoursbehind_preference'] = [
+ 'null' => NULL_NOT_ALLOWED,
+ 'default' => 1,
+ 'type' => PARAM_INT,
+ ];
+
+ return $preferences;
+}
+
+/**
+ * Renders the user table on the dashboard screen.
+ * We update the table via ajax.
+ *
+ * @param array $args
+ * @return string $o Form HTML.
+ */
+function assessfreqreport_activities_in_progress_output_fragment_get_in_progress_table(array $args) : string {
+ global $PAGE;
+
+ require_capability('assessfreqreport/activities_in_progress:view', $PAGE->context);
+
+ $sortpreference = explode(
+ '_',
+ get_user_preferences('assessfreqreport_activities_in_progress_table_sort_preference', 'name_asc')
+ );
+ $data = json_decode($args['data']);
+ $search = is_null($data->search) ? '' : $data->search;
+ $sorton = $sortpreference[0];
+ $direction = $sortpreference[1];
+
+ $output = $PAGE->get_renderer('assessfreqreport_activities_in_progress');
+ return $output->render_activities_inprogress_table($search, $data->page, $sorton, $direction);
+}
diff --git a/report/activities_in_progress/settings.php b/report/activities_in_progress/settings.php
new file mode 100644
index 00000000..6d1e0359
--- /dev/null
+++ b/report/activities_in_progress/settings.php
@@ -0,0 +1,65 @@
+.
+
+/**
+ * Settings file.
+ *
+ * @package assessfreqreport_activities_in_progress
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if (!$hassiteconfig) {
+ return;
+}
+
+// Graph settings.
+$settings->add(new admin_setting_heading(
+ 'assessfreqreport_activities_in_progress/graphsheading',
+ get_string('settings:graphsheading', 'assessfreqreport_activities_in_progress'),
+ get_string('settings:graphsheading_desc', 'assessfreqreport_activities_in_progress')
+));
+
+$settings->add(new admin_setting_configcolourpicker(
+ 'assessfreqreport_activities_in_progress/notloggedincolor',
+ get_string('settings:notloggedincolor', 'assessfreqreport_activities_in_progress'),
+ get_string('settings:notloggedincolor_desc', 'assessfreqreport_activities_in_progress'),
+ '#8C0010'
+));
+
+$settings->add(new admin_setting_configcolourpicker(
+ 'assessfreqreport_activities_in_progress/loggedincolor',
+ get_string('settings:loggedincolor', 'assessfreqreport_activities_in_progress'),
+ get_string('settings:loggedincolor_desc', 'assessfreqreport_activities_in_progress'),
+ '#FA8900'
+));
+
+$settings->add(new admin_setting_configcolourpicker(
+ 'assessfreqreport_activities_in_progress/inprogresscolor',
+ get_string('settings:inprogresscolor', 'assessfreqreport_activities_in_progress'),
+ get_string('settings:inprogresscolor_desc', 'assessfreqreport_activities_in_progress'),
+ '#875692'
+));
+
+$settings->add(new admin_setting_configcolourpicker(
+ 'assessfreqreport_activities_in_progress/finishedcolor',
+ get_string('settings:finishedcolor', 'assessfreqreport_activities_in_progress'),
+ get_string('settings:finishedcolor_desc', 'assessfreqreport_activities_in_progress'),
+ '#1B8700'
+));
diff --git a/report/activities_in_progress/styles.css b/report/activities_in_progress/styles.css
new file mode 100644
index 00000000..dd9125ba
--- /dev/null
+++ b/report/activities_in_progress/styles.css
@@ -0,0 +1,3 @@
+#local-assessfreq-report-activities-in-progress .chart-area .chart-image {
+ width: 100% !important;
+}
\ No newline at end of file
diff --git a/report/activities_in_progress/templates/activities-in-progress-table.mustache b/report/activities_in_progress/templates/activities-in-progress-table.mustache
new file mode 100644
index 00000000..34d7bd2f
--- /dev/null
+++ b/report/activities_in_progress/templates/activities-in-progress-table.mustache
@@ -0,0 +1,78 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template assessfreqreport_activities_in_progress/activities-in-progress-table
+
+ Report Summary template.
+
+ Example context (json):
+ {
+ "activities": 1,
+ "context": 1
+ }
+}}
+
\ No newline at end of file
diff --git a/templates/quiz-dashboard-cards.mustache b/report/activities_in_progress/templates/activities-in-progress.mustache
similarity index 50%
rename from templates/quiz-dashboard-cards.mustache
rename to report/activities_in_progress/templates/activities-in-progress.mustache
index 22da4c96..f2ce8b59 100644
--- a/templates/quiz-dashboard-cards.mustache
+++ b/report/activities_in_progress/templates/activities-in-progress.mustache
@@ -15,7 +15,7 @@
along with Moodle. If not, see .
}}
{{!
- @template local_assessfreq/quiz-dashboard-cards
+ @template assessfreqreport_activities_in_progress/activities-in-progress
Report Summary template.
@@ -24,22 +24,26 @@
}
}}
+
-
diff --git a/report/activities_in_progress/templates/filter-hoursahead.mustache b/report/activities_in_progress/templates/filter-hoursahead.mustache
new file mode 100644
index 00000000..4f6ae0e8
--- /dev/null
+++ b/report/activities_in_progress/templates/filter-hoursahead.mustache
@@ -0,0 +1,76 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template local_assessfreq/nav-quiz-table-hoursahead-filter
+
+ This template renders the day range selector for the timeline view.
+
+ Example context (json):
+ {}
+}}
+
diff --git a/report/activities_in_progress/templates/filter-hoursbehind.mustache b/report/activities_in_progress/templates/filter-hoursbehind.mustache
new file mode 100644
index 00000000..147f0295
--- /dev/null
+++ b/report/activities_in_progress/templates/filter-hoursbehind.mustache
@@ -0,0 +1,76 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template local_assessfreq/nav-quiz-table-hoursbehind-filter
+
+ This template renders the day range selector for the timeline view.
+
+ Example context (json):
+ {}
+}}
+
diff --git a/templates/nav-assess-type-filter.mustache b/report/activities_in_progress/templates/filter-type.mustache
similarity index 62%
rename from templates/nav-assess-type-filter.mustache
rename to report/activities_in_progress/templates/filter-type.mustache
index 448e9455..5c60ae18 100644
--- a/templates/nav-assess-type-filter.mustache
+++ b/report/activities_in_progress/templates/filter-type.mustache
@@ -15,27 +15,27 @@
along with Moodle. If not, see .
}}
{{!
- @template local_assessfreq/nav-assess-type-filter
+ @template assessfreqreport_activities_in_progress/filter-type
- This template renders the day range selector for the timeline view.
+ This template renders the type filter.
Example context (json):
{}
}}
-
diff --git a/report/activities_in_progress/templates/filters.mustache b/report/activities_in_progress/templates/filters.mustache
new file mode 100644
index 00000000..6fb0b8b7
--- /dev/null
+++ b/report/activities_in_progress/templates/filters.mustache
@@ -0,0 +1,28 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template assessfreqreport_activities_in_progress/filters
+
+ tab template.
+}}
+
+
diff --git a/report/activities_in_progress/version.php b/report/activities_in_progress/version.php
new file mode 100644
index 00000000..dd49fa85
--- /dev/null
+++ b/report/activities_in_progress/version.php
@@ -0,0 +1,33 @@
+.
+
+/**
+ * Version file.
+ *
+ * @package assessfreqreport_activities_in_progress
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->component = 'assessfreqreport_activities_in_progress';
+$plugin->release = '2024040300';
+$plugin->version = 2024040300;
+$plugin->requires = 2022041906; // Requires 4.0
+$plugin->supported = [400, 401];
+$plugin->maturity = MATURITY_STABLE;
\ No newline at end of file
diff --git a/report/activity_dashboard/amd/build/activity_dashboard.min.js b/report/activity_dashboard/amd/build/activity_dashboard.min.js
new file mode 100644
index 00000000..1aba659a
--- /dev/null
+++ b/report/activity_dashboard/amd/build/activity_dashboard.min.js
@@ -0,0 +1,11 @@
+define("assessfreqreport_activity_dashboard/activity_dashboard",["exports","assessfreqreport_activity_dashboard/form_modal"],(function(_exports,FormModal){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)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,FormModal=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}
+/**
+ * Chart data JS module.
+ *
+ * @module assessfreqreport/activity_dashboard
+ * @package
+ * @copyright Simon Thornett
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */(FormModal);_exports.init=(context,incourse)=>{FormModal.init(context,incourse)}}));
+
+//# sourceMappingURL=activity_dashboard.min.js.map
\ No newline at end of file
diff --git a/report/activity_dashboard/amd/build/activity_dashboard.min.js.map b/report/activity_dashboard/amd/build/activity_dashboard.min.js.map
new file mode 100644
index 00000000..58362932
--- /dev/null
+++ b/report/activity_dashboard/amd/build/activity_dashboard.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"activity_dashboard.min.js","sources":["../src/activity_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 assessfreqreport/activity_dashboard\n * @package\n * @copyright Simon Thornett \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as FormModal from 'assessfreqreport_activity_dashboard/form_modal';\n\n/**\n * Init function.\n * @param {int} context\n * @param {boolean} incourse\n */\nexport const init = (context, incourse) => {\n FormModal.init(context, incourse); // Create modal for activity selection modal.\n};\n"],"names":["context","incourse","FormModal","init"],"mappings":";;;;;;;;+BA+BoB,CAACA,QAASC,YAC1BC,UAAUC,KAAKH,QAASC"}
\ No newline at end of file
diff --git a/report/activity_dashboard/amd/build/form_modal.min.js b/report/activity_dashboard/amd/build/form_modal.min.js
new file mode 100644
index 00000000..64425991
--- /dev/null
+++ b/report/activity_dashboard/amd/build/form_modal.min.js
@@ -0,0 +1,10 @@
+/**
+ * Javascript for report card display and processing.
+ *
+ * @package
+ * @copyright 2020 Matt Porritt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define("assessfreqreport_activity_dashboard/form_modal",["core/str","core/modal","local_assessfreq/modal_large","core/fragment","core/ajax","core/templates"],(function(Str,Modal,ModalLarge,Fragment,Ajax,Templates){let contextid,iscourse,modalObj,FormModal={},resetOptions=[];const spinner='
',observerConfig={attributes:!0,childList:!1,subtree:!0};FormModal.init=function(context,course){contextid=context,iscourse=course,createModal(),document.getElementById("local-assessfreq-find-activity").addEventListener("click",displayModalForm)};const createModal=function(){Str.get_string("modal:loading","assessfreqreport_activity_dashboard","","").then((title=>{Modal.create({type:ModalLarge.TYPE,title:title,body:spinner,large:!0}).then((modal=>{modalObj=modal,modalObj.getRoot().on("click","#id_submitbutton",processModalForm),modalObj.getRoot().on("click","#id_cancel",(e=>{e.preventDefault(),modalObj.setBody(spinner),modalObj.hide()}))}))}))},displayModalForm=function(){updateModalBody(),modalObj.show()},updateModalBody=function(){let formdata=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},params={jsonformdata:JSON.stringify(formdata)};getOptionPlaceholders().then((()=>{Str.get_string("modal:searchactivity","assessfreqreport_activity_dashboard","","").then((title=>{modalObj.setTitle(title),Fragment.loadFragment("assessfreqreport_activity_dashboard","search_form",contextid,params).done(((response,js)=>{modalObj.setBody(response),js&&Templates.runTemplateJS(js),iscourse&&updateActivities(document.getElementsByName("coursechoice")[0].value)}));let modalContainer=document.querySelectorAll('[data-region*="modal-container"]')[0];observer.observe(modalContainer,observerConfig)}))}))},updateActivities=function(courseid){Ajax.call([{methodname:"local_assessfreq_get_activities",args:{courseid:courseid}}])[0].done((response=>{let activityArray=JSON.parse(response),selectElement=document.getElementById("id_activity"),selectElementLength=selectElement.options.length;null!==document.getElementById("noactivitywarning")&&document.getElementById("noactivitywarning").remove();for(let j=selectElementLength-1;j>=0;j--)selectElement.options[j]=null;if(activityArray.length>0){for(let k=0;k{selectElement.appendChild(option)})),document.getElementById("id_activity").value=0,selectElement.disabled=!0}))},observer=new MutationObserver((function(mutationsList){for(let i=0;i{Str.get_strings([{key:"modal:selectcourse",component:"assessfreqreport_activity_dashboard"},{key:"modal:loadingactivity",component:"assessfreqreport_activity_dashboard"}]).then((stringReturn=>{for(let i=0;i{let element=document.createElement("div");element.innerHTML=warning,element.id="noactivitywarning",element.classList.add("alert","alert-danger"),modalObj.getBody().prepend(element)}));else{modalObj.hide(),modalObj.setBody(""),observer.disconnect();let params=new URLSearchParams(location.search);params.set("activityid",activityId),window.location.search=params.toString()}};return FormModal}));
+
+//# sourceMappingURL=form_modal.min.js.map
\ No newline at end of file
diff --git a/report/activity_dashboard/amd/build/form_modal.min.js.map b/report/activity_dashboard/amd/build/form_modal.min.js.map
new file mode 100644
index 00000000..2626fa08
--- /dev/null
+++ b/report/activity_dashboard/amd/build/form_modal.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"form_modal.min.js","sources":["../src/form_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 .\n\n/**\n * Javascript for report card display and processing.\n *\n * @package\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n ['core/str', 'core/modal', 'local_assessfreq/modal_large', 'core/fragment', 'core/ajax', 'core/templates'],\n function(Str, Modal, ModalLarge, Fragment, Ajax, Templates) {\n\n /**\n * Module level variables.\n */\n let FormModal = {};\n let contextid;\n let iscourse;\n let modalObj;\n let resetOptions = [];\n\n const spinner = '
'\n + ''\n + '
';\n\n const observerConfig = {attributes: true, childList: false, subtree: true};\n\n /**\n * Initialise method for activity dashboard rendering.\n * @param {int} context\n * @param {boolean} course\n */\n FormModal.init = function(context, course) {\n contextid = context;\n iscourse = course;\n\n createModal();\n document.getElementById('local-assessfreq-find-activity').addEventListener('click', displayModalForm);\n };\n\n /**\n * Create the modal window.\n *\n * @private\n */\n const createModal = function() {\n // eslint-disable-next-line promise/catch-or-return,promise/always-return\n Str.get_string('modal:loading', 'assessfreqreport_activity_dashboard', '', '').then((title) => {\n // Create the Modal.\n Modal.create({\n type: ModalLarge.TYPE,\n title: title,\n body: spinner,\n large: true\n }).then((modal) => {\n modalObj = modal;\n\n // Explicitly handle form click events.\n modalObj.getRoot().on('click', '#id_submitbutton', processModalForm);\n modalObj.getRoot().on('click', '#id_cancel', (e) => {\n e.preventDefault();\n modalObj.setBody(spinner);\n modalObj.hide();\n });\n });\n });\n };\n\n /**\n * Display the Modal form.\n */\n const displayModalForm = function() {\n updateModalBody();\n modalObj.show();\n };\n\n /**\n * Updates the body of the modal window.\n *\n * @param {Object} formdata\n * @private\n */\n const updateModalBody = function(formdata = {}) {\n\n let params = {\n 'jsonformdata': JSON.stringify(formdata)\n };\n\n // eslint-disable-next-line promise/catch-or-return\n getOptionPlaceholders()\n // eslint-disable-next-line promise/always-return\n .then(() => {\n // eslint-disable-next-line promise/always-return\n Str.get_string('modal:searchactivity', 'assessfreqreport_activity_dashboard', '', '').then((title) => {\n modalObj.setTitle(title);\n Fragment.loadFragment('assessfreqreport_activity_dashboard', 'search_form', contextid, params)\n .done((response, js) => {\n modalObj.setBody(response);\n if (js) {\n Templates.runTemplateJS(js);\n }\n if (iscourse) {\n updateActivities(document.getElementsByName(\"coursechoice\")[0].value);\n }\n });\n let modalContainer = document.querySelectorAll('[data-region*=\"modal-container\"]')[0];\n observer.observe(modalContainer, observerConfig);\n });\n });\n };\n\n const updateActivities = function(courseid) {\n Ajax.call([{\n methodname: 'local_assessfreq_get_activities',\n args: {\n courseid: courseid\n },\n }])[0].done((response) => {\n let activityArray = JSON.parse(response);\n let selectElement = document.getElementById('id_activity');\n let selectElementLength = selectElement.options.length;\n if (document.getElementById('noactivitywarning') !== null) {\n document.getElementById('noactivitywarning').remove();\n }\n // Clear exisitng options.\n for (let j = selectElementLength - 1; j >= 0; j--) {\n selectElement.options[j] = null;\n }\n\n if (activityArray.length > 0) {\n // Add new options.\n for (let k = 0; k < activityArray.length; k++) {\n let opt = activityArray[k];\n let el = document.createElement('option');\n el.textContent = opt.name;\n el.value = opt.id;\n selectElement.appendChild(el);\n }\n selectElement.removeAttribute('disabled');\n if (document.getElementById('noactivitywarning') !== null) {\n document.getElementById('noactivitywarning').remove();\n }\n } else {\n resetOptions.forEach((option) => {\n selectElement.appendChild(option);\n });\n document.getElementById('id_activity').value = 0;\n selectElement.disabled = true;\n }\n });\n };\n\n const ObserverCallback = function(mutationsList) {\n for (let i = 0; i < mutationsList.length; i++) {\n let element = mutationsList[i].target;\n if (element.tagName.toLowerCase() === 'span' && element.classList.contains('badge')) {\n element.addEventListener('click', updateModalBody);\n updateActivities(mutationsList[i].target.dataset.value);\n break;\n }\n }\n };\n\n const observer = new MutationObserver(ObserverCallback);\n\n const getOptionPlaceholders = function() {\n return new Promise((resolve) => {\n const stringArr = [\n {key: 'modal:selectcourse', component: 'assessfreqreport_activity_dashboard'},\n {key: 'modal:loadingactivity', component: 'assessfreqreport_activity_dashboard'},\n ];\n\n Str.get_strings(stringArr).then(stringReturn => { // Save string to global to be used later.\n // eslint-disable-next-line promise/always-return\n for (let i = 0; i < stringReturn.length; i++) {\n let el = document.createElement('option');\n el.textContent = stringReturn[i];\n el.value = 0 - i;\n resetOptions.push(el);\n }\n resolve();\n });\n });\n };\n\n /**\n * Updates Moodle form with selected information.\n *\n * @param {Object} e\n * @private\n */\n const processModalForm = function(e) {\n e.preventDefault(); // Stop modal from closing.\n\n let activityElement = document.getElementById('id_activity');\n let activityId = activityElement.options[activityElement.selectedIndex].value;\n let courseId = document.getElementsByName(\"coursechoice\")[0].value;\n\n if (courseId === undefined || activityId < 1) {\n if (document.getElementById('noactivitywarning') === null) {\n // eslint-disable-next-line promise/always-return\n Str.get_string('modal:noactivityselected', 'assessfreqreport_activity_dashboard', '', '').then((warning) => {\n let element = document.createElement('div');\n element.innerHTML = warning;\n element.id = 'noactivitywarning';\n element.classList.add('alert', 'alert-danger');\n modalObj.getBody().prepend(element);\n });\n }\n } else {\n modalObj.hide(); // Close modal.\n modalObj.setBody(''); // Cleaer form.\n observer.disconnect(); // Remove observer.\n\n // Trigger redirect with activityid.\n let params = new URLSearchParams(location.search);\n params.set('activityid', activityId);\n window.location.search = params.toString();\n }\n\n };\n\n return FormModal;\n }\n);\n"],"names":["define","Str","Modal","ModalLarge","Fragment","Ajax","Templates","contextid","iscourse","modalObj","FormModal","resetOptions","spinner","observerConfig","attributes","childList","subtree","init","context","course","createModal","document","getElementById","addEventListener","displayModalForm","get_string","then","title","create","type","TYPE","body","large","modal","getRoot","on","processModalForm","e","preventDefault","setBody","hide","updateModalBody","show","formdata","params","JSON","stringify","getOptionPlaceholders","setTitle","loadFragment","done","response","js","runTemplateJS","updateActivities","getElementsByName","value","modalContainer","querySelectorAll","observer","observe","courseid","call","methodname","args","activityArray","parse","selectElement","selectElementLength","options","length","remove","j","k","opt","el","createElement","textContent","name","id","appendChild","removeAttribute","forEach","option","disabled","MutationObserver","mutationsList","i","element","target","tagName","toLowerCase","classList","contains","dataset","Promise","resolve","get_strings","key","component","stringReturn","push","activityElement","activityId","selectedIndex","undefined","warning","innerHTML","add","getBody","prepend","disconnect","URLSearchParams","location","search","set","window","toString"],"mappings":"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"}
\ No newline at end of file
diff --git a/report/activity_dashboard/amd/src/activity_dashboard.js b/report/activity_dashboard/amd/src/activity_dashboard.js
new file mode 100644
index 00000000..13505649
--- /dev/null
+++ b/report/activity_dashboard/amd/src/activity_dashboard.js
@@ -0,0 +1,34 @@
+// 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 .
+
+/**
+ * Chart data JS module.
+ *
+ * @module assessfreqreport/activity_dashboard
+ * @package
+ * @copyright Simon Thornett
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import * as FormModal from 'assessfreqreport_activity_dashboard/form_modal';
+
+/**
+ * Init function.
+ * @param {int} context
+ * @param {boolean} incourse
+ */
+export const init = (context, incourse) => {
+ FormModal.init(context, incourse); // Create modal for activity selection modal.
+};
diff --git a/report/activity_dashboard/amd/src/form_modal.js b/report/activity_dashboard/amd/src/form_modal.js
new file mode 100644
index 00000000..55501765
--- /dev/null
+++ b/report/activity_dashboard/amd/src/form_modal.js
@@ -0,0 +1,240 @@
+// 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 .
+
+/**
+ * Javascript for report card display and processing.
+ *
+ * @package
+ * @copyright 2020 Matt Porritt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+ ['core/str', 'core/modal', 'local_assessfreq/modal_large', 'core/fragment', 'core/ajax', 'core/templates'],
+ function(Str, Modal, ModalLarge, Fragment, Ajax, Templates) {
+
+ /**
+ * Module level variables.
+ */
+ let FormModal = {};
+ let contextid;
+ let iscourse;
+ let modalObj;
+ let resetOptions = [];
+
+ const spinner = '
'
+ + ''
+ + '
';
+
+ const observerConfig = {attributes: true, childList: false, subtree: true};
+
+ /**
+ * Initialise method for activity dashboard rendering.
+ * @param {int} context
+ * @param {boolean} course
+ */
+ FormModal.init = function(context, course) {
+ contextid = context;
+ iscourse = course;
+
+ createModal();
+ document.getElementById('local-assessfreq-find-activity').addEventListener('click', displayModalForm);
+ };
+
+ /**
+ * Create the modal window.
+ *
+ * @private
+ */
+ const createModal = function() {
+ // eslint-disable-next-line promise/catch-or-return,promise/always-return
+ Str.get_string('modal:loading', 'assessfreqreport_activity_dashboard', '', '').then((title) => {
+ // Create the Modal.
+ Modal.create({
+ type: ModalLarge.TYPE,
+ title: title,
+ body: spinner,
+ large: true
+ }).then((modal) => {
+ modalObj = modal;
+
+ // Explicitly handle form click events.
+ modalObj.getRoot().on('click', '#id_submitbutton', processModalForm);
+ modalObj.getRoot().on('click', '#id_cancel', (e) => {
+ e.preventDefault();
+ modalObj.setBody(spinner);
+ modalObj.hide();
+ });
+ });
+ });
+ };
+
+ /**
+ * Display the Modal form.
+ */
+ const displayModalForm = function() {
+ updateModalBody();
+ modalObj.show();
+ };
+
+ /**
+ * Updates the body of the modal window.
+ *
+ * @param {Object} formdata
+ * @private
+ */
+ const updateModalBody = function(formdata = {}) {
+
+ let params = {
+ 'jsonformdata': JSON.stringify(formdata)
+ };
+
+ // eslint-disable-next-line promise/catch-or-return
+ getOptionPlaceholders()
+ // eslint-disable-next-line promise/always-return
+ .then(() => {
+ // eslint-disable-next-line promise/always-return
+ Str.get_string('modal:searchactivity', 'assessfreqreport_activity_dashboard', '', '').then((title) => {
+ modalObj.setTitle(title);
+ Fragment.loadFragment('assessfreqreport_activity_dashboard', 'search_form', contextid, params)
+ .done((response, js) => {
+ modalObj.setBody(response);
+ if (js) {
+ Templates.runTemplateJS(js);
+ }
+ if (iscourse) {
+ updateActivities(document.getElementsByName("coursechoice")[0].value);
+ }
+ });
+ let modalContainer = document.querySelectorAll('[data-region*="modal-container"]')[0];
+ observer.observe(modalContainer, observerConfig);
+ });
+ });
+ };
+
+ const updateActivities = function(courseid) {
+ Ajax.call([{
+ methodname: 'local_assessfreq_get_activities',
+ args: {
+ courseid: courseid
+ },
+ }])[0].done((response) => {
+ let activityArray = JSON.parse(response);
+ let selectElement = document.getElementById('id_activity');
+ let selectElementLength = selectElement.options.length;
+ if (document.getElementById('noactivitywarning') !== null) {
+ document.getElementById('noactivitywarning').remove();
+ }
+ // Clear exisitng options.
+ for (let j = selectElementLength - 1; j >= 0; j--) {
+ selectElement.options[j] = null;
+ }
+
+ if (activityArray.length > 0) {
+ // Add new options.
+ for (let k = 0; k < activityArray.length; k++) {
+ let opt = activityArray[k];
+ let el = document.createElement('option');
+ el.textContent = opt.name;
+ el.value = opt.id;
+ selectElement.appendChild(el);
+ }
+ selectElement.removeAttribute('disabled');
+ if (document.getElementById('noactivitywarning') !== null) {
+ document.getElementById('noactivitywarning').remove();
+ }
+ } else {
+ resetOptions.forEach((option) => {
+ selectElement.appendChild(option);
+ });
+ document.getElementById('id_activity').value = 0;
+ selectElement.disabled = true;
+ }
+ });
+ };
+
+ const ObserverCallback = function(mutationsList) {
+ for (let i = 0; i < mutationsList.length; i++) {
+ let element = mutationsList[i].target;
+ if (element.tagName.toLowerCase() === 'span' && element.classList.contains('badge')) {
+ element.addEventListener('click', updateModalBody);
+ updateActivities(mutationsList[i].target.dataset.value);
+ break;
+ }
+ }
+ };
+
+ const observer = new MutationObserver(ObserverCallback);
+
+ const getOptionPlaceholders = function() {
+ return new Promise((resolve) => {
+ const stringArr = [
+ {key: 'modal:selectcourse', component: 'assessfreqreport_activity_dashboard'},
+ {key: 'modal:loadingactivity', component: 'assessfreqreport_activity_dashboard'},
+ ];
+
+ Str.get_strings(stringArr).then(stringReturn => { // Save string to global to be used later.
+ // eslint-disable-next-line promise/always-return
+ for (let i = 0; i < stringReturn.length; i++) {
+ let el = document.createElement('option');
+ el.textContent = stringReturn[i];
+ el.value = 0 - i;
+ resetOptions.push(el);
+ }
+ resolve();
+ });
+ });
+ };
+
+ /**
+ * Updates Moodle form with selected information.
+ *
+ * @param {Object} e
+ * @private
+ */
+ const processModalForm = function(e) {
+ e.preventDefault(); // Stop modal from closing.
+
+ let activityElement = document.getElementById('id_activity');
+ let activityId = activityElement.options[activityElement.selectedIndex].value;
+ let courseId = document.getElementsByName("coursechoice")[0].value;
+
+ if (courseId === undefined || activityId < 1) {
+ if (document.getElementById('noactivitywarning') === null) {
+ // eslint-disable-next-line promise/always-return
+ Str.get_string('modal:noactivityselected', 'assessfreqreport_activity_dashboard', '', '').then((warning) => {
+ let element = document.createElement('div');
+ element.innerHTML = warning;
+ element.id = 'noactivitywarning';
+ element.classList.add('alert', 'alert-danger');
+ modalObj.getBody().prepend(element);
+ });
+ }
+ } else {
+ modalObj.hide(); // Close modal.
+ modalObj.setBody(''); // Cleaer form.
+ observer.disconnect(); // Remove observer.
+
+ // Trigger redirect with activityid.
+ let params = new URLSearchParams(location.search);
+ params.set('activityid', activityId);
+ window.location.search = params.toString();
+ }
+
+ };
+
+ return FormModal;
+ }
+);
diff --git a/report/activity_dashboard/classes/form/search_form.php b/report/activity_dashboard/classes/form/search_form.php
new file mode 100644
index 00000000..f2f7573d
--- /dev/null
+++ b/report/activity_dashboard/classes/form/search_form.php
@@ -0,0 +1,100 @@
+.
+
+/**
+ * Form to search for activities.
+ *
+ * @package local_assessfreq
+ * @copyright 2020 Matt Porritt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assessfreqreport_activity_dashboard\form;
+
+use html_writer;
+use moodleform;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->libdir/formslib.php");
+
+/**
+ * Form to search for activities.
+ *
+ * @package local_assessfreq
+ * @copyright 2020 Matt Porritt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class search_form extends moodleform {
+
+ /**
+ * Build form for the broadcast message.
+ *
+ * {@inheritDoc}
+ * @see moodleform::definition
+ */
+ public function definition() {
+ global $PAGE;
+
+ $mform = $this->_form;
+ $mform->disable_form_change_checker();
+
+ // Form heading.
+ $mform->addElement(
+ 'html',
+ html_writer::div(get_string('form:searchactivityform', 'assessfreqreport_activity_dashboard'), 'form-description mb-3')
+ );
+
+ if ($PAGE->course->id == SITEID) {
+ $courseoptions = [
+ 'multiple' => false,
+ 'placeholder' => get_string('form:entercourse', 'assessfreqreport_activity_dashboard'),
+ 'noselectionstring' => get_string('form:nocourse', 'assessfreqreport_activity_dashboard'),
+ 'ajax' => 'local_assessfreq/course_selector',
+ 'casesensitive' => false,
+ ];
+ $mform->addElement('autocomplete', 'courses', get_string('course'), [], $courseoptions);
+
+ $mform->addElement('hidden', 'coursechoice', '0');
+ $selectoptions = [
+ 0 => get_string('form:selectcourse', 'assessfreqreport_activity_dashboard'),
+ -1 => get_string('form:loadingactivity', 'assessfreqreport_activity_dashboard'),
+ ];
+ } else {
+ $mform->addElement(
+ 'html',
+ html_writer::div($PAGE->course->fullname, 'form-description mb-3')
+ );
+ $mform->addElement('hidden', 'coursechoice', $PAGE->course->id);
+
+ $selectoptions = [
+ -1 => get_string('form:loadingactivity', 'assessfreqreport_activity_dashboard'),
+ ];
+ }
+ $mform->setType('coursechoice', PARAM_INT);
+
+ $mform->addElement(
+ 'select',
+ 'activity',
+ get_string('form:activity', 'assessfreqreport_activity_dashboard'),
+ $selectoptions
+ );
+ $mform->disabledIf('activity', 'coursechoice', 'eq', '0');
+
+ $btnstring = get_string('form:selectactivity', 'assessfreqreport_activity_dashboard');
+ $this->add_action_buttons(true, $btnstring);
+ }
+}
diff --git a/report/activity_dashboard/classes/output/renderer.php b/report/activity_dashboard/classes/output/renderer.php
new file mode 100644
index 00000000..7b01cb9a
--- /dev/null
+++ b/report/activity_dashboard/classes/output/renderer.php
@@ -0,0 +1,60 @@
+.
+
+/**
+ * Renderer.
+ *
+ * @package assessfreqreport_activity_dashboard
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assessfreqreport_activity_dashboard\output;
+
+use local_assessfreq\source_base;
+use plugin_renderer_base;
+
+class renderer extends plugin_renderer_base {
+
+ /**
+ * Generate the HTML for the report.
+ *
+ * @return bool|string
+ */
+ public function render_report() {
+
+ $activityid = optional_param('activityid', 0, PARAM_INT);
+ $sources = get_sources();
+
+ $report = '';
+ if ($activityid) {
+ [$course, $cm] = get_course_and_cm_from_cmid($activityid);
+ if (isset($sources[$cm->modname])) {
+ /* @var $source source_base */
+ $source = $sources[$cm->modname];
+ if (method_exists($source, 'get_activity_dashboard')) {
+ $report = $source->get_activity_dashboard($cm, $course);
+ }
+ }
+ }
+
+ return $this->render_from_template(
+ 'assessfreqreport_activity_dashboard/activity-dashboard',
+ ['report' => $report, 'activity' => '']
+ );
+ }
+}
diff --git a/report/activity_dashboard/classes/report.php b/report/activity_dashboard/classes/report.php
new file mode 100644
index 00000000..ec332e3b
--- /dev/null
+++ b/report/activity_dashboard/classes/report.php
@@ -0,0 +1,96 @@
+.
+
+/**
+ * Main report class.
+ *
+ * @package assessfreqreport_activity_dashboard
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assessfreqreport_activity_dashboard;
+
+use context_system;
+use local_assessfreq\report_base;
+
+class report extends report_base {
+ const WEIGHT = 20;
+
+ /**
+ * @inheritDoc
+ */
+ public function get_name() : string {
+ return get_string("tab:name", "assessfreqreport_activity_dashboard");
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get_tab_weight() : int {
+ return self::WEIGHT;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get_tablink() : string {
+ return 'activity_dashboard';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function has_access() : bool {
+ global $PAGE;
+
+ return has_capability('assessfreqreport/activity_dashboard:view', $PAGE->context);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get_contents() : string {
+ global $PAGE;
+
+ $renderer = $PAGE->get_renderer("assessfreqreport_activity_dashboard");
+
+ return $renderer->render_report();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function get_required_js() : void {
+ global $PAGE;
+
+ $PAGE->requires->js_call_amd(
+ 'assessfreqreport_activity_dashboard/activity_dashboard',
+ 'init',
+ [$PAGE->context->id, $PAGE->course->id != SITEID]
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function get_required_css(): void {
+ global $PAGE;
+
+ $PAGE->requires->css('/local/assessfreq/report/activity_dashboard/styles.css');
+ }
+}
diff --git a/report/activity_dashboard/db/access.php b/report/activity_dashboard/db/access.php
new file mode 100644
index 00000000..ed0bac11
--- /dev/null
+++ b/report/activity_dashboard/db/access.php
@@ -0,0 +1,34 @@
+.
+
+/**
+ * Access file.
+ *
+ * @package assessfreqreport_activity_dashboard
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = [
+ 'assessfreqreport/activity_dashboard:view' => [
+ 'captype' => 'read',
+ 'contextlevel' => CONTEXT_COURSE,
+ 'archetypes' => [],
+ ],
+];
diff --git a/report/activity_dashboard/lang/en/assessfreqreport_activity_dashboard.php b/report/activity_dashboard/lang/en/assessfreqreport_activity_dashboard.php
new file mode 100644
index 00000000..6bcb8f57
--- /dev/null
+++ b/report/activity_dashboard/lang/en/assessfreqreport_activity_dashboard.php
@@ -0,0 +1,66 @@
+.
+
+/**
+ * Lang file.
+ *
+ * @package assessfreqreport_activity_dashboard
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'Report - Activity Dashboard';
+
+$string['tab:name'] = 'Activity Dashboard';
+
+$string['activity_dashboard:view'] = 'Ability to view the activity dashboard report.';
+
+$string['searchactivity'] = 'Search for activity';
+
+$string['settings:chartheading'] = 'Chart settings';
+$string['settings:chartheading_desc'] = 'These settings allow you to configure the the settings used in the charts and graphs';
+$string['settings:notloggedincolor'] = 'Not logged in color';
+$string['settings:notloggedincolor_desc'] = 'Select color to display for not logged in users in charts';
+$string['settings:loggedincolor'] = 'Logged in color';
+$string['settings:loggedincolor_desc'] = 'Select color to display for logged in users in charts';
+$string['settings:inprogresscolor'] = 'In progress color';
+$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 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';
+$string['form:entersearch'] = 'Enter search text';
+$string['form:loadingactivity'] = 'Loading activity';
+$string['form:nocourse'] = 'No course';
+$string['form:searchactivityform'] = 'Search and select the activity to display on the dashboard';
+$string['form:selectactivity'] = 'Select activity';
+$string['form:selectcourse'] = 'Select course';
+
+$string['modal:loading'] = 'Loading';
+$string['modal:loadingactivity'] = 'Loading activities';
+$string['modal:noactivityselected'] = 'No activity selected';
+$string['modal:searchactivity'] = 'Search for activity';
+$string['modal:selectcourse'] = 'Select course';
diff --git a/report/activity_dashboard/lib.php b/report/activity_dashboard/lib.php
new file mode 100644
index 00000000..733945d4
--- /dev/null
+++ b/report/activity_dashboard/lib.php
@@ -0,0 +1,44 @@
+.
+
+/**
+ * Lib file.
+ *
+ * @package assessfreqreport_activity_dashboard
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use assessfreqreport_activity_dashboard\form\search_form;
+
+/**
+ * Renders the search form for the modal on the dashboard.
+ *
+ * @param array $args
+ * @return string $o Form HTML.
+ */
+function assessfreqreport_activity_dashboard_output_fragment_search_form($args) : string {
+
+ $mform = new search_form(null, null, 'post', '', ['class' => 'ignoredirty']);
+
+ ob_start();
+ $mform->display();
+ $o = ob_get_contents();
+ ob_end_clean();
+
+ return $o;
+}
diff --git a/report/activity_dashboard/settings.php b/report/activity_dashboard/settings.php
new file mode 100644
index 00000000..a8e4f106
--- /dev/null
+++ b/report/activity_dashboard/settings.php
@@ -0,0 +1,80 @@
+.
+
+/**
+ * Settings file.
+ *
+ * @package assessfreqreport_activity_dashboard
+ * @author Simon Thornett
+ * @copyright Catalyst IT, 2024
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if (!$hassiteconfig) {
+ return;
+}
+
+// Chart settings.
+$settings->add(new admin_setting_heading(
+ 'assessfreqreport_activity_dashboard/chartheading',
+ get_string('settings:chartheading', 'assessfreqreport_activity_dashboard'),
+ get_string('settings:chartheading_desc', 'assessfreqreport_activity_dashboard')
+));
+
+require_once($CFG->dirroot . '/local/assessfreq/settingslib.php');
+$settings->add(new admin_setting_configint(
+ 'assessfreqreport_activity_dashboard/trendcount',
+ get_string('settings:trendcount', 'assessfreqreport_activity_dashboard'),
+ get_string('settings:trendcount_desc', 'assessfreqreport_activity_dashboard'),
+ 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'),
+ get_string('settings:notloggedincolor_desc', 'assessfreqreport_activity_dashboard'),
+ '#8C0010'
+));
+
+$settings->add(new admin_setting_configcolourpicker(
+ 'assessfreqreport_activity_dashboard/loggedincolor',
+ get_string('settings:loggedincolor', 'assessfreqreport_activity_dashboard'),
+ get_string('settings:loggedincolor_desc', 'assessfreqreport_activity_dashboard'),
+ '#FA8900'
+));
+
+$settings->add(new admin_setting_configcolourpicker(
+ 'assessfreqreport_activity_dashboard/inprogresscolor',
+ get_string('settings:inprogresscolor', 'assessfreqreport_activity_dashboard'),
+ get_string('settings:inprogresscolor_desc', 'assessfreqreport_activity_dashboard'),
+ '#875692'
+));
+
+$settings->add(new admin_setting_configcolourpicker(
+ 'assessfreqreport_activity_dashboard/finishedcolor',
+ get_string('settings:finishedcolor', 'assessfreqreport_activity_dashboard'),
+ get_string('settings:finishedcolor_desc', 'assessfreqreport_activity_dashboard'),
+ '#1B8700'
+));
diff --git a/report/activity_dashboard/styles.css b/report/activity_dashboard/styles.css
new file mode 100644
index 00000000..f8a392b5
--- /dev/null
+++ b/report/activity_dashboard/styles.css
@@ -0,0 +1,49 @@
+#local-assessfreq-report-activity-dashboard table {
+ width: 100%;
+ margin-top: 10px;
+}
+
+#local-assessfreq-report-activity-dashboard td,
+#local-assessfreq-report-activity-dashboard th {
+ padding: 10px;
+ border: 1px solid #ddd;
+ text-align: left;
+}
+
+#local-assessfreq-report-activity-dashboard thead {
+ background-color: rgba(0, 0, 0, .03);
+}
+
+#local-assessfreq-report-activity-dashboard tr:nth-of-type(2n) {
+ background-color: rgba(0, 0, 0, .03);
+}
+
+#local-assessfreq-report-activity-dashboard .title {
+ font-weight: bold;
+}
+
+#local-assessfreq-report-activity-dashboard tr.empty {
+ height: 20px;
+}
+
+
+#local-assessfreq-report-activity-dashboard .local-assessfreq-status-icon {
+ display: block;
+ float: left;
+ width: 20px;
+ height: 20px;
+ margin-right: 5px;
+ margin-top: 2px;
+ border-radius: 3px;
+ box-shadow: 0 3px 4px rgba(0, 0, 0, .3);
+}
+
+#local-assessfreq-report-activity-dashboard .local-assessfreq-override-status {
+ font-weight: 600;
+ text-shadow: 0 1px 1px rgba(0, 0, 0, .3);
+}
+
+#local-assessfreq-report-activity-dashboard .local-assessfreq-disabled {
+ font-weight: 200;
+ color: grey;
+}
diff --git a/report/activity_dashboard/templates/activity-dashboard.mustache b/report/activity_dashboard/templates/activity-dashboard.mustache
new file mode 100644
index 00000000..2919e6f4
--- /dev/null
+++ b/report/activity_dashboard/templates/activity-dashboard.mustache
@@ -0,0 +1,42 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template assessfreqreport_activity_dashboard/activity-dashboard
+
+ Report Summary template.
+
+ Example context (json):
+ {
+
+ }
+}}
+
diff --git a/report/heatmap/templates/download.mustache b/report/heatmap/templates/download.mustache
new file mode 100644
index 00000000..bcc29ca4
--- /dev/null
+++ b/report/heatmap/templates/download.mustache
@@ -0,0 +1,40 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template assessfreqreport_heatmap/download
+
+ Download template.
+}}
+
+
+
+
+
+
diff --git a/report/heatmap/templates/filter-metric.mustache b/report/heatmap/templates/filter-metric.mustache
new file mode 100644
index 00000000..d476cfbc
--- /dev/null
+++ b/report/heatmap/templates/filter-metric.mustache
@@ -0,0 +1,56 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template assessfreqreport_heatmap/filter-metric
+
+ This template renders the metric filter.
+
+ Example context (json):
+ {}
+}}
+
+
+
+
diff --git a/report/heatmap/templates/filter-type.mustache b/report/heatmap/templates/filter-type.mustache
new file mode 100644
index 00000000..b2050bce
--- /dev/null
+++ b/report/heatmap/templates/filter-type.mustache
@@ -0,0 +1,60 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template assessfreqreport_heatmap/filter-type
+
+ This template renders the type filter.
+
+ Example context (json):
+ {}
+}}
+
+
+
+
diff --git a/templates/nav-year-heat-filter.mustache b/report/heatmap/templates/filter-year.mustache
similarity index 61%
rename from templates/nav-year-heat-filter.mustache
rename to report/heatmap/templates/filter-year.mustache
index caf72c6a..3911f315 100644
--- a/templates/nav-year-heat-filter.mustache
+++ b/report/heatmap/templates/filter-year.mustache
@@ -15,29 +15,29 @@
along with Moodle. If not, see .
}}
{{!
- @template local_assessfreq/nav-year-heat-filter
+ @template assessfreqreport_heatmap/filter-year
- This template renders the day range selector for the timeline view.
+ This template renders the year filter.
Example context (json):
{}
}}
-
+
diff --git a/report/heatmap/templates/filters.mustache b/report/heatmap/templates/filters.mustache
new file mode 100644
index 00000000..a5ad206e
--- /dev/null
+++ b/report/heatmap/templates/filters.mustache
@@ -0,0 +1,56 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template assessfreqreport_heatmap/filters
+
+ This template renders all of the filters.
+}}
+
+
diff --git a/report/student_search/templates/filter-hoursbehind.mustache b/report/student_search/templates/filter-hoursbehind.mustache
new file mode 100644
index 00000000..e6dff5fb
--- /dev/null
+++ b/report/student_search/templates/filter-hoursbehind.mustache
@@ -0,0 +1,76 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template assessfreqreport_student_search/filter-hoursbehind
+
+ This template renders the day range selector for the timeline view.
+
+ Example context (json):
+ {}
+}}
+
diff --git a/report/student_search/templates/filters.mustache b/report/student_search/templates/filters.mustache
new file mode 100644
index 00000000..c03b719e
--- /dev/null
+++ b/report/student_search/templates/filters.mustache
@@ -0,0 +1,27 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template assessfreqreport_student_search/filters
+
+ tab template.
+}}
+
+
diff --git a/report/summary_graphs/templates/filters.mustache b/report/summary_graphs/templates/filters.mustache
new file mode 100644
index 00000000..f4b1c3f5
--- /dev/null
+++ b/report/summary_graphs/templates/filters.mustache
@@ -0,0 +1,28 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template assessfreqreport_summary_graphs/filters
+
+ tab template.
+}}
+
+