diff --git a/README.md b/README.md
index a98fcba9..d001320d 100644
--- a/README.md
+++ b/README.md
@@ -46,3 +46,30 @@ 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.
+In addition the report now loads in a tabbed format instead of in different locations.
+
+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 context menu under reports) 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
diff --git a/amd/build/calendar.min.js b/amd/build/calendar.min.js
deleted file mode 100644
index 45d125d1..00000000
--- a/amd/build/calendar.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-function _slicedToArray(a,b){return _arrayWithHoles(a)||_iterableToArrayLimit(a,b)||_unsupportedIterableToArray(a,b)||_nonIterableRest()}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _unsupportedIterableToArray(a,b){if(!a)return;if("string"==typeof a)return _arrayLikeToArray(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return _arrayLikeToArray(a,b)}function _arrayLikeToArray(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);cg;g++){if("undefined"!=typeof f[g]){for(var l=f[g],m=0;32>m;m++){if("undefined"!=typeof l[m]){e.push(l[m].number)}}}}h=Math.max.apply(Math,e);k=Math.min.apply(Math,e)}else{h=0;k=0}c(a)})},t=function(a){if(a==k){return 1}var b=h-k,c=(a-k)/b,d=Math.round(c*5+1);if(1>d){d=1}if(6s;s++){j=document.createElement("th");j.innerHTML=g[s];p.appendChild(j)}o.appendChild(q);m.appendChild(o);m.appendChild(p);r.appendChild(m);r.appendChild(n);l.appendChild(r);f.appendChild(l);h++}if("undefined"==typeof b||"undefined"==typeof c||"undefined"==typeof d){e(Error("Failed to create calendar tables."))}else{a({calendarContainer:f,year:b,startMonth:c})}})},x=function(a){for(var b="",c=0,d=Object.entries(a);c"+m[f]+": "+g+" "}return b},y=function(a,b,c){for(var d=new Date(b,c).getDay(),e=v(b,c+1),f=1,g=0,h;6>g;g++){h=document.createElement("tr");for(var q=0;7>q;q++){if(0===g&&qp(c,b)){break}else{k=document.createElement("td");m=document.createTextNode(f);if("undefined"!=typeof e&&e.hasOwnProperty(f)){var j=t(e[f].number);if(0==n[j]||n[j]>e[f].number){n[j]=e[f].number}k.style.backgroundColor=l[j];k.style.color=o(l[j]);k.dataset.toggle="tooltip";k.dataset.html="true";k.dataset.event="true";k.dataset.date=b+"-"+(c+1)+"-"+f;k.title=x(e[f]);k.style.cursor="pointer"}f++}k.appendChild(m);h.appendChild(k)}a.appendChild(h)}},z=function(a){var b=a.calendarContainer,c=a.year,d=a.startMonth;return new Promise(function(a,e){for(var f=b.getElementsByTagName("tbody"),g=d,h=0,j;he;e++){if(0!==n[e]){var f=document.createElement("td"),g=document.createTextNode(n[e]+"+");f.appendChild(g);f.style.backgroundColor=l[e];f.style.color=o(l[e]);d.appendChild(f)}}c.appendChild(d);b.appendChild(c);n={1:0,2:0,3:0,4:0,5:0,6:0};a(b)})};d.generate=function(c,d,e,h,i){return new Promise(function(j,k){var l={year:c,startMonth:d,endMonth:e},m={year:c,metric:h,modules:i};a.get_strings(f).catch(function(){b.exception(new Error("Failed to load strings"))}).then(function(a){g=a;return m}).then(u).then(function(a){s(a,l)}).then(q).then(r).then(function(){return l}).then(w).then(z).then(function(a){if("undefined"!=typeof a){j(a)}else{k(Error("Could not generate calendar"))}})})};return d});
-//# sourceMappingURL=calendar.min.js.map
diff --git a/amd/build/calendar.min.js.map b/amd/build/calendar.min.js.map
deleted file mode 100644
index 3bac26b8..00000000
--- a/amd/build/calendar.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["../src/calendar.js"],"names":["define","Str","Notification","Ajax","Calendar","eventArray","stringArr","key","component","stringResult","heatRangeMax","heatRangeMin","colorArray","processModules","heatRangeScale","getContrast","hexcolor","slice","r","parseInt","substr","g","b","daysInMonth","month","year","Date","getDate","getHeatColors","Promise","resolve","reject","call","methodname","args","done","response","JSON","parse","fail","Error","getProcessModules","calcHeatRange","dateObj","eventArrayLength","Object","keys","length","eventcount","i","j","push","number","Math","max","min","getHeat","eventCount","localRange","localPercent","heat","round","getEvents","metric","modules","jsonArgs","stringify","jsondata","getMonthEvents","monthevents","createTables","startMonth","endMonth","calendarContainer","document","createElement","container","classList","add","table","thead","tbody","id","monthRow","dayrow","monthHeader","colSpan","innerHTML","dayHeader","appendChild","getTooltip","dayArray","tipHTML","entries","value","populateCalendarDays","firstDay","getDay","monthEvents","date","row","cell","cellText","createTextNode","dataset","event","hasOwnProperty","style","backgroundColor","color","toggle","html","title","cursor","populateCalendar","tables","getElementsByTagName","createHeatScale","trow","generate","eventObj","get_strings","catch","exception","then","stringReturn","calendarHTML"],"mappings":"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","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 * @package local_assessfreq\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} 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\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 * @param {Number} year The year to get the events for.\n * @param {String} metric The type of metric to get, 'students' or 'assess'.\n * @param {Array} modules Array of the modules to get.\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 * @oaram {Number} year The year to generate the tables for.\n * @param {Number} startMonth The month to start table generation from.\n * @param {Number} endMonth The month to generate the tables to.\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\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 }\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} calendarContainer the container to populate.\n * @param {Number} year The year to generate calendar for.\n * @param {Number} startMonth The month to start generation from.\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\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"],"file":"calendar.min.js"}
\ 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 d9f4d651..00000000
--- a/amd/build/chart_data.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("local_assessfreq/chart_data",["exports","core/fragment","core/notification","core/str","core/templates"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=a.getCardCharts=void 0;b=h(b);c=h(c);d=g(d);e=h(e);function f(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;f=function(){return a};return a}function g(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=f();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var g=d?Object.getOwnPropertyDescriptor(a,e):null;if(g&&(g.get||g.set)){Object.defineProperty(c,e,g)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function h(a){return a&&a.__esModule?a:{default:a}}var i,j,k,l,m=function(a,f,g){i.forEach(function(h){var i=document.getElementById(h.cardId),m=i.getElementsByClassName("overlay-icon-container")[0],n=i.getElementsByClassName("chart-body")[0],o={call:h.call};if(f){o.hoursahead=f[0];o.hoursbehind=f[1]}if(a){o.quiz=a}if(g){o.year=g}var p={data:JSON.stringify(o)};m.classList.remove("hide");b.default.loadFragment("local_assessfreq",k,j,p).done(function(a){var b=JSON.parse(a);if(!0===b.hasdata){var f={withtable:!0,chartdata:JSON.stringify(b.chart)};if("undefined"!=typeof h.aspect){f.aspect=h.aspect}e.default.render(l,f).done(function(a,b){m.classList.add("hide");e.default.replaceNodeContents(n,a,b)}).fail(function(){c.default.exception(new Error("Failed to load chart template."))})}else{d.get_string("nodata","local_assessfreq").then(function(a){var b=document.createElement("h3");b.innerHTML=a;n.innerHTML=b.outerHTML;m.classList.add("hide")}).catch(function(){c.default.exception(new Error("Failed to load string: nodata"))})}}).fail(function(){c.default.exception(new Error("Failed to load card."))})})};a.getCardCharts=m;a.init=function init(a,b,c,d){i=a;j=b;k=c;l=d}});
-//# sourceMappingURL=chart_data.min.js.map
diff --git a/amd/build/chart_data.min.js.map b/amd/build/chart_data.min.js.map
deleted file mode 100644
index 352ad826..00000000
--- a/amd/build/chart_data.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["../src/chart_data.js"],"names":["cards","contextId","fragment","template","getCardCharts","quizId","hoursFilter","yearSelect","forEach","cardData","cardElement","document","getElementById","cardId","spinner","getElementsByClassName","chartBody","values","call","hoursahead","hoursbehind","quiz","year","params","JSON","stringify","classList","remove","Fragment","loadFragment","done","response","resObj","parse","hasdata","context","chart","aspect","Templates","render","html","js","add","replaceNodeContents","fail","Notification","exception","Error","Str","get_string","then","str","noDatastr","createElement","innerHTML","outerHTML","catch","init","cardsArray","contextIdChart","fragmentChart","templateChart"],"mappings":"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","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 * @package local_assessfreq\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"],"file":"chart_data.min.js"}
\ 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 bbf31b5f..00000000
--- a/amd/build/chart_output_chartjs.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-define ("local_assessfreq/chart_output_chartjs",["core/chart_output_chartjs"],function(a){var b={},c=!1,d=!1;a.prototype._makeConfig=function(){var a={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())}}},b=this._chart.getLegendOptions();if(b){a.options.legend=b}if(d){a.options.legend=d}this._chart.getXAxes().forEach(function(b,c){var d=b.getLabels();a.options.scales=a.options.scales||{};a.options.scales.xAxes=a.options.scales.xAxes||[];a.options.scales.xAxes[c]=this._makeAxisConfig(b,"x",c);if(null!==d){a.options.scales.xAxes[c].ticks.callback=function(a,b){return d[b]||""}}a.options.scales.xAxes[c].stacked=this._isStacked()}.bind(this));this._chart.getYAxes().forEach(function(b,c){var d=b.getLabels();a.options.scales=a.options.scales||{};a.options.scales.yAxes=a.options.scales.yAxes||[];a.options.scales.yAxes[c]=this._makeAxisConfig(b,"y",c);if(null!==d){a.options.scales.yAxes[c].ticks.callback=function(a){return d[parseInt(a,10)]||""}}a.options.scales.yAxes[c].stacked=this._isStacked()}.bind(this));a.options.tooltips={callbacks:{label:this._makeTooltip.bind(this)}};a.options.maintainAspectRatio=c;return a};b.init=function(b,e,f,g){c=f;d=g;new a(b,e)};return b});
-//# sourceMappingURL=chart_output_chartjs.min.js.map
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 37919818..00000000
--- a/amd/build/chart_output_chartjs.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["../src/chart_output_chartjs.js"],"names":["define","Output","ChartOutput","aspectRatio","rtLegendoptions","prototype","_makeConfig","config","type","_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":"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","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 * @package local_assessfreq\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 * @param {module:core/chart_axis} axis The axis.\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 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"],"file":"chart_output_chartjs.min.js"}
\ No newline at end of file
diff --git a/amd/build/course_selector.min.js b/amd/build/course_selector.min.js
index 396c4f30..a69f54f1 100644
--- a/amd/build/course_selector.min.js
+++ b/amd/build/course_selector.min.js
@@ -1,2 +1,12 @@
-define ("local_assessfreq/course_selector",["core/ajax","core/notification"],function(a,b){return{transport:function(c,d,e){a.call([{methodname:"local_assessfreq_get_courses",args:{query:d}}])[0].then(function(a){var b=JSON.parse(a);e(b)}).fail(function(){b.exception(new Error("Failed to get events"))})},processResults:function(a,b){var c=[];b.forEach(function(a){c.push({value:a.id,label:a.fullname})});return c}}});
-//# sourceMappingURL=course_selector.min.js.map
+/**
+ * Frameworks datasource.
+ *
+ * This module is compatible with core/form-autocomplete.
+ *
+ * @packagetool_lpmigrate
+ * @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){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)})).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}));
+
+//# 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 935ac513..6db776df 100644
--- a/amd/build/course_selector.min.js.map
+++ b/amd/build/course_selector.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/course_selector.js"],"names":["define","Ajax","Notification","transport","selector","query","callback","call","methodname","args","then","response","courseArray","JSON","parse","fail","exception","Error","processResults","results","options","forEach","element","push","value","id","label","fullname"],"mappings":"AAyBAA,OAAM,oCAAC,CAAC,WAAD,CAAc,mBAAd,CAAD,CAAqC,SAASC,CAAT,CAAeC,CAAf,CAA6B,CAgDpE,MA3CqB,CAUNC,SAVM,CAUM,SAASC,CAAT,CAAmBC,CAAnB,CAA0BC,CAA1B,CAAoC,CAC3DL,CAAI,CAACM,IAAL,CAAU,CAAC,CACPC,UAAU,CAAE,8BADL,CAEPC,IAAI,CAAE,CACFJ,KAAK,CAAEA,CADL,CAFC,CAAD,CAAV,EAKI,CALJ,EAKOK,IALP,CAKY,SAACC,CAAD,CAAc,CACtB,GAAIC,CAAAA,CAAW,CAAGC,IAAI,CAACC,KAAL,CAAWH,CAAX,CAAlB,CACAL,CAAQ,CAACM,CAAD,CACX,CARD,EAQGG,IARH,CAQQ,UAAM,CACVb,CAAY,CAACc,SAAb,CAAuB,GAAIC,CAAAA,KAAJ,CAAU,sBAAV,CAAvB,CACH,CAVD,CAWH,CAtBoB,CA+BNC,cA/BM,CA+BW,SAASd,CAAT,CAAmBe,CAAnB,CAA4B,CACxD,GAAIC,CAAAA,CAAO,CAAG,EAAd,CACAD,CAAO,CAACE,OAAR,CAAgB,SAACC,CAAD,CAAa,CACzBF,CAAO,CAACG,IAAR,CAAa,CACTC,KAAK,CAAEF,CAAO,CAACG,EADN,CAETC,KAAK,CAAEJ,CAAO,CAACK,QAFN,CAAb,CAIH,CALD,EAOA,MAAOP,CAAAA,CACV,CAzCoB,CA4CxB,CAjDK,CAAN","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"],"file":"course_selector.min.js"}
\ 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 }).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,CASrBA,UAA2B,SAASC,SAAUC,MAAOC,UACjDL,KAAKM,KAAK,CAAC,CACPC,WAAY,+BACZC,KAAM,CACFJ,MAAOA,UAEX,GAAGK,MAAMC,eACLC,YAAcC,KAAKC,MAAMH,UAE7BL,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
diff --git a/amd/build/dashboard.min.js b/amd/build/dashboard.min.js
new file mode 100644
index 00000000..45562ff9
--- /dev/null
+++ b/amd/build/dashboard.min.js
@@ -0,0 +1,3 @@
+define("local_assessfreq/dashboard",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=()=>{tabs()};const tabs=()=>{document.getElementsByClassName("tablinks").forEach((el=>el.addEventListener("click",(event=>{let target=event.target.dataset.target,tabcontent=document.getElementsByClassName("tabcontent");for(let i=0;i1?urlParts[1]:null;anchor&&null!==document.querySelector('[data-target="tab-'+anchor+'"]')?document.querySelector('[data-target="tab-'+anchor+'"]').click():document.querySelector('[data-target="tab-heatmap"]').click()}}));
+
+//# 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..2f8e0917
--- /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\nexport const init = () => {\n\n // Load the tab cuntionality.\n tabs();\n\n};\n\nconst tabs = () => {\n\n const tabcontent = document.getElementsByClassName(\"tablinks\");\n\n tabcontent.forEach(el => el.addEventListener('click', event => {\n let target = event.target.dataset.target;\n\n let tabcontent = document.getElementsByClassName(\"tabcontent\");\n for (let i = 0; i < tabcontent.length; i++) {\n tabcontent[i].style.display = \"none\";\n }\n\n // Get all elements with class=\"tablinks\" and remove the class \"active\"\n let tablinks = document.getElementsByClassName(\"tablinks\");\n for (let i = 0; i < tablinks.length; i++) {\n tablinks[i].className = tablinks[i].className.replace(\" active\", \"\");\n }\n\n // Show the current tab, and add an \"active\" class to the button that opened the tab\n document.getElementById(target).style.display = \"block\";\n event.currentTarget.className += \" active\";\n }));\n\n const currentUrl = document.URL;\n const urlParts = currentUrl.split('#');\n\n const anchor = (urlParts.length > 1) ? urlParts[1] : null;\n // First tab should be open by default unless we have an anchor.\n if (!anchor || document.querySelector('[data-target=\"tab-' + anchor + '\"]') === null) {\n document.querySelector('[data-target=\"tab-heatmap\"]').click();\n } else {\n document.querySelector('[data-target=\"tab-' + anchor + '\"]').click();\n }\n};\n"],"names":["tabs","document","getElementsByClassName","forEach","el","addEventListener","event","target","dataset","tabcontent","i","length","style","display","tablinks","className","replace","getElementById","currentTarget","urlParts","URL","split","anchor","querySelector","click"],"mappings":"+JAwBoB,KAGhBA,cAIEA,KAAO,KAEUC,SAASC,uBAAuB,YAExCC,SAAQC,IAAMA,GAAGC,iBAAiB,SAASC,YAC9CC,OAASD,MAAMC,OAAOC,QAAQD,OAE9BE,WAAaR,SAASC,uBAAuB,kBAC5C,IAAIQ,EAAI,EAAGA,EAAID,WAAWE,OAAQD,IACnCD,WAAWC,GAAGE,MAAMC,QAAU,WAI9BC,SAAWb,SAASC,uBAAuB,gBAC1C,IAAIQ,EAAI,EAAGA,EAAII,SAASH,OAAQD,IACjCI,SAASJ,GAAGK,UAAYD,SAASJ,GAAGK,UAAUC,QAAQ,UAAW,IAIrEf,SAASgB,eAAeV,QAAQK,MAAMC,QAAU,QAChDP,MAAMY,cAAcH,WAAa,qBAI/BI,SADalB,SAASmB,IACAC,MAAM,KAE5BC,OAAUH,SAASR,OAAS,EAAKQ,SAAS,GAAK,KAEhDG,QAA2E,OAAjErB,SAASsB,cAAc,qBAAuBD,OAAS,MAGlErB,SAASsB,cAAc,qBAAuBD,OAAS,MAAME,QAF7DvB,SAASsB,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 5747a610..00000000
--- a/amd/build/dashboard_assessment.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}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(a,b,c,d,e,f,g){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=j(b);c=j(c);d=i(d);e=j(e);f=i(f);g=j(g);function h(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;h=function(){return a};return a}function i(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=h();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function j(a){return a&&a.__esModule?a:{default:a}}var k,l,m,n,o,p="",q="",r=[{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"}],s=function(a){a.preventDefault();var b=a.target;if("a"===b.tagName.toLowerCase()&&b.dataset.year!==l){l=b.dataset.year;f.setUserPreference("local_assessfreq_overview_year_preference",l);var c=document.getElementById("local-assessfreq-report-overview").getElementsByClassName("local-assessfreq-year")[0];c.innerHTML=l;d.getCardCharts(0,null,l)}},t=function(){clearTimeout(o);o=setTimeout(x(),750)},u=function(a){var b=a.target;if("td"===b.tagName.toLowerCase()&&"true"===b.dataset.event){e.default.display(b.dataset.date)}},v=function(){var a=JSON.parse(q),d=parseInt(a.year),e=a.metric,f=a.modules,g=document.getElementById("local-assessfreq-report-heatmap"),h=g.getElementsByClassName("overlay-icon-container")[0];h.classList.remove("hide");c.default.generate(d,0,11,e,f).then(function(a){var b=document.getElementById("local-assessfreq-report-heatmap-months");b.innerHTML=a.innerHTML;b.addEventListener("click",u)}).then(c.default.createHeatScale).then(function(a){var b=document.getElementById("local-assessfreq-report-heatmap-scale");b.innerHTML=a.outerHTML;h.classList.add("hide")}).catch(function(){b.default.exception(new Error("Failed to calendar."))})},w=function(a){var b=a.year,c=a.metric,d=a.modules,e=document.getElementById("local-assessfreq-heatmap-form"),f=e.elements,g=[];if(0===d.length){d=["all"]}for(var l=0;l.\n\n/**\n * Javascript for report card display and processing.\n *\n * @module local_assessfreq/dashboard_assessment\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 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\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/**\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"],"file":"dashboard_assessment.min.js"}
\ 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 544d244b..00000000
--- a/amd/build/dashboard_quiz.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}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(a,b,c,d,e,f,g,h,i,j,k){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=n(b);c=n(c);d=m(d);e=n(e);f=m(f);g=m(g);h=n(h);i=m(i);j=m(j);k=m(k);function l(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;l=function(){return a};return a}function m(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=l();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function n(a){return a&&a.__esModule?a:{default:a}}var o="",p,q=0,r=60,s,t=[{cardId:"local-assessfreq-quiz-summary-graph",call:"participant_summary",aspect:!0},{cardId:"local-assessfreq-quiz-summary-trend",call:"participant_trend",aspect:!1}],u=function(){var a=0";g.innerHTML=b.name+" ";g.appendChild(s);var t=new URL(window.location.href),v=t.origin+t.pathname+"?id="+q;history.pushState({},"",v);d.get_string("dashboard:quiztitle","local_assessfreq",{quiz:b.name,course:b.courseshortname}).then(function(a){document.title=a}).catch(function(){c.default.exception(new Error("Failed to load string: dashboard:quiztitle"))});e.default.render("local_assessfreq/quiz-summary-card-content",b).done(function(a){l.classList.add("hide");var b=document.getElementById("local-assessfreq-quiz-summary-card-content");e.default.replaceNodeContents(b,a,"")}).fail(function(){c.default.exception(new Error("Failed to load quiz summary template."))});h.classList.remove("hide");j.classList.remove("hide");m.classList.remove("hide");n.classList.remove("hide");f.getCardCharts(q);i.getTable(q);u();o.addEventListener("keyup",i.tableSearch);o.addEventListener("paste",i.tableSearch);p.addEventListener("click",i.tableSearchReset);r.addEventListener("click",i.tableSearchRowSet)}).fail(function(){c.default.exception(new Error("Failed to get quiz data"))})},w=function(a){a.preventDefault();var b=a.target;if(null!==b.closest("button")&&"local-assessfreq-refresh-quiz-dashboard"===b.closest("button").id){u(!0);v(q)}else if("a"===b.tagName.toLowerCase()){r=b.dataset.period;u(!0);j.setUserPreference("local_assessfreq_quiz_refresh_preference",r)}},x=function(a){var b=a.target.closest("div").dataset.call,c={data:JSON.stringify({quiz:q,call:b})};k.zoomGraph(a,c,"get_quiz_chart")},y=function(a,b){p=a;g.init(a,v);k.init(a);h.default.init(a,v);i.init(q,p,"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");f.init(t,a,"get_quiz_chart","local_assessfreq/chart");d.get_string("loadingquiztitle","local_assessfreq").then(function(a){o=a}).catch(function(){c.default.exception(new Error("Failed to load string: loadingquiz"))}).then(function(){if(0.\n\n/**\n * Javascript for report card display and processing.\n *\n * @module local_assessfreq/dashboard_quiz\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 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/**\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"],"file":"dashboard_quiz.min.js"}
\ 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 394c4239..00000000
--- a/amd/build/dashboard_quiz_inprogress.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}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(a,b,c,d,e,f,g,h){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=k(b);c=k(c);d=k(d);e=j(e);f=j(f);g=j(g);h=j(h);function i(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;i=function(){return a};return a}function j(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=i();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function k(a){return a&&a.__esModule?a:{default:a}}var l,m=60,n,o="name_asc",p=0,q=0,r,s=[{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}],t=function(){var a=0.\n\n/**\n * Javascript for quizzes in progress display and processing.\n *\n * @module local_assessfreq/dashboard_quiz_inprogress\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 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"],"file":"dashboard_quiz_inprogress.min.js"}
\ 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 7538d451..00000000
--- a/amd/build/dayview.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-function asyncGeneratorStep(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function _asyncToGenerator(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){asyncGeneratorStep(h,d,e,f,g,"next",a)}function g(a){asyncGeneratorStep(h,d,e,f,g,"throw",a)}f(void 0)})}}define ("local_assessfreq/dayview",["core/str","core/notification","core/modal_factory","local_assessfreq/modal_large","core/templates","core/ajax"],function(a,b,c,d,e,f){var g={},h,i="
",j=[{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"}],k,l="Australia/Melbourne",m="",n=function(a,b){return new Promise(function(c){var d=new Date(1e3*a).toLocaleString("en-US",{timeZone:l}),e=new Date(d),f=e.getFullYear(),g=k[7+e.getMonth()],h=e.getDate(),i=e.getHours(),j="0"+e.getMinutes(),m=i+":"+j.substr(-2);if("strftimetime"===b){c(m)}else{c(h+" "+g+" "+f+", "+m)}})},o=function(){var a=_asyncToGenerator(regeneratorRuntime.mark(function a(b){var c,d,e,f,g,h,j,k,m,o,p,q,r,s;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:c=JSON.parse(b);d=5/72;e=0;case 3:if(!(e=q)){a.next=23;break}q=0;s=(p-j)/60*d;a.next=20;return n(c[e].timestart,"strftimedatetime");case 20:c[e].start=a.sent;a.next=28;break;case 23:r=q/60*d;s=(p-m)/60*d;a.next=27;return n(c[e].timestart,"strftimetime");case 27:c[e].start=a.sent;case 28:if(100.\n\n/**\n * Javascript for heatmap calendar generation and display.\n *\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\ndefine(['core/str', 'core/notification', 'core/modal_factory', 'local_assessfreq/modal_large', 'core/templates', 'core/ajax'],\nfunction(Str, Notification, ModalFactory, 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 */\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 * @param {integer} context The current context id.\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 ModalFactory.create({\n type: ModalLarge.TYPE,\n title: title,\n body: spinner\n })\n .done((modal) => {\n modalObj = modal;\n\n });\n }).catch(() => {\n Notification.exception(new Error('Failed to load string: loading'));\n });\n\n };\n\n return Dayview;\n});\n"],"file":"dayview.min.js"}
\ No newline at end of file
diff --git a/amd/build/debouncer.min.js b/amd/build/debouncer.min.js
index 20d1b83f..1fca9c82 100644
--- a/amd/build/debouncer.min.js
+++ b/amd/build/debouncer.min.js
@@ -1,2 +1,3 @@
-define ("local_assessfreq/debouncer",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.debouncer=void 0;a.debouncer=function debouncer(a,b){var c;return function(){for(var d=arguments.length,e=Array(d),f=0;f{let timeout;return function(){for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++)args[_key]=arguments[_key];const later=()=>{clearTimeout(timeout),func(...args)};clearTimeout(timeout),timeout=setTimeout(later,wait)}}}));
+
+//# sourceMappingURL=debouncer.min.js.map
\ No newline at end of file
diff --git a/amd/build/debouncer.min.js.map b/amd/build/debouncer.min.js.map
index 7668fcb3..ceb85d0a 100644
--- a/amd/build/debouncer.min.js.map
+++ b/amd/build/debouncer.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/debouncer.js"],"names":["debouncer","func","wait","timeout","args","later","clearTimeout","setTimeout"],"mappings":"0JAmCyB,QAAZA,CAAAA,SAAY,CAACC,CAAD,CAAOC,CAAP,CAAgB,CACrC,GAAIC,CAAAA,CAAJ,CAEA,MAAO,WAAmC,4BAANC,CAAM,uBAANA,CAAM,iBACtC,GAAMC,CAAAA,CAAK,CAAG,UAAM,CAChBC,YAAY,CAACH,CAAD,CAAZ,CACAF,CAAI,MAAJ,QAAQG,CAAR,CACH,CAHD,CAKAE,YAAY,CAACH,CAAD,CAAZ,CACAA,CAAO,CAAGI,UAAU,CAACF,CAAD,CAAQH,CAAR,CACvB,CACJ,C","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 local_assessfreq\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"],"file":"debouncer.min.js"}
\ 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 63bcb0f9..00000000
--- a/amd/build/form_modal.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-define ("local_assessfreq/form_modal",["core/str","core/modal_factory","core/fragment","core/ajax"],function(a,b,c,d){var e={},f,g,h=[],i,j="
",k={attributes:!0,childList:!1,subtree:!0},l=new MutationObserver(function ObserverCallback(a){for(var b=0,c;bd){if(null===document.getElementById("noquizwarning")){a.get_string("noquizselected","local_assessfreq").then(function(a){var b=document.createElement("div");b.innerHTML=a;b.id="noquizwarning";b.classList.add("alert","alert-danger");g.getBody().prepend(b)}).catch(function(){Notification.exception(new Error("Failed to load string: searchquiz"))})}}else{g.hide();g.setBody("");l.disconnect();i(d,e)}},q=function(){o();g.show()};e.init=function(a,b){f=a;i=b;m();var c=document.getElementById("local-assessfreq-find-quiz");c.addEventListener("click",q)};return e});
-//# sourceMappingURL=form_modal.min.js.map
diff --git a/amd/build/form_modal.min.js.map b/amd/build/form_modal.min.js.map
deleted file mode 100644
index 7411e0b4..00000000
--- a/amd/build/form_modal.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["../src/form_modal.js"],"names":["define","Str","ModalFactory","Fragment","Ajax","FormModal","contextid","modalObj","resetOptions","callback","spinner","observerConfig","attributes","childList","subtree","observer","MutationObserver","ObserverCallback","mutationsList","i","element","length","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","Notification","exception","Error","createModal","get_string","then","title","create","type","types","DEFAULT","body","large","modal","getRoot","on","processModalForm","e","preventDefault","setBody","hide","catch","getOptionPlaceholders","Promise","resolve","reject","get_strings","key","component","stringReturn","push","formdata","params","stringify","setTitle","loadFragment","modalContainer","querySelectorAll","observe","quizElement","quizId","selectedIndex","courseId","warning","innerHTML","add","getBody","prepend","disconnect","displayModalForm","show","init","context","processDashboard","createBroadcastButton"],"mappings":"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","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 local_assessfreq\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/modal_factory', 'core/fragment', 'core/ajax'],\nfunction(Str, ModalFactory, Fragment, Ajax) {\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\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\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 ModalFactory.create({\n type: ModalFactory.types.DEFAULT,\n title: title,\n body: spinner,\n large: true\n })\n .done((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(() => {\n Notification.exception(new Error('Failed to load string: loading'));\n });\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 }\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 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"],"file":"form_modal.min.js"}
\ No newline at end of file
diff --git a/amd/build/modal_large.min.js b/amd/build/modal_large.min.js
index 122e2488..19b59b2b 100644
--- a/amd/build/modal_large.min.js
+++ b/amd/build/modal_large.min.js
@@ -1,2 +1,11 @@
-define ("local_assessfreq/modal_large",["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry"],function(a,b,c,d,e){var f=!1,g=function(a){d.call(this,a)};g.TYPE="local_assesfreq-large_modal";g.prototype=Object.create(d.prototype);g.prototype.constructor=g;g.prototype.registerEventListeners=function(){d.prototype.registerEventListeners.call(this)};if(!f){e.register(g.TYPE,g,"local_assessfreq/modal_large");f=!0}return g});
-//# sourceMappingURL=modal_large.min.js.map
+/**
+ * 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){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 60ee9107..dfdc2028 100644
--- a/amd/build/modal_large.min.js.map
+++ b/amd/build/modal_large.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/modal_large.js"],"names":["define","$","Notification","CustomEvents","Modal","ModalRegistry","registered","ModalLarge","root","call","TYPE","prototype","Object","create","constructor","registerEventListeners","register"],"mappings":"AAuBAA,OAAM,gCAAC,CAAC,QAAD,CAAW,mBAAX,CAAgC,gCAAhC,CAAkE,YAAlE,CAAgF,qBAAhF,CAAD,CACN,SAASC,CAAT,CAAYC,CAAZ,CAA0BC,CAA1B,CAAwCC,CAAxC,CAA+CC,CAA/C,CAA8D,IAEtDC,CAAAA,CAAU,GAF4C,CAStDC,CAAU,CAAG,SAASC,CAAT,CAAe,CAC5BJ,CAAK,CAACK,IAAN,CAAW,IAAX,CAAiBD,CAAjB,CACH,CAXyD,CAa1DD,CAAU,CAACG,IAAX,CAAkB,6BAAlB,CACAH,CAAU,CAACI,SAAX,CAAuBC,MAAM,CAACC,MAAP,CAAcT,CAAK,CAACO,SAApB,CAAvB,CACAJ,CAAU,CAACI,SAAX,CAAqBG,WAArB,CAAmCP,CAAnC,CAOAA,CAAU,CAACI,SAAX,CAAqBI,sBAArB,CAA8C,UAAW,CAErDX,CAAK,CAACO,SAAN,CAAgBI,sBAAhB,CAAuCN,IAAvC,CAA4C,IAA5C,CACH,CAHD,CAOA,GAAI,CAACH,CAAL,CAAiB,CACbD,CAAa,CAACW,QAAd,CAAuBT,CAAU,CAACG,IAAlC,CAAwCH,CAAxC,CAAoD,8BAApD,EACAD,CAAU,GACb,CAED,MAAOC,CAAAA,CACV,CApCK,CAAN","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 * @package local_assessfreq\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_registry'],\nfunction($, 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});"],"file":"modal_large.min.js"}
\ 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 421b5acf..f643d4c9 100644
--- a/amd/build/override_modal.min.js
+++ b/amd/build/override_modal.min.js
@@ -1,2 +1,10 @@
-define ("local_assessfreq/override_modal",["jquery","core/str","core/modal_factory","core/modal_events","core/fragment","core/ajax"],function(a,b,c,d,e,f){var h={},i,j,k,l,m,n,o="
",p=function(){b.get_string("loading","local_assessfreq").then(function(a){c.create({type:c.types.DEFAULT,title:a,body:o,large:!0}).done(function(a){j=a;j.getRoot().on("click","#id_submitbutton",g);j.getRoot().on("click","#id_cancel",function(a){a.preventDefault();j.setBody(o);j.hide()})})}).catch(function(){Notification.exception(new Error("Failed to load string: loading"))})},q=function(a,c,d){if("undefined"==typeof d){d={}}var f={jsonformdata:JSON.stringify(d),quizid:a,userid:c};j.setBody(o);b.get_string("useroverride","local_assessfreq").then(function(a){j.setTitle(a);j.setBody(e.loadFragment("local_assessfreq","new_override_form",i,f))}).catch(function(){Notification.exception(new Error("Failed to load string: useroverride"))})};function g(b){b.preventDefault();var c=j.getRoot().find("form").serialize(),d=JSON.stringify(c),e=a.merge(j.getRoot().find("[aria-invalid=\"true\"]"),j.getRoot().find(".error"));if(e.length){e.first().focus();return}f.call([{methodname:"local_assessfreq_process_override_form",args:{jsonformdata:d,quizid:l}}])[0].done(function(){j.setBody(o);j.hide();if(n){k(l,n)}else{k(l)}}).fail(function(){q(l,m,c)})}h.displayModalForm=function(a,b){var c=2
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define("local_assessfreq/override_modal",["jquery","core/str","core/modal","core/modal_factory","core/modal_events","core/fragment","core/ajax"],(function($,Str,Modal,ModalFactory,ModalEvents,Fragment,Ajax){let contextid,activitytype,modalObj,activityid,userid,tableHandler,OverrideModal={};const spinner='
',createModal=function(){Str.get_string("loading").then((title=>{ModalFactory.create({type:ModalFactory.types.DEFAULT,title:title,body:spinner,large:!0}).done((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 70e2a09b..738f8ef4 100644
--- a/amd/build/override_modal.min.js.map
+++ b/amd/build/override_modal.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/override_modal.js"],"names":["define","$","Str","ModalFactory","ModalEvents","Fragment","Ajax","OverrideModal","contextid","modalObj","callback","quizid","userid","hoursFilter","spinner","createModal","get_string","then","title","create","type","types","DEFAULT","body","large","done","modal","getRoot","on","processModalForm","e","preventDefault","setBody","hide","catch","Notification","exception","Error","updateModalBody","quiz","user","formdata","params","JSON","stringify","setTitle","loadFragment","overrideform","find","serialize","formjson","invalid","merge","length","first","focus","call","methodname","args","fail","displayModalForm","hours","show","init","context","callbackFunction"],"mappings":"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","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 local_assessfreq\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/fragment', 'core/ajax'],\nfunction($,Str, ModalFactory, ModalEvents, Fragment, Ajax) {\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 ModalFactory.create({\n type: ModalFactory.types.DEFAULT,\n title: title,\n body: spinner,\n large: true\n })\n .done((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 {Object} formdata\n * @private\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 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 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"],"file":"override_modal.min.js"}
\ 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 ModalFactory.create({\n type: ModalFactory.types.DEFAULT,\n title: title,\n body: spinner,\n large: true\n })\n .done((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","done","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","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,QAE5Bf,aAAagB,OAAO,CAChBC,KAAMjB,aAAakB,MAAMC,QACzBJ,MAAOA,MACPK,KAAMT,QACNU,OAAO,IAENC,MAAMC,QACHjB,SAAWiB,MAEXjB,SAASkB,UAAUC,GAAG,QAAS,mBAAoBC,kBACnDpB,SAASkB,UAAUC,GAAG,QAAS,cAAc,SAASE,GAClDA,EAAEC,iBACFtB,SAASuB,QAAQlB,SACjBL,SAASwB,iBAcvBC,gBAAkB,SAASC,SAAUC,KAAMC,eACrB,IAAbA,WACPA,SAAW,QAGXC,OAAS,cACOC,KAAKC,UAAUH,uBACf7B,wBACF2B,gBACJC,MAGd3B,SAASuB,QAAQlB,SACjBb,IAAIe,WAAW,qBAAsB,oBAAoBC,MAAMC,QAC3DT,SAASgC,SAASvB,OAClBT,SAASuB,QAAQ3B,SAASqC,aAAa,mBAAoB,oBAAqBnC,UAAW+B,sBAU1FT,iBAAiBC,GACtBA,EAAEC,qBAGEY,aAAelC,SAASkB,UAAUiB,KAAK,QAAQC,YAC/CC,SAAWP,KAAKC,UAAUG,cAI1BI,QAAU/C,EAAEgD,MACZvC,SAASkB,UAAUiB,KAAK,yBACxBnC,SAASkB,UAAUiB,KAAK,WAGxBG,QAAQE,OACRF,QAAQG,QAAQC,QAKpB7C,KAAK8C,KAAK,CAAC,CACPC,WAAY,yCACZC,KAAM,cACcR,oBACFpC,wBACEF,iBAEpB,GAAGiB,MAAK,KAERhB,SAASuB,QAAQlB,SACjBL,SAASwB,YACYsB,IAAjB3C,cACAA,aAAa4C,cAElBC,MAAK,KAEJvB,gBAAgBxB,WAAYC,OAAQgC,wBAS5C9B,cAAc6C,iBAAmB,SAASvB,SAAUC,MAChD1B,WAAayB,SACbxB,OAASyB,KACTF,gBAAgBxB,WAAY0B,MAC5B3B,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
deleted file mode 100644
index 5e70d3fd..00000000
--- a/amd/build/student_search.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("local_assessfreq/student_search",["exports","jquery","core/notification","local_assessfreq/override_modal","local_assessfreq/table_handler","local_assessfreq/user_preferences"],function(a,b,c,d,e,f){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=i(b);c=i(c);d=i(d);e=h(e);f=h(f);function g(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;g=function(){return a};return a}function h(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=g();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function i(a){return a&&a.__esModule?a:{default:a}}var j,k=4,l=1,m=60,n,o=function(){var a=0.\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"],"file":"student_search.min.js"}
\ 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 9106e762..00000000
--- a/amd/build/summary_participants.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-define ("local_assessfreq/summary_participants",["core/fragment","core/templates","core/str","core/notification"],function(a,b,c,d){return{chart:function(e,f){e.forEach(function(e){var g=document.getElementById(e+"-summary-graph"),h={data:JSON.stringify({quiz:e,call:"participant_summary"})};a.loadFragment("local_assessfreq","get_quiz_chart",f,h).done(function(a){var e=JSON.parse(a);if(!0==e.hasdata){var f={withtable:!1,chartdata:JSON.stringify(e.chart),aspect:!1,legend:JSON.stringify({position:"left"})};b.render("local_assessfreq/chart",f).done(function(a,c){b.replaceNodeContents(g,a,c)}).fail(function(){d.exception(new Error("Failed to load chart template."))})}else{c.get_string("nodata","local_assessfreq").then(function(a){var b=document.createElement("h3");b.innerHTML=a;g.innerHTML=b.outerHTML}).catch(function(){d.exception(new Error("Failed to load string: nodata"))})}}).fail(function(){d.exception(new Error("Failed to load card."))})})}}});
-//# sourceMappingURL=summary_participants.min.js.map
diff --git a/amd/build/summary_participants.min.js.map b/amd/build/summary_participants.min.js.map
deleted file mode 100644
index 491ab4e4..00000000
--- a/amd/build/summary_participants.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["../src/summary_participants.js"],"names":["define","Fragment","Templates","Str","Notification","chart","assessids","contextid","forEach","assessid","chartElement","document","getElementById","params","JSON","stringify","loadFragment","done","response","resObj","parse","hasdata","context","position","render","html","js","replaceNodeContents","fail","exception","Error","get_string","then","str","noDatastr","createElement","innerHTML","outerHTML","catch"],"mappings":"AAuBAA,OAAM,yCAAC,CAAC,eAAD,CAAkB,gBAAlB,CAAoC,UAApC,CAAgD,mBAAhD,CAAD,CACN,SAASC,CAAT,CAAmBC,CAAnB,CAA8BC,CAA9B,CAAmCC,CAAnC,CAAiD,CAgD7C,MA3Cc,CAENC,KAFM,CAEE,SAASC,CAAT,CAAoBC,CAApB,CAA+B,CAC3CD,CAAS,CAACE,OAAV,CAAkB,SAACC,CAAD,CAAc,IACxBC,CAAAA,CAAY,CAAGC,QAAQ,CAACC,cAAT,CAAwBH,CAAQ,CAAG,gBAAnC,CADS,CAExBI,CAAM,CAAG,CAAC,KAAQC,IAAI,CAACC,SAAL,CAAe,CAAC,KAASN,CAAV,CAAoB,KAAQ,qBAA5B,CAAf,CAAT,CAFe,CAI5BR,CAAQ,CAACe,YAAT,CAAsB,kBAAtB,CAA0C,gBAA1C,CAA4DT,CAA5D,CAAuEM,CAAvE,EACCI,IADD,CACM,SAACC,CAAD,CAAc,CAChB,GAAIC,CAAAA,CAAM,CAAGL,IAAI,CAACM,KAAL,CAAWF,CAAX,CAAb,CACA,GAAI,IAAAC,CAAM,CAACE,OAAX,CAA4B,IAEpBC,CAAAA,CAAO,CAAG,CACV,YADU,CAEV,UAAcR,IAAI,CAACC,SAAL,CAAeI,CAAM,CAACd,KAAtB,CAFJ,CAGV,SAHU,CAIV,OAAWS,IAAI,CAACC,SAAL,CALF,CAACQ,QAAQ,CAAE,MAAX,CAKE,CAJD,CAFU,CAQxBrB,CAAS,CAACsB,MAAV,CAAiB,wBAAjB,CAA2CF,CAA3C,EAAoDL,IAApD,CAAyD,SAACQ,CAAD,CAAOC,CAAP,CAAc,CAEnExB,CAAS,CAACyB,mBAAV,CAA8BjB,CAA9B,CAA4Ce,CAA5C,CAAkDC,CAAlD,CACH,CAHD,EAGGE,IAHH,CAGQ,UAAM,CACVxB,CAAY,CAACyB,SAAb,CAAuB,GAAIC,CAAAA,KAAJ,CAAU,gCAAV,CAAvB,CAEH,CAND,CAQH,CAhBD,IAgBO,CACH3B,CAAG,CAAC4B,UAAJ,CAAe,QAAf,CAAyB,kBAAzB,EAA6CC,IAA7C,CAAkD,SAACC,CAAD,CAAS,CACvD,GAAMC,CAAAA,CAAS,CAAGvB,QAAQ,CAACwB,aAAT,CAAuB,IAAvB,CAAlB,CACAD,CAAS,CAACE,SAAV,CAAsBH,CAAtB,CACAvB,CAAY,CAAC0B,SAAb,CAAyBF,CAAS,CAACG,SAEtC,CALD,EAKGC,KALH,CAKS,UAAM,CACXlC,CAAY,CAACyB,SAAb,CAAuB,GAAIC,CAAAA,KAAJ,CAAU,+BAAV,CAAvB,CACH,CAPD,CAQH,CACJ,CA7BD,EA6BGF,IA7BH,CA6BQ,UAAM,CACVxB,CAAY,CAACyB,SAAb,CAAuB,GAAIC,CAAAA,KAAJ,CAAU,sBAAV,CAAvB,CAEH,CAhCD,CAiCH,CArCD,CAsCH,CAzCa,CA4CjB,CAlDK,CAAN","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 * @package local_assessfreq\n * @copyright 2020 Matt Porritt \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['core/fragment', 'core/templates', 'core/str', 'core/notification'],\nfunction(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});"],"file":"summary_participants.min.js"}
\ No newline at end of file
diff --git a/amd/build/table_handler.min.js b/amd/build/table_handler.min.js
index 59bb5900..674d05ff 100644
--- a/amd/build/table_handler.min.js
+++ b/amd/build/table_handler.min.js
@@ -1,2 +1,11 @@
-function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("local_assessfreq/table_handler",["exports","core/ajax","core/fragment","core/notification","core/templates","local_assessfreq/debouncer","local_assessfreq/override_modal","local_assessfreq/user_preferences"],function(a,b,c,d,e,f,g,h){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=a.tableSortButtonAction=a.tableSearchRowSet=a.tableSearchReset=a.tableSearch=a.getTable=void 0;b=k(b);c=k(c);d=k(d);e=k(e);f=j(f);g=k(g);h=j(h);function i(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;i=function(){return a};return a}function j(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=i();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function k(a){return a&&a.__esModule?a:{default:a}}var l,m,n,o,p,q=0,r=!1,s,t,u,v,w,x=function(a){var b=1
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=_interopRequireDefault(_ajax),_fragment=_interopRequireDefault(_fragment),_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates),Debouncer=_interopRequireWildcard(Debouncer),_override_modal=_interopRequireDefault(_override_modal),UserPreference=_interopRequireWildcard(UserPreference);return _exports.default=class{constructor(activity,context,tableElementId,tableFragmentComponent,tableFragmentValue,tableRowPreference,tableSortPreference,tableSearchElement){let tableId=arguments.length>8&&void 0!==arguments[8]?arguments[8]:null,tableMethodName=arguments.length>9&&void 0!==arguments[9]?arguments[9]:null;this.activityId=activity,this.contextId=context,this.elementId=tableElementId,this.fragmentComponent=tableFragmentComponent,this.fragmentValue=tableFragmentValue,this.rowPreference=tableRowPreference,this.sortPreference=tableSortPreference,this.searchElement=tableSearchElement,this.id=tableId,this.methodName=tableMethodName,this.overridden=!1}getTable=(()=>{var _this=this;return function(){let page=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;_this.overridden=!1;let search=document.getElementById(_this.searchElement).value.trim(),tableElement=document.getElementById(_this.elementId),spinner=tableElement.getElementsByClassName("overlay-icon-container")[0],tableBody=tableElement.getElementsByClassName("table-body")[0],values={search:search,page:page};_this.activityId>0&&(values.activityid=_this.activityId);let params={data:JSON.stringify(values)};spinner.classList.remove("hide"),_fragment.default.loadFragment(_this.fragmentComponent,_this.fragmentValue,_this.contextId,params).done(((response,js)=>{tableBody.innerHTML=response,js&&_templates.default.runTemplateJS(js),spinner.classList.add("hide"),_this.tableEventListeners()})).fail((()=>{_notification.default.exception(new Error("Failed to update table."))}))}})();debounceTable=Debouncer.debouncer((()=>{this.getTable()}),750);tableSort=event=>{event.preventDefault();let sortArray={};const linkUrl=new URL(event.target.closest("a").href),targetSortBy=linkUrl.searchParams.get("tsort");let targetSortOrder=linkUrl.searchParams.get("tdir");""===targetSortOrder&&(targetSortOrder="4"),sortArray[targetSortBy]=targetSortOrder,_ajax.default.call([{methodname:this.methodName,args:{tableid:this.id,preference:"sortby",values:JSON.stringify(sortArray)}}])[0].then((()=>{this.getTable()}))};tableHide=event=>{event.preventDefault();let hideArray={};const linkUrl=new URL(event.target.closest("a").href),links=document.getElementById(this.elementId).querySelectorAll("a");let targetAction,targetColumn,action,column;-1!==linkUrl.search.indexOf("thide")?(targetAction="hide",targetColumn=linkUrl.searchParams.get("thide")):(targetAction="show",targetColumn=linkUrl.searchParams.get("tshow"));for(let i=0;i{this.getTable()}))};tableReset=event=>{event.preventDefault(),_ajax.default.call([{methodname:this.methodName,args:{tableid:this.id,preference:"reset",values:JSON.stringify({})}}])[0].then((()=>{this.getTable()}))};tableSearch=event=>"Meta"!==event.key&&!event.ctrlKey&&((0===event.target.value.length||event.target.value.length>2)&&this.debounceTable(),!0);tableSearchReset=()=>{let tableSearchInputElement=document.getElementById(this.searchElement);tableSearchInputElement.value="",tableSearchInputElement.focus(),this.getTable()};tableSearchRowSet=event=>{if(event.preventDefault(),"a"===event.target.tagName.toLowerCase()){let rows=event.target.dataset.metric;UserPreference.setUserPreference(this.rowPreference,rows).then((()=>{this.getTable()})).fail((()=>{_notification.default.exception(new Error("Failed to update user preference: rows"))}))}};tableNav=event=>{event.preventDefault();const page=new URL(event.target.closest("a").href).searchParams.get("page");page&&this.getTable(page)};tableSortButtonAction=event=>{event.preventDefault();var element=event.target;if("a"===element.tagName.toLowerCase()&&element.dataset.sort!==this.sortValue){this.sortValue=element.dataset.sort;let links=element.parentNode.getElementsByTagName("a");for(let i=0;i{const tableElement=document.getElementById(this.elementId),links=tableElement.querySelectorAll("a"),resetLink=tableElement.getElementsByClassName("resettable"),overrideLinks=tableElement.getElementsByClassName("action-icon override"),disabledLinks=tableElement.getElementsByClassName("action-icon disabled"),tableNavElement=tableElement.querySelectorAll("nav");for(let i=0;i0&&resetLink[0].addEventListener("click",this.tableReset);for(let i=0;i{event.preventDefault()}));tableNavElement.forEach((navElement=>{navElement.addEventListener("click",this.tableNav)}))};triggerOverrideModal=event=>{event.preventDefault();let userid=event.target.closest("a").id.substring(25);if(userid.includes("-")){let elements=userid.split("-");this.activityId=elements.pop(),userid=elements.pop()}_override_modal.default.displayModalForm(this.activityId,userid,this.hoursFilter)}},_exports.default}));
+
+//# 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 4b50485e..3cb88dd6 100644
--- a/amd/build/table_handler.min.js.map
+++ b/amd/build/table_handler.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/table_handler.js"],"names":["cardElement","contextId","elementId","fragmentValue","hoursFilter","quizId","overridden","rowPreference","sortValue","searchElement","id","methodName","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","Fragment","loadFragment","done","response","js","innerHTML","Templates","runTemplateJS","add","tableEventListeners","fail","Notification","exception","Error","debounceTable","Debouncer","debouncer","tableSort","event","preventDefault","linkUrl","URL","target","closest","href","targetSortBy","searchParams","get","targetSortOrder","Ajax","call","methodname","args","tableid","preference","then","tableHide","hideArray","links","querySelectorAll","targetAction","targetColumn","action","column","indexOf","i","hideLinkUrl","length","tableReset","tableSearch","key","ctrlKey","tableSearchReset","tableSearchInputElement","focus","tableSearchRowSet","tagName","toLowerCase","rows","dataset","metric","UserPreference","setUserPreference","tableNav","tableSortButtonAction","element","sort","parentNode","getElementsByTagName","tableNavElement","tableCardElement","resetLink","overrideLinks","disabledLinks","addEventListener","triggerOverrideModal","forEach","navElement","userid","substring","includes","elements","pop","OverrideModal","displayModalForm","init","context","tableElementId","tableFragmentValue","tableRowPreference","tableSearchElement","tableId","tableMethodName"],"mappings":"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","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 local_assessfreq\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\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 */\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/**\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\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"],"file":"table_handler.min.js"}
\ 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 this.overridden = false;\n\n let search = document.getElementById(this.searchElement).value.trim();\n let tableElement = document.getElementById(this.elementId);\n let spinner = tableElement.getElementsByClassName('overlay-icon-container')[0];\n let tableBody = tableElement.getElementsByClassName('table-body')[0];\n let values = {'search': search, 'page': page};\n\n // Add values to Object depending on dashboard type.\n if (this.activityId > 0) {\n values.activityid = this.activityId;\n }\n\n let params = {'data': JSON.stringify(values)};\n\n spinner.classList.remove('hide'); // Show spinner if not already shown.\n Fragment.loadFragment(this.fragmentComponent, this.fragmentValue, this.contextId, params)\n .done((response, js) => {\n tableBody.innerHTML = response;\n if (js) {\n Templates.runTemplateJS(js); // Magic call the initialises JS from template included in response template HTML.\n }\n spinner.classList.add('hide');\n this.tableEventListeners(); // Re-add table event listeners.\n\n }).fail(() => {\n Notification.exception(new Error('Failed to update table.'));\n });\n };\n\n /**\n * This stops the ajax method that updates the table from being updated\n * while the user is still checking options.\n *\n */\n debounceTable = Debouncer.debouncer(() => {\n this.getTable();\n }, 750);\n\n /**\n * Process the sort click events from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableSort = (event) => {\n event.preventDefault();\n\n let sortArray = {};\n const linkUrl = new URL(event.target.closest('a').href);\n const targetSortBy = linkUrl.searchParams.get('tsort');\n let targetSortOrder = linkUrl.searchParams.get('tdir');\n\n // We want to flip the clicked column.\n if (targetSortOrder === '') {\n targetSortOrder = \"4\";\n }\n\n sortArray[targetSortBy] = targetSortOrder;\n\n // Set option via ajax.\n // eslint-disable-next-line promise/catch-or-return\n Ajax.call([{\n methodname: this.methodName,\n args: {\n tableid: this.id,\n preference: 'sortby',\n values: JSON.stringify(sortArray)\n },\n // eslint-disable-next-line promise/always-return\n }])[0].then(() => {\n this.getTable(); // Reload the table.\n });\n\n };\n\n /**\n * Process the sort click events from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableHide = (event) => {\n event.preventDefault();\n\n let hideArray = {};\n const linkUrl = new URL(event.target.closest('a').href);\n const tableElement = document.getElementById(this.elementId);\n const links = tableElement.querySelectorAll('a');\n let targetAction;\n let targetColumn;\n let action;\n let column;\n\n if (linkUrl.search.indexOf('thide') !== -1) {\n targetAction = 'hide';\n targetColumn = linkUrl.searchParams.get('thide');\n } else {\n targetAction = 'show';\n targetColumn = linkUrl.searchParams.get('tshow');\n }\n\n for (let i = 0; i < links.length; i++) {\n let hideLinkUrl = new URL(links[i].href);\n if (hideLinkUrl.search.indexOf('thide') !== -1) {\n action = 'hide';\n column = hideLinkUrl.searchParams.get('thide');\n } else {\n action = 'show';\n column = hideLinkUrl.searchParams.get('tshow');\n }\n\n if (action === 'show') {\n hideArray[column] = 1;\n }\n }\n\n hideArray[targetColumn] = (targetAction === 'hide') ? 1 : 0; // We want to flip the clicked column.\n\n // Set option via ajax.\n // eslint-disable-next-line promise/catch-or-return\n Ajax.call([{\n methodname: this.methodName,\n args: {\n tableid: this.id,\n preference: 'collapse',\n values: JSON.stringify(hideArray)\n },\n // eslint-disable-next-line promise/always-return\n }])[0].then(() => {\n this.getTable(); // Reload the table.\n });\n\n };\n\n /**\n * Process the reset click event from the table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableReset = (event) => {\n event.preventDefault();\n\n // Set option via ajax.\n // eslint-disable-next-line promise/catch-or-return\n Ajax.call([{\n methodname: this.methodName,\n args: {\n tableid: this.id,\n preference: 'reset',\n values: JSON.stringify({})\n },\n // eslint-disable-next-line promise/always-return\n }])[0].then(() => {\n this.getTable(); // Reload the table.\n });\n\n };\n\n /**\n * Process the search events from the student table.\n *\n * @param {Event} event\n * @return {Boolean}\n */\n tableSearch = (event) => {\n if (event.key === 'Meta' || event.ctrlKey) {\n return false;\n }\n\n if (event.target.value.length === 0 || event.target.value.length > 2) {\n this.debounceTable();\n }\n return true;\n };\n\n /**\n * Process the search reset click event from the student table.\n *\n */\n tableSearchReset = () => {\n let tableSearchInputElement = document.getElementById(this.searchElement);\n tableSearchInputElement.value = '';\n tableSearchInputElement.focus();\n this.getTable();\n };\n\n /**\n * Process the row set event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableSearchRowSet = (event) => {\n event.preventDefault();\n if (event.target.tagName.toLowerCase() === 'a') {\n let rows = event.target.dataset.metric;\n UserPreference.setUserPreference(this.rowPreference, rows)\n // eslint-disable-next-line promise/always-return\n .then(() => {\n this.getTable(); // Reload the table.\n })\n .fail(() => {\n Notification.exception(new Error('Failed to update user preference: rows'));\n });\n }\n };\n\n /**\n * Process the nav event from the student table.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableNav = (event) => {\n event.preventDefault();\n\n const linkUrl = new URL(event.target.closest('a').href);\n const page = linkUrl.searchParams.get('page');\n\n if (page) {\n this.getTable(page);\n }\n };\n\n /**\n * Get and process the selected assessment metric from the dropdown for the heatmap display,\n * and update the corresponding user preference.\n *\n * @param {Event} event The triggered event for the element.\n */\n tableSortButtonAction = (event) => {\n event.preventDefault();\n var element = event.target;\n\n if (element.tagName.toLowerCase() === 'a' && element.dataset.sort !== this.sortValue) {\n this.sortValue = element.dataset.sort;\n\n let links = element.parentNode.getElementsByTagName('a');\n for (let i = 0; i < links.length; i++) {\n links[i].classList.remove('active');\n }\n\n element.classList.add('active');\n\n // Save selection as a user preference.\n UserPreference.setUserPreference(this.sortPreference, this.sortValue);\n\n this.debounceTable(); // Call function to update table.\n }\n };\n\n /**\n * Re-add event listeners when the student table is updated.\n */\n tableEventListeners = () => {\n const tableElement = document.getElementById(this.elementId);\n const links = tableElement.querySelectorAll('a');\n const resetLink = tableElement.getElementsByClassName('resettable');\n const overrideLinks = tableElement.getElementsByClassName('action-icon override');\n const disabledLinks = tableElement.getElementsByClassName('action-icon disabled');\n const tableNavElement = tableElement.querySelectorAll('nav'); // There are two nav paging elements per table.\n\n for (let i = 0; i < links.length; i++) {\n let linkUrl = new URL(links[i].href);\n if (linkUrl.search.indexOf('thide') !== -1 || linkUrl.search.indexOf('tshow') !== -1) {\n links[i].addEventListener('click', this.tableHide);\n } else if (linkUrl.search.indexOf('tsort') !== -1) {\n links[i].addEventListener('click', this.tableSort);\n }\n }\n\n if (resetLink.length > 0) {\n resetLink[0].addEventListener('click', this.tableReset);\n }\n\n for (let i = 0; i < overrideLinks.length; i++) {\n overrideLinks[i].addEventListener('click', this.triggerOverrideModal);\n }\n\n for (let i = 0; i < disabledLinks.length; i++) {\n disabledLinks[i].addEventListener('click', (event) => {\n event.preventDefault();\n });\n }\n\n tableNavElement.forEach((navElement) => {\n navElement.addEventListener('click', this.tableNav);\n });\n };\n\n /**\n * Trigger the override modal form. Thin wrapper to add extra data to click event.\n *\n * @param {Event} event The triggered event for the element.\n */\n triggerOverrideModal = (event) => {\n event.preventDefault();\n let userid = event.target.closest('a').id.substring(25);\n if (userid.includes('-')) {\n let elements = userid.split('-');\n this.activityId = elements.pop();\n userid = elements.pop();\n }\n\n OverrideModal.displayModalForm(this.activityId, userid, this.hoursFilter);\n };\n}\n"],"names":["constructor","activity","context","tableElementId","tableFragmentComponent","tableFragmentValue","tableRowPreference","tableSortPreference","tableSearchElement","tableId","tableMethodName","activityId","contextId","elementId","fragmentComponent","fragmentValue","rowPreference","sortPreference","searchElement","id","methodName","overridden","getTable","page","_this","search","document","getElementById","value","trim","tableElement","spinner","getElementsByClassName","tableBody","values","activityid","params","JSON","stringify","classList","remove","loadFragment","done","response","js","innerHTML","runTemplateJS","add","tableEventListeners","fail","exception","Error","debounceTable","Debouncer","debouncer","tableSort","event","preventDefault","sortArray","linkUrl","URL","target","closest","href","targetSortBy","searchParams","get","targetSortOrder","call","methodname","this","args","tableid","preference","then","tableHide","hideArray","links","querySelectorAll","targetAction","targetColumn","action","column","indexOf","i","length","hideLinkUrl","tableReset","tableSearch","key","ctrlKey","tableSearchReset","tableSearchInputElement","focus","tableSearchRowSet","tagName","toLowerCase","rows","dataset","metric","UserPreference","setUserPreference","tableNav","tableSortButtonAction","element","sort","sortValue","parentNode","getElementsByTagName","resetLink","overrideLinks","disabledLinks","tableNavElement","addEventListener","triggerOverrideModal","forEach","navElement","userid","substring","includes","elements","split","pop","displayModalForm","hoursFilter"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/user_preferences.min.js b/amd/build/user_preferences.min.js
index 6eb7bdc3..2540e4f3 100644
--- a/amd/build/user_preferences.min.js
+++ b/amd/build/user_preferences.min.js
@@ -1,2 +1,11 @@
-define ("local_assessfreq/user_preferences",["exports","core/ajax","core/notification"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.getUserPreference=a.setUserPreference=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var e=function(a,d){return b.default.call([{methodname:"core_user_update_user_preferences",args:{preferences:[{type:a,value:d}]}}])[0].fail(function(){c.default.exception(new Error("Failed to update user preference"))})};a.setUserPreference=e;var f=function(a){return b.default.call([{methodname:"core_user_get_user_preferences",args:{name:a}}])[0]};a.getUserPreference=f});
-//# sourceMappingURL=user_preferences.min.js.map
+define("local_assessfreq/user_preferences",["exports","core/ajax","core/notification"],(function(_exports,_ajax,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+/**
+ * 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]}}));
+
+//# sourceMappingURL=user_preferences.min.js.map
\ No newline at end of file
diff --git a/amd/build/user_preferences.min.js.map b/amd/build/user_preferences.min.js.map
index 18f98efe..d0054997 100644
--- a/amd/build/user_preferences.min.js.map
+++ b/amd/build/user_preferences.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/user_preferences.js"],"names":["setUserPreference","type","value","Ajax","call","methodname","args","preferences","fail","Notification","exception","Error","getUserPreference","name"],"mappings":"qNAwBA,OACA,O,mDAUO,GAAMA,CAAAA,CAAiB,CAAG,SAACC,CAAD,CAAOC,CAAP,CAAiB,CAQ9C,MAAOC,WAAKC,IAAL,CAAU,CAPD,CACZC,UAAU,CAAE,mCADA,CAEZC,IAAI,CAAE,CACFC,WAAW,CAAE,CAAC,CAACN,IAAI,CAAEA,CAAP,CAAaC,KAAK,CAAEA,CAApB,CAAD,CADX,CAFM,CAOC,CAAV,EAAqB,CAArB,EACNM,IADM,CACD,UAAM,CACRC,UAAaC,SAAb,CAAuB,GAAIC,CAAAA,KAAJ,CAAU,kCAAV,CAAvB,CACH,CAHM,CAIV,CAZM,C,sBAqBA,GAAMC,CAAAA,CAAiB,CAAG,SAACC,CAAD,CAAU,CAQvC,MAAOV,WAAKC,IAAL,CAAU,CAPD,CACZC,UAAU,CAAE,gCADA,CAEZC,IAAI,CAAE,CACF,KAAQO,CADN,CAFM,CAOC,CAAV,EAAqB,CAArB,CACV,CATM,C","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 local_assessfreq\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"],"file":"user_preferences.min.js"}
\ 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 209a8b66..00000000
--- a/amd/build/zoom_modal.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-define ("local_assessfreq/zoom_modal",["core/str","core/modal_factory","core/fragment","core/ajax","core/templates","local_assessfreq/modal_large","core/notification"],function(a,b,c,d,e,f,g){var h={},i,j;h.zoomGraph=function(b,d,f){var h=b.target.parentElement.dataset.title;c.loadFragment("local_assessfreq",f,i,d).done(function(b){var c=JSON.parse(b);if(!0==c.hasdata){var d={withtable:!1,chartdata:JSON.stringify(c.chart),aspect:!1};j.setTitle(h);j.setBody(e.render("local_assessfreq/chart",d));j.show()}else{a.get_string("nodata","local_assessfreq").then(function(a){var b=document.createElement("h3");b.innerHTML=a;j.setTitle(h);j.setBody(b.outerHTML);j.show()}).catch(function(){g.exception(new Error("Failed to load string: nodata"))})}}).fail(function(){g.exception(new Error("Failed to load zoomed graph"))})};var k=function(){return new Promise(function(c,d){a.get_string("loading","core").then(function(a){b.create({type:f.TYPE,title:a,body:"
"}).done(function(a){j=a;c()})}).catch(function(){d(new Error("Failed to load string: loading"))})})};h.init=function(a){i=a;k()};return h});
-//# sourceMappingURL=zoom_modal.min.js.map
diff --git a/amd/build/zoom_modal.min.js.map b/amd/build/zoom_modal.min.js.map
deleted file mode 100644
index 6f16f9bd..00000000
--- a/amd/build/zoom_modal.min.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["../src/zoom_modal.js"],"names":["define","Str","ModalFactory","Fragment","Ajax","Templates","ModalLarge","Notification","ZoomModal","contextid","modalObj","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","createModal","Promise","resolve","reject","create","type","TYPE","body","modal","init"],"mappings":"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","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 local_assessfreq\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/modal_factory', 'core/fragment', 'core/ajax', 'core/templates', 'local_assessfreq/modal_large',\n 'core/notification'],\nfunction(Str, ModalFactory, 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 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, reject) => {\n Str.get_string('loading', 'core').then((title) => {\n // Create the Modal.\n ModalFactory.create({\n type: ModalLarge.TYPE,\n title: title,\n body: spinner\n })\n .done((modal) => {\n modalObj = modal;\n resolve();\n });\n }).catch(() => {\n reject(new Error('Failed to load string: loading'));\n });\n });\n };\n\n /**\n * Initialise method for quiz dashboard rendering.\n */\n ZoomModal.init = function(context) {\n contextid = context;\n createModal();\n };\n\n return ZoomModal;\n});\n"],"file":"zoom_modal.min.js"}
\ No newline at end of file
diff --git a/amd/src/calendar.js b/amd/src/calendar.js
deleted file mode 100644
index 5c0de108..00000000
--- a/amd/src/calendar.js
+++ /dev/null
@@ -1,520 +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.
- *
- * @package local_assessfreq
- * @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} 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 {Number} year The year to get the events for.
- * @param {String} metric The type of metric to get, 'students' or 'assess'.
- * @param {Array} modules Array of the modules to get.
- * @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.
- *
- * @oaram {Number} year The year to generate the tables for.
- * @param {Number} startMonth The month to start table generation from.
- * @param {Number} endMonth The month to generate the tables to.
- * @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} calendarContainer the container to populate.
- * @param {Number} year The year to generate calendar for.
- * @param {Number} startMonth The month to start generation from.
- * @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 c66a818d..00000000
--- a/amd/src/chart_data.js
+++ /dev/null
@@ -1,117 +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
- * @package local_assessfreq
- * @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 4cc3039c..00000000
--- a/amd/src/chart_output_chartjs.js
+++ /dev/null
@@ -1,115 +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.
- *
- * @package local_assessfreq
- * @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
- * @param {module:core/chart_axis} axis The axis.
- * @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.
- */
- 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..fe958cf1 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,6 +45,7 @@ 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..9d4241c6
--- /dev/null
+++ b/amd/src/dashboard.js
@@ -0,0 +1,65 @@
+// 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
+ */
+
+export const init = () => {
+
+ // Load the tab cuntionality.
+ tabs();
+
+};
+
+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 6ecaacea..00000000
--- a/amd/src/dashboard_assessment.js
+++ /dev/null
@@ -1,372 +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
- * @package local_assessfreq
- * @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 6a036bfb..00000000
--- a/amd/src/dashboard_quiz.js
+++ /dev/null
@@ -1,252 +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
- * @package local_assessfreq
- * @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 4d465d89..00000000
--- a/amd/src/dashboard_quiz_inprogress.js
+++ /dev/null
@@ -1,286 +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
- * @package local_assessfreq
- * @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 4e3bdb08..00000000
--- a/amd/src/dayview.js
+++ /dev/null
@@ -1,209 +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.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define(
- ['core/str', 'core/notification', 'core/modal_factory', 'local_assessfreq/modal_large', 'core/templates', 'core/ajax'],
- function (Str, Notification, ModalFactory, 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.
- *
- */
- 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.
- *
- * @param {integer} context The current context id.
- */
- 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.
- ModalFactory.create({
- type: ModalLarge.TYPE,
- title: title,
- body: spinner
- })
- .done((modal) => {
- modalObj = modal;
-
- });
- }).catch(() => {
- Notification.exception(new Error('Failed to load string: loading'));
- });
-
- };
-
- return Dayview;
- }
-);
diff --git a/amd/src/debouncer.js b/amd/src/debouncer.js
index b1874d6e..531d6050 100644
--- a/amd/src/debouncer.js
+++ b/amd/src/debouncer.js
@@ -17,7 +17,7 @@
* Debounce JS module.
*
* @module local_assessfreq/debouncer
- * @package local_assessfreq
+ * @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 23117f27..00000000
--- a/amd/src/form_modal.js
+++ /dev/null
@@ -1,241 +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.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define(
- ['core/str', 'core/modal_factory', 'core/fragment', 'core/ajax'],
- function (Str, ModalFactory, Fragment, Ajax) {
-
- /**
- * 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.
- ModalFactory.create({
- type: ModalFactory.types.DEFAULT,
- title: title,
- body: spinner,
- large: true
- })
- .done((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(new Error('Failed to load string: loading'));
- });
- };
-
- 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.
- */
- 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 4ba79b6f..16422da1 100644
--- a/amd/src/modal_large.js
+++ b/amd/src/modal_large.js
@@ -16,23 +16,24 @@
/**
* Javascript for large modal .
*
- * @package local_assessfreq
+ * @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 6409e511..bc839c06 100644
--- a/amd/src/override_modal.js
+++ b/amd/src/override_modal.js
@@ -16,25 +16,25 @@
/**
* Javascript for report card display and processing.
*
- * @package local_assessfreq
+ * @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'],
- function ($,Str, ModalFactory, ModalEvents, Fragment, Ajax) {
+ ['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 = '
'
+ ''
@@ -45,8 +45,8 @@ 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.
ModalFactory.create({
type: ModalFactory.types.DEFAULT,
@@ -54,46 +54,43 @@ define(
body: spinner,
large: true
})
- .done((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();
+ .done((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 {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 +109,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,41 +124,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 {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.
+ * 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 e0c80aa9..00000000
--- a/amd/src/student_search.js
+++ /dev/null
@@ -1,192 +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
- * @package local_assessfreq
- * @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 3182a805..00000000
--- a/amd/src/summary_participants.js
+++ /dev/null
@@ -1,76 +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.
- *
- * @package local_assessfreq
- * @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 3e713a7f..d0cc3a3f 100644
--- a/amd/src/table_handler.js
+++ b/amd/src/table_handler.js
@@ -17,7 +17,7 @@
* Table handler JS module.
*
* @module local_assessfreq/table_handler
- * @package local_assessfreq
+ * @package
* @copyright 2020 Guillermo Gomez
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -30,328 +30,311 @@ 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;
+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;
}
- 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;
- }
+ /**
+ * 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) => {
+ this.overridden = false;
+
+ 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};
+
+ // Add values to Object depending on dashboard type.
+ if (this.activityId > 0) {
+ values.activityid = this.activityId;
+ }
- let params = {'data': JSON.stringify(values)};
+ 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.
+ 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.
- }).fail(() => {
- Notification.exception(new Error('Failed to update table.'));
+ }).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";
- }
-
- sortArray[targetSortBy] = targetSortOrder;
+ };
+
+ /**
+ * 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";
+ }
- // 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.
- });
+ 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 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');
- }
+ /**
+ * Process the sort click events from the student table.
+ *
+ * @param {Event} event The triggered event for the element.
+ */
+ tableHide = (event) => {
+ event.preventDefault();
- 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');
+ 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 {
- action = 'show';
- column = hideLinkUrl.searchParams.get('tshow');
+ targetAction = 'show';
+ targetColumn = linkUrl.searchParams.get('tshow');
}
- if (action === 'show') {
- hideArray[column] = 1;
- }
- }
-
- 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.
- });
-
-};
-
-/**
- * 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 search events from the student table.
- *
- */
-export const tableSearch = (event) => {
- if (event.key === 'Meta' || event.ctrlKey) {
- return false;
- }
-
- 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++) {
@@ -359,61 +342,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 fe2ca89c..544408a0 100644
--- a/amd/src/user_preferences.js
+++ b/amd/src/user_preferences.js
@@ -17,7 +17,7 @@
* User preferences JS module.
*
* @module local_assessfreq/user_preferences
- * @package local_assessfreq
+ * @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 06bbc1ed..00000000
--- a/amd/src/zoom_modal.js
+++ /dev/null
@@ -1,107 +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.
- *
- * @package local_assessfreq
- * @copyright 2020 Matt Porritt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define(
- ['core/str', 'core/modal_factory', 'core/fragment', 'core/ajax', 'core/templates', 'local_assessfreq/modal_large',
- 'core/notification'],
- function (Str, ModalFactory, Fragment, Ajax, Templates, ModalLarge, Notification) {
-
- /**
- * Module level variables.
- */
- var ZoomModal = {};
- var contextid;
- var modalObj;
- const spinner = '
'
- + ''
- + '
';
-
- /**
- * Provides zoom functionality for card graphs.
- */
- 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, reject) => {
- Str.get_string('loading', 'core').then((title) => {
- // Create the Modal.
- ModalFactory.create({
- type: ModalLarge.TYPE,
- title: title,
- body: spinner
- })
- .done((modal) => {
- modalObj = modal;
- resolve();
- });
- }).catch(() => {
- reject(new Error('Failed to load string: loading'));
- });
- });
- };
-
- /**
- * Initialise method for quiz dashboard rendering.
- */
- 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 7681cccb..a729e62f 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] = array_column($users, 'userid');
+ 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');
@@ -483,7 +403,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 +417,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 +444,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 +492,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 +509,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 +529,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 +556,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 +578,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 +622,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 +636,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 +683,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 +710,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 +733,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 +755,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 +791,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,12 +804,13 @@ 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');
@@ -896,12 +829,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 +851,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 +861,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 +880,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 +891,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 +922,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 +938,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 +957,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 +969,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 +1000,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 +1016,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 +1033,10 @@ 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 ';
$includehiddencourses = get_config('local_assessfreq', 'hiddencourses');
if (!$includehiddencourses) {
@@ -1052,7 +1044,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 +1075,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 +1092,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 +1110,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 +1122,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 +1137,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 +1161,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 +1205,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,17 +1234,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) {
- $events = array_merge($events, $this->$functionname($module, $from, $to));
+ 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;
@@ -1249,7 +1276,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);
@@ -1266,17 +1293,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';
}
@@ -1286,28 +1317,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'));
@@ -1336,116 +1368,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..270c04d6 100644
--- a/classes/output/renderer.php
+++ b/classes/output/renderer.php
@@ -15,463 +15,70 @@
// 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
- *
- * @return string html to display.
- */
- 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;
- }
-
- $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.
+ * Render each of the assessfreqreport subplugins as tabs to display.
*
- * @return string The heatmap HTML.
+ * @return void
*/
- 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;
+ public function render_reports() : void {
+ global $PAGE;
+ $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(),
+ ];
}
-
- 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]];
- }
+ usort($reportoutputs, function($a, $b) {
+ return $a['weight'] <=> $b['weight'];
+ });
+ $courseselectoptions = [];
+ $courseselect = '';
+ $categories = \core_course_category::make_categories_list();
+
+ foreach ($categories as $categoryid => $category) {
+ $courses = get_courses($categoryid);
+ foreach ($courses as $course) {
+ $courseselectoptions[$course->id] = $category . ' - ' . $course->fullname;
}
- } 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]];
- }
- }
+ if (!empty($courseselectoptions)) {
+ $courseselect = $this->single_select(
+ '#',
+ 'courseid',
+ $courseselectoptions,
+ $PAGE->course->id != SITEID ? $PAGE->course->id : '',
+ ['' => get_string('courseselect', 'local_assessfreq')],
+ );
}
-
- // 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;
+ $output = $this->output->header();
+ $output .= $this->render_from_template(
+ 'local_assessfreq/index',
+ [
+ 'reports' => $reportoutputs,
+ 'courseselect' => $courseselect,
+ ]
+ );
+ $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..fc2f0ed0
--- /dev/null
+++ b/classes/source_base.php
@@ -0,0 +1,176 @@
+.
+
+/**
+ * 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 = [];
+
+ 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;
+
+ $trendlimit = get_config('assessfreqreport_activity_dashboard', 'trendcount');
+ $return = [];
+
+ $trends = $DB->get_records(
+ 'local_assessfreq_trend',
+ ['assessid' => $assessid, 'module' => $this->get_module()],
+ 'timecreated ASC'
+ );
+ if (!$limited) {
+ return $trends;
+ }
+ $modulus = round(count($trends) / $trendlimit);
+ $i = 0;
+ if (count($trends) < $trendlimit) {
+ return $trends;
+ }
+ foreach ($trends as $trend) {
+ if ($i % $modulus == 0) {
+ $return[] = $trend;
+ }
+ $i++;
+ }
+
+ 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 ce873ffa..cb3cd9fb 100644
--- a/history.php
+++ b/history.php
@@ -34,21 +34,21 @@
// 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'),
+ get_string('history:confirmreprocess', 'local_assessfreq'),
new single_button($actionurl, get_string('continue'), 'post', true),
new single_button($cancelurl, get_string('cancel'), 'get')
);
@@ -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..36ab3616 100644
--- a/lang/en/local_assessfreq.php
+++ b/lang/en/local_assessfreq.php
@@ -15,207 +15,49 @@
// 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['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..4df6e71e
--- /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()){if(event.target.classList.contains("active"))return;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()){if(event.target.classList.contains("active"))return;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..ad3ef18a
--- /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 // Don't process already selected links.\n if (event.target.classList.contains('active')) {\n return;\n }\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 // Don't process already selected links.\n if (event.target.classList.contains('active')) {\n return;\n }\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,IAExCC,MAAMM,OAAOF,UAAUI,SAAS,qBAGhCS,MAAQjB,MAAMM,OAAOR,QAAQoB,OACjCR,eAAeC,kBAAkB,gEAAiEM,OAElGH,SAASC,WASXzB,qBAAwBU,WAC1BA,MAAMC,iBACqC,MAAvCD,MAAMM,OAAOU,QAAQjB,cAAuB,IAExCC,MAAMM,OAAOF,UAAUI,SAAS,qBAGhCS,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..4a7d0884
--- /dev/null
+++ b/report/activities_in_progress/amd/src/activities_in_progress.js
@@ -0,0 +1,161 @@
+// 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') {
+ // Don't process already selected links.
+ if (event.target.classList.contains('active')) {
+ return;
+ }
+ 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') {
+ // Don't process already selected links.
+ if (event.target.classList.contains('active')) {
+ return;
+ }
+ 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..5548e3cb
--- /dev/null
+++ b/report/activities_in_progress/classes/output/renderer.php
@@ -0,0 +1,352 @@
+.
+
+/**
+ * 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 ($participants) {
+ $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 = [];
+ $modulepreference = json_decode(
+ get_user_preferences('assessfreqreport_activities_in_progress_modules_preference', '["all"]')
+ );
+ /* @var $source source_base */
+ foreach ($sources as $source) {
+ if (!in_array('all', $modulepreference) && !in_array($source->get_module(), $modulepreference)) {
+ continue;
+ }
+ 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..775d996d
--- /dev/null
+++ b/report/activities_in_progress/lang/en/assessfreqreport_activities_in_progress.php
@@ -0,0 +1,84 @@
+.
+
+/**
+ * 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:trendcount'] = 'Trend chart limit';
+$string['settings:trendcount_desc'] = 'The trend data is run every minute and can contain a lot of data.
+For example an assessment running for 5 days can have 7200 points that can be mapped which can overwhelm the chart.
+This setting specifies the number of points that will be evenly plotted on the graph';
+$string['settings:graphsheading'] = 'Graph settings';
+$string['settings:graphsheading_desc'] = 'Specify the graph settings for each graph report';
+
+$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..f3f76b76
--- /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_factory","local_assessfreq/modal_large","core/fragment","core/ajax","core/templates"],(function(Str,ModalFactory,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=>{ModalFactory.create({type:ModalLarge.TYPE,title:title,body:spinner,large:!0}).done((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..725a68d3
--- /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_factory', 'local_assessfreq/modal_large', 'core/fragment', 'core/ajax', 'core/templates'],\n function(Str, ModalFactory, 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 ModalFactory.create({\n type: ModalLarge.TYPE,\n title: title,\n body: spinner,\n large: true\n }).done((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","ModalFactory","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","done","modal","getRoot","on","processModalForm","e","preventDefault","setBody","hide","updateModalBody","show","formdata","params","JSON","stringify","getOptionPlaceholders","setTitle","loadFragment","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..4a57961a
--- /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_factory', 'local_assessfreq/modal_large', 'core/fragment', 'core/ajax', 'core/templates'],
+ function(Str, ModalFactory, 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.
+ ModalFactory.create({
+ type: ModalLarge.TYPE,
+ title: title,
+ body: spinner,
+ large: true
+ }).done((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..efae39d0
--- /dev/null
+++ b/report/activity_dashboard/lang/en/assessfreqreport_activity_dashboard.php
@@ -0,0 +1,62 @@
+.
+
+/**
+ * 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 limit';
+$string['settings:trendcount_desc'] = 'The trend data is run every minute and can contain a lot of data.
+For example an assessment running for 5 days can have 7200 points that can be mapped which can overwhelm the chart.
+This setting specifies the number of points that will be evenly plotted on the graph';
+
+$string['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..7ccfee24
--- /dev/null
+++ b/report/activity_dashboard/settings.php
@@ -0,0 +1,73 @@
+.
+
+/**
+ * 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_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..c8ecca12
--- /dev/null
+++ b/report/heatmap/templates/filters.mustache
@@ -0,0 +1,58 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @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.
+}}
+
+
diff --git a/templates/index.mustache b/templates/index.mustache
new file mode 100644
index 00000000..2a78ccdd
--- /dev/null
+++ b/templates/index.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 local_assessfreq/index
+
+ Index template.
+}}
+
+
+
+ {{> local_assessfreq/tabs}}
+
+
diff --git a/templates/quiz-refresh-controls.mustache b/templates/quiz-refresh-controls.mustache
deleted file mode 100644
index 7ec30841..00000000
--- a/templates/quiz-refresh-controls.mustache
+++ /dev/null
@@ -1,64 +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 .
-}}
-{{!
- @template local_assessfreq/quiz-refresh-controls
-
- Report Summary template.
-
- Example context (json):
- {
-
- }
-}}
-
diff --git a/templates/quiz-summary-card-content.mustache b/templates/quiz-summary-card-content.mustache
deleted file mode 100644
index 0a4e4f52..00000000
--- a/templates/quiz-summary-card-content.mustache
+++ /dev/null
@@ -1,88 +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 .
-}}
-{{!
- @template local_assessfreq/quiz-summary-card-content
-
- Report Summary template.
-
- Example context (json):
- {
-
- }
-}}
-
{{#pix}} i/preview, core{{/pix}}
diff --git a/templates/scheduler_form_element.mustache b/templates/scheduler_form_element.mustache
deleted file mode 100644
index fa4ed2b6..00000000
--- a/templates/scheduler_form_element.mustache
+++ /dev/null
@@ -1,44 +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 .
-}}
-{{!
- @template core_form/scheduler_form_element
-
- Static form element template. A static form element is an element that just outputs raw HTML.
-
- Classes required for JS:
- * none
-
- Data attributes required for JS:
- * none
-
- Context variables required for this template:
- * element - A context exported from an mform element.
- * element.html - The raw html to display.
-
- Example context (json):
- {
- "label": "Example label",
- "element": { "html": "Example HTML", "staticlabel": true }
- }
-}}
-