diff --git a/mod/forum/amd/build/local/grades/grader.min.js b/mod/forum/amd/build/local/grades/grader.min.js
index 5ee007bba2c..8db2e6caabe 100644
--- a/mod/forum/amd/build/local/grades/grader.min.js
+++ b/mod/forum/amd/build/local/grades/grader.min.js
@@ -1,2 +1,2 @@
-define ("mod_forum/local/grades/grader",["exports","core/templates","./local/grader/selectors","./local/grader/user_picker","mod_forum/local/layout/fullscreen","./local/grader/gradingpanel","core/toast","core/str"],function(a,b,c,d,e,f,g,h){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"getGradingPanelFunctions",{enumerable:!0,get:function get(){return f.default}});a.launch=void 0;b=i(b);c=i(c);d=function(a){if(a&&a.__esModule){return a}else{var b={};if(null!=a){for(var c in a){if(Object.prototype.hasOwnProperty.call(a,c)){var d=Object.defineProperty&&Object.getOwnPropertyDescriptor?Object.getOwnPropertyDescriptor(a,c):{};if(d.get||d.set){Object.defineProperty(b,c,d)}else{b[c]=a[c]}}}}b.default=a;return b}}(d);f=i(f);function i(a){return a&&a.__esModule?a:{default:a}}function j(a,b){return m(a)||l(a,b)||k()}function k(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}function l(a,b){var c=[],d=!0,e=!1,f=void 0;try{for(var g=a[Symbol.iterator](),h;!(d=(h=g.next()).done);d=!0){c.push(h.value);if(b&&c.length===b)break}}catch(a){e=!0;f=a}finally{try{if(!d&&null!=g["return"])g["return"]()}finally{if(e)throw f}}return c}function m(a){if(Array.isArray(a))return a}function n(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 o(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){n(h,d,e,f,g,"next",a)}function g(a){n(h,d,e,f,g,"throw",a)}f(void 0)})}}var p={grader:{app:"mod_forum/local/grades/grader"}},q=function(a,d){var e=a.querySelector(c.default.regions.pickerRegion);b.default.replaceNodeContents(e,d,"")},r=function(a,b){return[a,b]},s=function(a,d,e){return function(){var f=o(regeneratorRuntime.mark(function f(g){var h,i,k,l,m,n,o,p,q,s;return regeneratorRuntime.wrap(function(f){while(1){switch(f.prev=f.next){case 0:f.next=2;return Promise.all([d(g.id).then(r),e(g.id)]);case 2:h=f.sent;i=j(h,2);k=j(i[0],2);l=k[0];m=k[1];n=i[1];b.default.replaceNodeContents(a.querySelector(c.default.regions.moduleReplace),l,m);f.next=11;return b.default.render(n.templatename,n.grade).then(r);case 11:o=f.sent;p=j(o,2);q=p[0];s=p[1];b.default.replaceNodeContents(a.querySelector(c.default.regions.gradingPanel),q,s);case 16:case"end":return f.stop();}}},f)}));return function(){return f.apply(this,arguments)}}()},t=function(a){var b=a.getContainer();b.addEventListener("click",function(b){if(b.target.closest(c.default.buttons.toggleFullscreen)){b.stopImmediatePropagation();b.preventDefault();a.toggleFullscreen()}else if(b.target.closest(c.default.buttons.closeGrader)){b.stopImmediatePropagation();b.preventDefault();a.close()}})},u=function(a,b){return function(){var d=o(regeneratorRuntime.mark(function d(e){var f;return regeneratorRuntime.wrap(function(d){while(1){switch(d.prev=d.next){case 0:d.prev=0;d.next=3;return b(e.id,a.querySelector(c.default.regions.gradingPanel));case 3:f=d.sent;d.t0=g.add;d.next=7;return(0,h.get_string)("grades:gradesavedfor","mod_forum",e);case 7:d.t1=d.sent;(0,d.t0)(d.t1);return d.abrupt("return",f);case 12:d.prev=12;d.t2=d["catch"](0);throw d.t2;case 15:case"end":return d.stop();}}},d,null,[[0,12]])}));return function(){return d.apply(this,arguments)}}()},v=function(){var a=o(regeneratorRuntime.mark(function a(c,f,g,h){var i,k,l,m,n,o,r,v,w,x,y,z,A=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:i=4.\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/grader\n * @package mod_forum\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Templates from 'core/templates';\n// TODO import Notification from 'core/notification';\nimport Selectors from './local/grader/selectors';\nimport * as UserPicker from './local/grader/user_picker';\nimport {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen';\nimport getGradingPanelFunctions from './local/grader/gradingpanel';\nimport {add as addToast} from 'core/toast';\nimport {get_string as getString} from 'core/str';\n\nconst templateNames = {\n grader: {\n app: 'mod_forum/local/grades/grader',\n },\n};\n\nconst displayUserPicker = (root, html) => {\n const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);\n Templates.replaceNodeContents(pickerRegion, html, '');\n};\n\nconst fetchContentFromRender = (html, js) => {\n return [html, js];\n};\n\nconst getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser) => {\n return async(user) => {\n const [\n [html, js],\n userGrade,\n ] = await Promise.all([\n getContentForUser(user.id).then(fetchContentFromRender),\n getGradeForUser(user.id),\n ]);\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js);\n\n const [\n gradingPanelHtml,\n gradingPanelJS\n ] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender);\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanel), gradingPanelHtml, gradingPanelJS);\n };\n};\n\nconst registerEventListeners = (graderLayout) => {\n const graderContainer = graderLayout.getContainer();\n graderContainer.addEventListener('click', (e) => {\n if (e.target.closest(Selectors.buttons.toggleFullscreen)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n graderLayout.toggleFullscreen();\n } else if (e.target.closest(Selectors.buttons.closeGrader)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n\n graderLayout.close();\n }\n });\n};\n\n/**\n * Get the function used to save a user grade.\n *\n * @param {Element} root The contaienr\n * @param {Function} setGradeForUser The function that will be called.\n * @return {Function}\n */\nconst getSaveUserGradeFunction = (root, setGradeForUser) => {\n return async user => {\n try {\n const result = await setGradeForUser(user.id, root.querySelector(Selectors.regions.gradingPanel));\n addToast(await getString('grades:gradesavedfor', 'mod_forum', user));\n\n return result;\n } catch (error) {\n throw error;\n }\n };\n};\n\n// Make this explicit rather than object\nexport const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {\n initialUserId = 0, moduleName\n} = {}) => {\n\n const [\n graderLayout,\n graderHTML,\n userList,\n ] = await Promise.all([\n createFullScreenWindow({fullscreen: false, showLoader: false}),\n Templates.render(templateNames.grader.app, {moduleName: moduleName}),\n getListOfUsers(),\n ]);\n const graderContainer = graderLayout.getContainer();\n\n Templates.replaceNodeContents(graderContainer, graderHTML, '');\n registerEventListeners(graderLayout);\n const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser);\n\n const pickerHTML = await UserPicker.buildPicker(\n userList,\n initialUserId,\n updateUserContent,\n getSaveUserGradeFunction(graderContainer, setGradeForUser)\n );\n\n displayUserPicker(graderContainer, pickerHTML);\n};\n\nexport {getGradingPanelFunctions};\n"],"file":"grader.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/local/grades/grader.js"],"names":["templateNames","grader","app","displayUserPicker","root","html","pickerRegion","querySelector","Selectors","regions","Templates","replaceNodeContents","fetchContentFromRender","js","getUpdateUserContentFunction","getContentForUser","getGradeForUser","user","Promise","all","id","then","userGrade","moduleReplace","render","templatename","grade","gradingPanelHtml","gradingPanelJS","gradingPanel","registerEventListeners","graderLayout","graderContainer","getContainer","addEventListener","e","target","closest","buttons","toggleFullscreen","stopImmediatePropagation","preventDefault","closeGrader","close","getSaveUserGradeFunction","setGradeForUser","result","addToast","launch","getListOfUsers","initialUserId","moduleName","fullscreen","showLoader","graderHTML","userList","saveGradeFunction","updateUserContent","userPicker","rootNode"],"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 * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/grader\n * @package mod_forum\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Templates from 'core/templates';\n// TODO import Notification from 'core/notification';\nimport Selectors from './local/grader/selectors';\nimport getUserPicker from './local/grader/user_picker';\nimport {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen';\nimport getGradingPanelFunctions from './local/grader/gradingpanel';\nimport {add as addToast} from 'core/toast';\nimport {get_string as getString} from 'core/str';\n\nconst templateNames = {\n grader: {\n app: 'mod_forum/local/grades/grader',\n },\n};\n\nconst displayUserPicker = (root, html) => {\n const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);\n Templates.replaceNodeContents(pickerRegion, html, '');\n};\n\nconst fetchContentFromRender = (html, js) => {\n return [html, js];\n};\n\nconst getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser) => {\n return async(user) => {\n const [\n [html, js],\n userGrade,\n ] = await Promise.all([\n getContentForUser(user.id).then(fetchContentFromRender),\n getGradeForUser(user.id),\n ]);\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js);\n\n const [\n gradingPanelHtml,\n gradingPanelJS\n ] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender);\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanel), gradingPanelHtml, gradingPanelJS);\n };\n};\n\nconst registerEventListeners = (graderLayout) => {\n const graderContainer = graderLayout.getContainer();\n graderContainer.addEventListener('click', (e) => {\n if (e.target.closest(Selectors.buttons.toggleFullscreen)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n graderLayout.toggleFullscreen();\n } else if (e.target.closest(Selectors.buttons.closeGrader)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n\n graderLayout.close();\n }\n });\n};\n\n/**\n * Get the function used to save a user grade.\n *\n * @param {Element} root The contaienr\n * @param {Function} setGradeForUser The function that will be called.\n * @return {Function}\n */\nconst getSaveUserGradeFunction = (root, setGradeForUser) => {\n return async user => {\n try {\n const result = await setGradeForUser(user.id, root.querySelector(Selectors.regions.gradingPanel));\n addToast(await getString('grades:gradesavedfor', 'mod_forum', user));\n\n return result;\n } catch (error) {\n throw error;\n }\n };\n};\n\n// Make this explicit rather than object\nexport const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {\n initialUserId = 0, moduleName\n} = {}) => {\n\n const [\n graderLayout,\n graderHTML,\n userList,\n ] = await Promise.all([\n createFullScreenWindow({fullscreen: false, showLoader: false}),\n Templates.render(templateNames.grader.app, {moduleName: moduleName}),\n getListOfUsers(),\n ]);\n const graderContainer = graderLayout.getContainer();\n\n const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);\n\n Templates.replaceNodeContents(graderContainer, graderHTML, '');\n registerEventListeners(graderLayout);\n const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser);\n\n // Fetch the userpicker for display.\n const userPicker = await getUserPicker(\n userList,\n initialUserId,\n updateUserContent,\n saveGradeFunction\n );\n\n // Display the newly created user picker.\n displayUserPicker(graderContainer, userPicker.rootNode);\n};\n\nexport {getGradingPanelFunctions};\n"],"file":"grader.min.js"}
\ No newline at end of file
diff --git a/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js b/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js
index 93e39935457..9cb271e743c 100644
--- a/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js
+++ b/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js
@@ -1,2 +1,2 @@
-define ("mod_forum/local/grades/local/grader/user_picker",["exports","core/templates","./user_picker/selectors"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.buildPicker=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}function e(a,b){return h(a)||g(a,b)||f()}function f(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}function g(a,b){var c=[],d=!0,e=!1,f=void 0;try{for(var g=a[Symbol.iterator](),h;!(d=(h=g.next()).done);d=!0){c.push(h.value);if(b&&c.length===b)break}}catch(a){e=!0;f=a}finally{try{if(!d&&null!=g["return"])g["return"]()}finally{if(e)throw f}}return c}function h(a){if(Array.isArray(a))return a}function i(a){for(var b=1;bthis.currentUserIndex){this.currentUserIndex=this.userList.length-1}else if(this.currentUserIndex>this.userList.length-1){this.currentUserIndex=0}return this.currentUserIndex}},{key:"currentUser",get:function get(){return e({},this.userList[this.currentUserIndex],{total:this.userList.length,displayIndex:this.currentUserIndex+1})}},{key:"rootNode",get:function get(){return this.root}}]);return a}(),r=function(){var a=m(regeneratorRuntime.mark(function a(b,c,d,e){var f;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:f=new q(b,c,d,e,!1);a.next=3;return f.render();case 3:return a.abrupt("return",f);case 4:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}();a.default=r;return a.default});
//# sourceMappingURL=user_picker.min.js.map
diff --git a/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js.map b/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js.map
index ecf50613293..67fb8a63232 100644
--- a/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js.map
+++ b/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../../../../../src/local/grades/local/grader/user_picker.js"],"names":["renderNavigator","Templates","render","renderUserChange","context","showUser","root","users","currentUserIndex","showUserCallback","user","total","length","displayIndex","Promise","all","html","userRegion","querySelector","Selectors","regions","replaceNodeContents","bindEvents","saveUserCallback","addEventListener","e","button","target","closest","actions","changeUser","parseInt","dataset","direction","buildPicker","currentUserID","document","createElement","findIndex","id"],"mappings":"oNAwBA,OACA,O,ouCAEMA,CAAAA,CAAe,CAAG,UAAM,CAC1B,MAAOC,WAAUC,MAAV,CAAiB,iDAAjB,CAAoE,EAApE,CACV,C,CAEKC,CAAgB,CAAG,SAACC,CAAD,CAAa,CAClC,MAAOH,WAAUC,MAAV,CAAiB,sDAAjB,CAAyEE,CAAzE,CACV,C,CAEKC,CAAQ,4CAAG,WAAMC,CAAN,CAAYC,CAAZ,CAAmBC,CAAnB,CAAqCC,CAArC,iGACPC,CADO,MAENH,CAAK,CAACC,CAAD,CAFC,EAGTG,KAAK,CAAEJ,CAAK,CAACK,MAHJ,CAITC,YAAY,CAAEL,CAAgB,CAAG,CAJxB,kBAMQM,CAAAA,OAAO,CAACC,GAAR,CAAY,CAACZ,CAAgB,CAACO,CAAD,CAAjB,CAAyBD,CAAgB,CAACC,CAAD,CAAzC,CAAZ,CANR,0BAMNM,CANM,MAOPC,CAPO,CAOMX,CAAI,CAACY,aAAL,CAAmBC,UAAUC,OAAV,CAAkBH,UAArC,CAPN,CAQbhB,UAAUoB,mBAAV,CAA8BJ,CAA9B,CAA0CD,CAA1C,CAAgD,EAAhD,EARa,wCAAH,uD,CAWRM,CAAU,CAAG,SAAChB,CAAD,CAAOC,CAAP,CAAcC,CAAd,CAAgCC,CAAhC,CAAkDc,CAAlD,CAAuE,CACtFjB,CAAI,CAACkB,gBAAL,CAAsB,OAAtB,CAA+B,SAACC,CAAD,CAAO,CAClC,GAAMC,CAAAA,CAAM,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBT,UAAUU,OAAV,CAAkBC,UAAnC,CAAf,CACA,GAAIJ,CAAJ,CAAY,CACRH,CAAgB,CAAChB,CAAK,CAACC,CAAD,CAAN,CAAhB,CACAA,CAAgB,EAAIuB,QAAQ,CAACL,CAAM,CAACM,OAAP,CAAeC,SAAhB,CAA5B,CACA5B,CAAQ,CAACC,CAAD,CAAOC,CAAP,CAAcC,CAAd,CAAgCC,CAAhC,CACX,CACJ,CAPD,CAQH,C,CAEYyB,CAAW,4CAAG,WAAM3B,CAAN,CAAa4B,CAAb,CAA4B1B,CAA5B,CAA8Cc,CAA9C,iGACnBjB,CADmB,CACZ8B,QAAQ,CAACC,aAAT,CAAuB,KAAvB,CADY,gBAGFvB,CAAAA,OAAO,CAACC,GAAR,CAAY,CAACf,CAAe,EAAhB,CAAZ,CAHE,0BAGhBgB,CAHgB,MAIvBf,UAAUoB,mBAAV,CAA8Bf,CAA9B,CAAoCU,CAApC,CAA0C,EAA1C,EAEMR,CANiB,CAMED,CAAK,CAAC+B,SAAN,CAAgB,SAAC5B,CAAD,CAAU,CAC/C,MAAOA,CAAAA,CAAI,CAAC6B,EAAL,GAAYR,QAAQ,CAACI,CAAD,CAC9B,CAFwB,CANF,iBAUjB9B,CAAAA,CAAQ,CAACC,CAAD,CAAOC,CAAP,CAAcC,CAAd,CAAgCC,CAAhC,CAVS,SAYvBa,CAAU,CAAChB,CAAD,CAAOC,CAAP,CAAcC,CAAd,CAAgCC,CAAhC,CAAkDc,CAAlD,CAAV,CAZuB,yBAchBjB,CAdgB,2CAAH,uD","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 * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/local/grader/user_picker\n * @package mod_forum\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Selectors from './user_picker/selectors';\n\nconst renderNavigator = () => {\n return Templates.render('mod_forum/local/grades/local/grader/user_picker', {});\n};\n\nconst renderUserChange = (context) => {\n return Templates.render('mod_forum/local/grades/local/grader/user_picker/user', context);\n};\n\nconst showUser = async(root, users, currentUserIndex, showUserCallback) => {\n const user = {\n ...users[currentUserIndex],\n total: users.length,\n displayIndex: currentUserIndex + 1,\n };\n const [html] = await Promise.all([renderUserChange(user), showUserCallback(user)]);\n const userRegion = root.querySelector(Selectors.regions.userRegion);\n Templates.replaceNodeContents(userRegion, html, '');\n};\n\nconst bindEvents = (root, users, currentUserIndex, showUserCallback, saveUserCallback) => {\n root.addEventListener('click', (e) => {\n const button = e.target.closest(Selectors.actions.changeUser);\n if (button) {\n saveUserCallback(users[currentUserIndex]);\n currentUserIndex += parseInt(button.dataset.direction);\n showUser(root, users, currentUserIndex, showUserCallback);\n }\n });\n};\n\nexport const buildPicker = async(users, currentUserID, showUserCallback, saveUserCallback) => {\n let root = document.createElement('div');\n\n const [html] = await Promise.all([renderNavigator()]);\n Templates.replaceNodeContents(root, html, '');\n\n const currentUserIndex = users.findIndex((user) => {\n return user.id === parseInt(currentUserID);\n });\n\n await showUser(root, users, currentUserIndex, showUserCallback);\n\n bindEvents(root, users, currentUserIndex, showUserCallback, saveUserCallback);\n\n return root;\n};\n"],"file":"user_picker.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../../../src/local/grades/local/grader/user_picker.js"],"names":["UserPicker","userList","initialUserId","showUserCallback","preChangeUserCallback","render","currentUserIndex","findIndex","user","id","parseInt","bind","root","document","createElement","renderNavigator","html","js","Templates","replaceNodeContents","showUser","currentUser","registerEventListeners","renderForPromise","context","Promise","all","renderUserChange","userRegion","querySelector","Selectors","regions","addEventListener","e","button","target","closest","actions","changeUser","updateIndex","dataset","direction","length","total","displayIndex","users","currentUserID","userPicker"],"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 * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/local/grader/user_picker\n * @package mod_forum\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Selectors from './user_picker/selectors';\n\nconst templatePath = 'mod_forum/local/grades/local/grader';\n\nclass UserPicker {\n\n /**\n * Constructor for the User Picker.\n *\n * @param {Array} userList List of users\n * @param {Number} initialUserId The ID of the initial user to display\n * @param {Function} showUserCallback The callback used to display the user\n * @param {Function} preChangeUserCallback The callback to use before changing user\n * @param {Bool} render Whether to render on instantiation\n */\n constructor(userList, initialUserId, showUserCallback, preChangeUserCallback, render = true) {\n this.userList = userList;\n this.showUserCallback = showUserCallback;\n this.preChangeUserCallback = preChangeUserCallback;\n\n // Determine the current index.\n this.currentUserIndex = userList.findIndex(user => {\n return user.id === parseInt(initialUserId);\n });\n\n // Ensure that render is bound correctly.\n this.render = this.render.bind(this);\n\n if (render) {\n this.render();\n }\n }\n\n /**\n * Render the user picker.\n */\n async render() {\n // Create the root node.\n this.root = document.createElement('div');\n\n const {html, js} = await this.renderNavigator();\n Templates.replaceNodeContents(this.root, html, js);\n\n // Call the showUser function to show the first user immediately.\n await this.showUser(this.currentUser);\n\n // Ensure that the event listeners are all bound.\n this.registerEventListeners();\n }\n\n /**\n * Render the navigator itself.\n *\n * @returns {Promise}\n */\n renderNavigator() {\n return Templates.renderForPromise(`${templatePath}/user_picker`, {});\n }\n\n /**\n * Render the current user details for the picker.\n *\n * @param {Object} context The data used to render the user picker.\n * @returns {Promise}\n */\n renderUserChange(context) {\n return Templates.renderForPromise(`${templatePath}/user_picker/user`, context);\n }\n\n /**\n * Show the specified user in the picker.\n *\n * @param {Object} user\n */\n async showUser(user) {\n const [{html, js}] = await Promise.all([this.renderUserChange(user), this.showUserCallback(user)]);\n const userRegion = this.root.querySelector(Selectors.regions.userRegion);\n Templates.replaceNodeContents(userRegion, html, js);\n }\n\n /**\n * Register the event listeners for the user picker.\n */\n registerEventListeners() {\n this.root.addEventListener('click', (e) => {\n const button = e.target.closest(Selectors.actions.changeUser);\n if (button) {\n this.preChangeUserCallback(this.currentUser);\n this.updateIndex(parseInt(button.dataset.direction));\n this.showUser(this.currentUser);\n }\n });\n }\n\n /**\n * Update the current user index.\n *\n * @param {Number} direction\n * @returns {Number}}\n */\n updateIndex(direction) {\n this.currentUserIndex += direction;\n\n // Loop around the edges.\n if (this.currentUserIndex < 0) {\n this.currentUserIndex = this.userList.length - 1;\n } else if (this.currentUserIndex > this.userList.length - 1) {\n this.currentUserIndex = 0;\n }\n\n return this.currentUserIndex;\n }\n\n /**\n * Get the details of the user currently shown with the total number of users, and the 1-indexed count of the\n * current user.\n *\n * @returns {Object}\n */\n get currentUser() {\n return {\n ...this.userList[this.currentUserIndex],\n total: this.userList.length,\n displayIndex: this.currentUserIndex + 1,\n };\n }\n\n /**\n * Get the root node for the User Picker.\n *\n * @returns {HTMLElement}\n */\n get rootNode() {\n return this.root;\n }\n}\n\n/**\n * Create a new user picker.\n *\n * @param {Array} users The list of users\n * @param {Number} currentUserID The userid of the current user\n * @param {Function} showUserCallback The function to call to show a specific user\n * @param {Function} preChangeUserCallback The fucntion to call to save the grade for the current user\n * @returns {UserPicker}\n */\nexport default async(users, currentUserID, showUserCallback, preChangeUserCallback) => {\n const userPicker = new UserPicker(users, currentUserID, showUserCallback, preChangeUserCallback, false);\n await userPicker.render();\n\n return userPicker;\n};\n"],"file":"user_picker.min.js"}
\ No newline at end of file
diff --git a/mod/forum/amd/src/local/grades/grader.js b/mod/forum/amd/src/local/grades/grader.js
index 26975091373..0a842c4c1cf 100644
--- a/mod/forum/amd/src/local/grades/grader.js
+++ b/mod/forum/amd/src/local/grades/grader.js
@@ -24,7 +24,7 @@
import Templates from 'core/templates';
// TODO import Notification from 'core/notification';
import Selectors from './local/grader/selectors';
-import * as UserPicker from './local/grader/user_picker';
+import getUserPicker from './local/grader/user_picker';
import {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen';
import getGradingPanelFunctions from './local/grader/gradingpanel';
import {add as addToast} from 'core/toast';
@@ -116,18 +116,22 @@ export const launch = async(getListOfUsers, getContentForUser, getGradeForUser,
]);
const graderContainer = graderLayout.getContainer();
+ const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);
+
Templates.replaceNodeContents(graderContainer, graderHTML, '');
registerEventListeners(graderLayout);
const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser);
- const pickerHTML = await UserPicker.buildPicker(
+ // Fetch the userpicker for display.
+ const userPicker = await getUserPicker(
userList,
initialUserId,
updateUserContent,
- getSaveUserGradeFunction(graderContainer, setGradeForUser)
+ saveGradeFunction
);
- displayUserPicker(graderContainer, pickerHTML);
+ // Display the newly created user picker.
+ displayUserPicker(graderContainer, userPicker.rootNode);
};
export {getGradingPanelFunctions};
diff --git a/mod/forum/amd/src/local/grades/local/grader/user_picker.js b/mod/forum/amd/src/local/grades/local/grader/user_picker.js
index 51f381849c7..252ecfa9ac4 100644
--- a/mod/forum/amd/src/local/grades/local/grader/user_picker.js
+++ b/mod/forum/amd/src/local/grades/local/grader/user_picker.js
@@ -25,49 +25,153 @@
import Templates from 'core/templates';
import Selectors from './user_picker/selectors';
-const renderNavigator = () => {
- return Templates.render('mod_forum/local/grades/local/grader/user_picker', {});
-};
+const templatePath = 'mod_forum/local/grades/local/grader';
-const renderUserChange = (context) => {
- return Templates.render('mod_forum/local/grades/local/grader/user_picker/user', context);
-};
+class UserPicker {
-const showUser = async(root, users, currentUserIndex, showUserCallback) => {
- const user = {
- ...users[currentUserIndex],
- total: users.length,
- displayIndex: currentUserIndex + 1,
- };
- const [html] = await Promise.all([renderUserChange(user), showUserCallback(user)]);
- const userRegion = root.querySelector(Selectors.regions.userRegion);
- Templates.replaceNodeContents(userRegion, html, '');
-};
+ /**
+ * Constructor for the User Picker.
+ *
+ * @param {Array} userList List of users
+ * @param {Number} initialUserId The ID of the initial user to display
+ * @param {Function} showUserCallback The callback used to display the user
+ * @param {Function} preChangeUserCallback The callback to use before changing user
+ * @param {Bool} render Whether to render on instantiation
+ */
+ constructor(userList, initialUserId, showUserCallback, preChangeUserCallback, render = true) {
+ this.userList = userList;
+ this.showUserCallback = showUserCallback;
+ this.preChangeUserCallback = preChangeUserCallback;
-const bindEvents = (root, users, currentUserIndex, showUserCallback, saveUserCallback) => {
- root.addEventListener('click', (e) => {
- const button = e.target.closest(Selectors.actions.changeUser);
- if (button) {
- saveUserCallback(users[currentUserIndex]);
- currentUserIndex += parseInt(button.dataset.direction);
- showUser(root, users, currentUserIndex, showUserCallback);
+ // Determine the current index.
+ this.currentUserIndex = userList.findIndex(user => {
+ return user.id === parseInt(initialUserId);
+ });
+
+ // Ensure that render is bound correctly.
+ this.render = this.render.bind(this);
+
+ if (render) {
+ this.render();
}
- });
-};
+ }
-export const buildPicker = async(users, currentUserID, showUserCallback, saveUserCallback) => {
- let root = document.createElement('div');
+ /**
+ * Render the user picker.
+ */
+ async render() {
+ // Create the root node.
+ this.root = document.createElement('div');
- const [html] = await Promise.all([renderNavigator()]);
- Templates.replaceNodeContents(root, html, '');
+ const {html, js} = await this.renderNavigator();
+ Templates.replaceNodeContents(this.root, html, js);
- const currentUserIndex = users.findIndex((user) => {
- return user.id === parseInt(currentUserID);
- });
+ // Call the showUser function to show the first user immediately.
+ await this.showUser(this.currentUser);
- await showUser(root, users, currentUserIndex, showUserCallback);
+ // Ensure that the event listeners are all bound.
+ this.registerEventListeners();
+ }
- bindEvents(root, users, currentUserIndex, showUserCallback, saveUserCallback);
+ /**
+ * Render the navigator itself.
+ *
+ * @returns {Promise}
+ */
+ renderNavigator() {
+ return Templates.renderForPromise(`${templatePath}/user_picker`, {});
+ }
- return root;
+ /**
+ * Render the current user details for the picker.
+ *
+ * @param {Object} context The data used to render the user picker.
+ * @returns {Promise}
+ */
+ renderUserChange(context) {
+ return Templates.renderForPromise(`${templatePath}/user_picker/user`, context);
+ }
+
+ /**
+ * Show the specified user in the picker.
+ *
+ * @param {Object} user
+ */
+ async showUser(user) {
+ const [{html, js}] = await Promise.all([this.renderUserChange(user), this.showUserCallback(user)]);
+ const userRegion = this.root.querySelector(Selectors.regions.userRegion);
+ Templates.replaceNodeContents(userRegion, html, js);
+ }
+
+ /**
+ * Register the event listeners for the user picker.
+ */
+ registerEventListeners() {
+ this.root.addEventListener('click', (e) => {
+ const button = e.target.closest(Selectors.actions.changeUser);
+ if (button) {
+ this.preChangeUserCallback(this.currentUser);
+ this.updateIndex(parseInt(button.dataset.direction));
+ this.showUser(this.currentUser);
+ }
+ });
+ }
+
+ /**
+ * Update the current user index.
+ *
+ * @param {Number} direction
+ * @returns {Number}}
+ */
+ updateIndex(direction) {
+ this.currentUserIndex += direction;
+
+ // Loop around the edges.
+ if (this.currentUserIndex < 0) {
+ this.currentUserIndex = this.userList.length - 1;
+ } else if (this.currentUserIndex > this.userList.length - 1) {
+ this.currentUserIndex = 0;
+ }
+
+ return this.currentUserIndex;
+ }
+
+ /**
+ * Get the details of the user currently shown with the total number of users, and the 1-indexed count of the
+ * current user.
+ *
+ * @returns {Object}
+ */
+ get currentUser() {
+ return {
+ ...this.userList[this.currentUserIndex],
+ total: this.userList.length,
+ displayIndex: this.currentUserIndex + 1,
+ };
+ }
+
+ /**
+ * Get the root node for the User Picker.
+ *
+ * @returns {HTMLElement}
+ */
+ get rootNode() {
+ return this.root;
+ }
+}
+
+/**
+ * Create a new user picker.
+ *
+ * @param {Array} users The list of users
+ * @param {Number} currentUserID The userid of the current user
+ * @param {Function} showUserCallback The function to call to show a specific user
+ * @param {Function} preChangeUserCallback The fucntion to call to save the grade for the current user
+ * @returns {UserPicker}
+ */
+export default async(users, currentUserID, showUserCallback, preChangeUserCallback) => {
+ const userPicker = new UserPicker(users, currentUserID, showUserCallback, preChangeUserCallback, false);
+ await userPicker.render();
+
+ return userPicker;
};