blocks/accessreview/amd/src/module.js

  1. // This file is part of Moodle - http://moodle.org/
  2. //
  3. // Moodle is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU General Public License as published by
  5. // the Free Software Foundation, either version 3 of the License, or
  6. // (at your option) any later version.
  7. //
  8. // Moodle is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  15. /**
  16. * Manager for the accessreview block.
  17. *
  18. * @module block_accessreview/module
  19. * @author Max Larkin <max@brickfieldlabs.ie>
  20. * @copyright 2020 Brickfield Education Labs <max@brickfieldlabs.ie>
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. import {call as fetchMany} from 'core/ajax';
  24. import * as Templates from 'core/templates';
  25. import {exception as displayError} from 'core/notification';
  26. /**
  27. * The number of colours used to represent the heatmap. (Indexed on 0.)
  28. * @type {number}
  29. */
  30. const numColours = 2;
  31. /**
  32. * The toggle state of the heatmap.
  33. * @type {boolean}
  34. */
  35. let toggleState = true;
  36. /**
  37. * Renders the HTML template onto a particular HTML element.
  38. * @param {HTMLElement} element The element to attach the HTML to.
  39. * @param {number} errorCount The number of errors on this module/section.
  40. * @param {number} checkCount The number of checks triggered on this module/section.
  41. * @param {String} displayFormat
  42. * @param {Number} minViews
  43. * @param {Number} viewDelta
  44. * @returns {Promise}
  45. */
  46. const renderTemplate = (element, errorCount, checkCount, displayFormat, minViews, viewDelta) => {
  47. // Calculate a weight?
  48. const weight = parseInt((errorCount - minViews) / viewDelta * numColours);
  49. const context = {
  50. resultPassed: !errorCount,
  51. classList: '',
  52. passRate: {
  53. errorCount,
  54. checkCount,
  55. failureRate: Math.round(errorCount / checkCount * 100),
  56. },
  57. };
  58. if (!element) {
  59. return Promise.resolve();
  60. }
  61. const elementClassList = ['block_accessreview'];
  62. if (context.resultPassed) {
  63. elementClassList.push('block_accessreview_success');
  64. } else if (weight) {
  65. elementClassList.push('block_accessreview_danger');
  66. } else {
  67. elementClassList.push('block_accessreview_warning');
  68. }
  69. const showIcons = (displayFormat == 'showicons') || (displayFormat == 'showboth');
  70. const showBackground = (displayFormat == 'showbackground') || (displayFormat == 'showboth');
  71. if (showBackground && !showIcons) {
  72. // Only the background is displayed.
  73. // No need to display the template.
  74. // Note: The case where both the background and icons are shown is handled later to avoid jankiness.
  75. element.classList.add(...elementClassList, 'alert');
  76. return Promise.resolve();
  77. }
  78. if (showIcons && !showBackground) {
  79. context.classList = elementClassList.join(' ');
  80. }
  81. // The icons are displayed either with, or without, the background.
  82. return Templates.renderForPromise('block_accessreview/status', context)
  83. .then(({html, js}) => {
  84. Templates.appendNodeContents(element, html, js);
  85. if (showBackground) {
  86. element.classList.add(...elementClassList, 'alert');
  87. }
  88. return;
  89. })
  90. .catch();
  91. };
  92. /**
  93. * Applies the template to all sections and modules on the course page.
  94. *
  95. * @param {Number} courseId
  96. * @param {String} displayFormat
  97. * @param {Boolean} updatePreference
  98. * @returns {Promise}
  99. */
  100. const showAccessMap = (courseId, displayFormat, updatePreference = false) => {
  101. // Get error data.
  102. return Promise.all(fetchReviewData(courseId, updatePreference))
  103. .then(([sectionData, moduleData]) => {
  104. // Get total data.
  105. const {minViews, viewDelta} = getErrorTotals(sectionData, moduleData);
  106. sectionData.forEach(section => {
  107. const element = document.querySelector(`#section-${section.section} .summary`);
  108. if (!element) {
  109. return;
  110. }
  111. renderTemplate(element, section.numerrors, section.numchecks, displayFormat, minViews, viewDelta);
  112. });
  113. moduleData.forEach(module => {
  114. const element = document.getElementById(`module-${module.cmid}`);
  115. if (!element) {
  116. return;
  117. }
  118. renderTemplate(element, module.numerrors, module.numchecks, displayFormat, minViews, viewDelta);
  119. });
  120. // Change the icon display.
  121. document.querySelector('.icon-accessmap').classList.remove(...['fa-eye-slash']);
  122. document.querySelector('.icon-accessmap').classList.add(...['fa-eye']);
  123. return {
  124. sectionData,
  125. moduleData,
  126. };
  127. })
  128. .catch(displayError);
  129. };
  130. /**
  131. * Hides or removes the templates from the HTML of the current page.
  132. *
  133. * @param {Boolean} updatePreference
  134. */
  135. const hideAccessMap = (updatePreference = false) => {
  136. // Removes the added elements.
  137. document.querySelectorAll('.block_accessreview_view').forEach(node => node.remove());
  138. const classList = [
  139. 'block_accessreview',
  140. 'block_accessreview_success',
  141. 'block_accessreview_warning',
  142. 'block_accessreview_danger',
  143. 'block_accessreview_view',
  144. 'alert',
  145. ];
  146. // Removes the added classes.
  147. document.querySelectorAll('.block_accessreview').forEach(node => node.classList.remove(...classList));
  148. if (updatePreference) {
  149. setToggleStatePreference(false);
  150. }
  151. // Change the icon display.
  152. document.querySelector('.icon-accessmap').classList.remove(...['fa-eye']);
  153. document.querySelector('.icon-accessmap').classList.add(...['fa-eye-slash']);
  154. };
  155. /**
  156. * Toggles the heatmap on/off.
  157. *
  158. * @param {Number} courseId
  159. * @param {String} displayFormat
  160. */
  161. const toggleAccessMap = (courseId, displayFormat) => {
  162. toggleState = !toggleState;
  163. if (!toggleState) {
  164. hideAccessMap(true);
  165. } else {
  166. showAccessMap(courseId, displayFormat, true);
  167. }
  168. };
  169. /**
  170. * Parses information on the errors, generating the min, max and totals.
  171. *
  172. * @param {Object[]} sectionData The error data for course sections.
  173. * @param {Object[]} moduleData The error data for course modules.
  174. * @returns {Object} An object representing the extra error information.
  175. */
  176. const getErrorTotals = (sectionData, moduleData) => {
  177. const totals = {
  178. totalErrors: 0,
  179. totalUsers: 0,
  180. minViews: 0,
  181. maxViews: 0,
  182. viewDelta: 0,
  183. };
  184. [].concat(sectionData, moduleData).forEach(item => {
  185. totals.totalErrors += item.numerrors;
  186. if (item.numerrors < totals.minViews) {
  187. totals.minViews = item.numerrors;
  188. }
  189. if (item.numerrors > totals.maxViews) {
  190. totals.maxViews = item.numerrors;
  191. }
  192. totals.totalUsers += item.numchecks;
  193. });
  194. totals.viewDelta = totals.maxViews - totals.minViews + 1;
  195. return totals;
  196. };
  197. const registerEventListeners = (courseId, displayFormat) => {
  198. document.addEventListener('click', e => {
  199. if (e.target.closest('#toggle-accessmap')) {
  200. e.preventDefault();
  201. toggleAccessMap(courseId, displayFormat);
  202. }
  203. });
  204. };
  205. /**
  206. * Set the user preference for the toggle value.
  207. *
  208. * @param {Boolean} toggleState
  209. * @returns {Promise}
  210. */
  211. const getTogglePreferenceParams = toggleState => {
  212. return {
  213. methodname: 'core_user_update_user_preferences',
  214. args: {
  215. preferences: [{
  216. type: 'block_accessreviewtogglestate',
  217. value: toggleState,
  218. }],
  219. }
  220. };
  221. };
  222. const setToggleStatePreference = toggleState => fetchMany([getTogglePreferenceParams(toggleState)]);
  223. /**
  224. * Fetch the review data.
  225. *
  226. * @param {Number} courseid
  227. * @param {Boolean} updatePreference
  228. * @returns {Promise[]}
  229. */
  230. const fetchReviewData = (courseid, updatePreference = false) => {
  231. const calls = [
  232. {
  233. methodname: 'block_accessreview_get_section_data',
  234. args: {courseid}
  235. },
  236. {
  237. methodname: 'block_accessreview_get_module_data',
  238. args: {courseid}
  239. },
  240. ];
  241. if (updatePreference) {
  242. calls.push(getTogglePreferenceParams(true));
  243. }
  244. return fetchMany(calls);
  245. };
  246. /**
  247. * Setting up the access review module.
  248. * @param {number} toggled A number represnting the state of the review toggle.
  249. * @param {string} displayFormat A string representing the display format for icons.
  250. * @param {number} courseId The course ID.
  251. */
  252. export const init = (toggled, displayFormat, courseId) => {
  253. // Settings consts.
  254. toggleState = toggled == 1;
  255. if (toggleState) {
  256. showAccessMap(courseId, displayFormat);
  257. }
  258. registerEventListeners(courseId, displayFormat);
  259. };