diff --git a/public/mod/forum/amd/build/grades/expandconversation.min.js b/public/mod/forum/amd/build/grades/expandconversation.min.js index bb810d06b55..6f3ab8e4480 100644 --- a/public/mod/forum/amd/build/grades/expandconversation.min.js +++ b/public/mod/forum/amd/build/grades/expandconversation.min.js @@ -1,10 +1,10 @@ -define("mod_forum/grades/expandconversation",["exports","./grader/selectors","mod_forum/repository","core/notification","core/templates","core/modal_cancel","core/modal_events"],(function(_exports,ForumSelectors,_repository,_notification,_templates,_modal_cancel,ModalEvents){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +define("mod_forum/grades/expandconversation",["exports","./grader/selectors","mod_forum/repository","core/notification","core/templates","core/modal_cancel","core/modal_events","core/ajax"],(function(_exports,ForumSelectors,_repository,_notification,_templates,_modal_cancel,ModalEvents,_ajax){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} /** * This module handles the creation of a Modal that shows the user's post in context of the entire discussion. * * @module mod_forum/grades/expandconversation * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.registerEventListeners=void 0,ForumSelectors=_interopRequireWildcard(ForumSelectors),_repository=_interopRequireDefault(_repository),_templates=_interopRequireDefault(_templates),_modal_cancel=_interopRequireDefault(_modal_cancel),ModalEvents=_interopRequireWildcard(ModalEvents);_exports.registerEventListeners=rootNode=>{rootNode.addEventListener("click",(e=>{const rootNode=e.target.closest(ForumSelectors.expandConversation);if(rootNode){e.preventDefault();try{!async function(rootNode){let{focusOnClose:focusOnClose=null}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const postId=rootNode.dataset.postid,discussionId=rootNode.dataset.discussionid,discussionName=rootNode.dataset.name,experimentalDisplayMode="1"==rootNode.dataset.experimentalDisplayMode,[allPosts,modal]=await Promise.all([_repository.default.getDiscussionPosts(parseInt(discussionId)),_modal_cancel.default.create({title:discussionName,large:!0,removeOnClose:!0,returnElement:focusOnClose})]),postsById=new Map(allPosts.posts.map((post=>(post.readonly=!0,post.hasreplies=!1,post.replies=[],[post.id,post]))));let posts=[];allPosts.posts.forEach((post=>{if(post.parentid){const parent=postsById.get(post.parentid);parent?(post.parentauthorname=parent.author.fullname,parent.hasreplies=!0,parent.replies.push(post)):posts.push(post)}else posts.push(post)})),modal.getRoot().on(ModalEvents.bodyRendered,(()=>{const relevantPost=modal.getRoot()[0].querySelector("#p".concat(postId));relevantPost&&relevantPost.scrollIntoView({behavior:"smooth"})})),modal.show();const templatePromise=_templates.default.render("mod_forum/grades/grader/discussion/post_modal",{posts:posts,experimentaldisplaymode:experimentalDisplayMode});modal.setBody(templatePromise)}(rootNode,{focusOnClose:e.target})}catch(err){(0,_notification.exception)(err)}}}))}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.registerEventListeners=void 0,ForumSelectors=_interopRequireWildcard(ForumSelectors),_repository=_interopRequireDefault(_repository),_templates=_interopRequireDefault(_templates),_modal_cancel=_interopRequireDefault(_modal_cancel),ModalEvents=_interopRequireWildcard(ModalEvents),_ajax=_interopRequireDefault(_ajax);_exports.registerEventListeners=rootNode=>{rootNode.addEventListener("click",(e=>{const rootNode=e.target.closest(ForumSelectors.expandConversation);if(rootNode){e.preventDefault();try{!async function(rootNode){let{focusOnClose:focusOnClose=null}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const postId=rootNode.dataset.postid,discussionId=rootNode.dataset.discussionid,discussionName=rootNode.dataset.name,experimentalDisplayMode="1"==rootNode.dataset.experimentalDisplayMode,[allPosts,modal]=await Promise.all([_repository.default.getDiscussionPosts(parseInt(discussionId)),_modal_cancel.default.create({title:discussionName,large:!0,removeOnClose:!0,returnElement:focusOnClose})]),postsById=new Map(allPosts.posts.map((post=>(post.readonly=!0,post.hasreplies=!1,post.replies=[],[post.id,post]))));let posts=[];allPosts.posts.forEach((post=>{if(post.parentid){const parent=postsById.get(post.parentid);parent?(post.parentauthorname=parent.author.fullname,parent.hasreplies=!0,parent.replies.push(post)):posts.push(post)}else posts.push(post)})),modal.getRoot().on(ModalEvents.bodyRendered,(()=>{const relevantPost=modal.getRoot()[0].querySelector("#p".concat(postId));relevantPost&&relevantPost.scrollIntoView({behavior:"smooth"})})),modal.show();const templatePromise=_templates.default.render("mod_forum/grades/grader/discussion/post_modal",{posts:posts,experimentaldisplaymode:experimentalDisplayMode});modal.setBody(templatePromise)}(rootNode,{focusOnClose:e.target})}catch(err){(0,_notification.exception)(err)}}}));document.querySelectorAll(ForumSelectors.showContent).forEach((element=>{element.addEventListener("click",(e=>{const target=e.currentTarget,postId=parseInt(target.dataset.parentPostid,10),discussionId=parseInt(target.dataset.parentDiscussionid,10);markPostsRead([postId],discussionId)}))}))};const markPostsRead=(postIds,discussionId)=>{_ajax.default.call([{methodname:"mod_forum_mark_posts_read",args:{postids:postIds,discussionid:discussionId}}])[0].catch(_notification.exception.exception)}})); //# sourceMappingURL=expandconversation.min.js.map \ No newline at end of file diff --git a/public/mod/forum/amd/build/grades/expandconversation.min.js.map b/public/mod/forum/amd/build/grades/expandconversation.min.js.map index 15d89c86883..4532e3f3480 100644 --- a/public/mod/forum/amd/build/grades/expandconversation.min.js.map +++ b/public/mod/forum/amd/build/grades/expandconversation.min.js.map @@ -1 +1 @@ -{"version":3,"file":"expandconversation.min.js","sources":["../../src/grades/expandconversation.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 * This module handles the creation of a Modal that shows the user's post in context of the entire discussion.\n *\n * @module mod_forum/grades/expandconversation\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as ForumSelectors from './grader/selectors';\nimport Repository from 'mod_forum/repository';\nimport {exception as showException} from \"core/notification\";\nimport Templates from 'core/templates';\nimport Modal from 'core/modal_cancel';\nimport * as ModalEvents from 'core/modal_events';\n\n/**\n * Find the Node containing the gradable details from the provided node by searching up the tree.\n *\n * @param {HTMLElement} node\n * @returns {HTMLElement}\n */\nconst findGradableNode = node => node.closest(ForumSelectors.expandConversation);\n\n/**\n * Show the post in context in a modal.\n *\n * @param {HTMLElement} rootNode The button that has been clicked\n * @param {object} param\n * @param {bool} [param.focusOnClose=null]\n */\nconst showPostInContext = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const postId = rootNode.dataset.postid;\n const discussionId = rootNode.dataset.discussionid;\n const discussionName = rootNode.dataset.name;\n const experimentalDisplayMode = rootNode.dataset.experimentalDisplayMode == \"1\";\n\n const [\n allPosts,\n modal,\n ] = await Promise.all([\n Repository.getDiscussionPosts(parseInt(discussionId)),\n Modal.create({\n title: discussionName,\n large: true,\n removeOnClose: true,\n returnElement: focusOnClose,\n }),\n ]);\n\n const postsById = new Map(allPosts.posts.map(post => {\n post.readonly = true;\n post.hasreplies = false;\n post.replies = [];\n return [post.id, post];\n }));\n\n let posts = [];\n allPosts.posts.forEach(post => {\n if (post.parentid) {\n const parent = postsById.get(post.parentid);\n if (parent) {\n post.parentauthorname = parent.author.fullname;\n parent.hasreplies = true;\n parent.replies.push(post);\n } else {\n posts.push(post);\n }\n } else {\n posts.push(post);\n }\n });\n\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n const relevantPost = modal.getRoot()[0].querySelector(`#p${postId}`);\n if (relevantPost) {\n relevantPost.scrollIntoView({behavior: \"smooth\"});\n }\n });\n\n modal.show();\n\n // Note: We do not use await here because it messes with the Modal transitions.\n const templatePromise = Templates.render('mod_forum/grades/grader/discussion/post_modal', {\n posts,\n experimentaldisplaymode: experimentalDisplayMode\n });\n modal.setBody(templatePromise);\n};\n\n/**\n * Register event listeners for the expand conversations button.\n *\n * @param {HTMLElement} rootNode The root to listen to.\n */\nexport const registerEventListeners = (rootNode) => {\n rootNode.addEventListener('click', (e) => {\n const rootNode = findGradableNode(e.target);\n\n if (rootNode) {\n e.preventDefault();\n\n try {\n showPostInContext(rootNode, {\n focusOnClose: e.target,\n });\n } catch (err) {\n showException(err);\n }\n }\n });\n};\n"],"names":["rootNode","addEventListener","e","target","closest","ForumSelectors","expandConversation","preventDefault","async","focusOnClose","postId","dataset","postid","discussionId","discussionid","discussionName","name","experimentalDisplayMode","allPosts","modal","Promise","all","Repository","getDiscussionPosts","parseInt","Modal","create","title","large","removeOnClose","returnElement","postsById","Map","posts","map","post","readonly","hasreplies","replies","id","forEach","parentid","parent","get","parentauthorname","author","fullname","push","getRoot","on","ModalEvents","bodyRendered","relevantPost","querySelector","scrollIntoView","behavior","show","templatePromise","Templates","render","experimentaldisplaymode","setBody","showPostInContext","err"],"mappings":";;;;;;;8XA8GuCA,WACnCA,SAASC,iBAAiB,SAAUC,UAC1BF,SAA4BE,EAAEC,OA7ENC,QAAQC,eAAeC,uBA+EjDN,SAAU,CACVE,EAAEK,sBAvEYC,eAAMR,cAAUS,aACtCA,aAAe,6DACf,SACMC,OAASV,SAASW,QAAQC,OAC1BC,aAAeb,SAASW,QAAQG,aAChCC,eAAiBf,SAASW,QAAQK,KAClCC,wBAAsE,KAA5CjB,SAASW,QAAQM,yBAG7CC,SACAC,aACMC,QAAQC,IAAI,CAClBC,oBAAWC,mBAAmBC,SAASX,eACvCY,sBAAMC,OAAO,CACTC,MAAOZ,eACPa,OAAO,EACPC,eAAe,EACfC,cAAerB,iBAIjBsB,UAAY,IAAIC,IAAId,SAASe,MAAMC,KAAIC,OACzCA,KAAKC,UAAW,EAChBD,KAAKE,YAAa,EAClBF,KAAKG,QAAU,GACR,CAACH,KAAKI,GAAIJ,cAGjBF,MAAQ,GACZf,SAASe,MAAMO,SAAQL,UACfA,KAAKM,SAAU,OACTC,OAASX,UAAUY,IAAIR,KAAKM,UAC9BC,QACAP,KAAKS,iBAAmBF,OAAOG,OAAOC,SACtCJ,OAAOL,YAAa,EACpBK,OAAOJ,QAAQS,KAAKZ,OAEpBF,MAAMc,KAAKZ,WAGfF,MAAMc,KAAKZ,SAInBhB,MAAM6B,UAAUC,GAAGC,YAAYC,cAAc,WACnCC,aAAejC,MAAM6B,UAAU,GAAGK,0BAAmB3C,SACvD0C,cACAA,aAAaE,eAAe,CAACC,SAAU,cAI/CpC,MAAMqC,aAGAC,gBAAkBC,mBAAUC,OAAO,gDAAiD,CACtF1B,MAAAA,MACA2B,wBAAyB3C,0BAE7BE,MAAM0C,QAAQJ,iBAgBFK,CAAkB9D,SAAU,CACxBS,aAAcP,EAAEC,SAEtB,MAAO4D,iCACSA"} \ No newline at end of file +{"version":3,"file":"expandconversation.min.js","sources":["../../src/grades/expandconversation.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 * This module handles the creation of a Modal that shows the user's post in context of the entire discussion.\n *\n * @module mod_forum/grades/expandconversation\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as ForumSelectors from './grader/selectors';\nimport Repository from 'mod_forum/repository';\nimport {exception as showException} from \"core/notification\";\nimport Templates from 'core/templates';\nimport Modal from 'core/modal_cancel';\nimport * as ModalEvents from 'core/modal_events';\nimport Ajax from 'core/ajax';\n\n/**\n * Find the Node containing the gradable details from the provided node by searching up the tree.\n *\n * @param {HTMLElement} node\n * @returns {HTMLElement}\n */\nconst findGradableNode = node => node.closest(ForumSelectors.expandConversation);\n\n/**\n * Show the post in context in a modal.\n *\n * @param {HTMLElement} rootNode The button that has been clicked\n * @param {object} param\n * @param {bool} [param.focusOnClose=null]\n */\nconst showPostInContext = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const postId = rootNode.dataset.postid;\n const discussionId = rootNode.dataset.discussionid;\n const discussionName = rootNode.dataset.name;\n const experimentalDisplayMode = rootNode.dataset.experimentalDisplayMode == \"1\";\n\n const [\n allPosts,\n modal,\n ] = await Promise.all([\n Repository.getDiscussionPosts(parseInt(discussionId)),\n Modal.create({\n title: discussionName,\n large: true,\n removeOnClose: true,\n returnElement: focusOnClose,\n }),\n ]);\n\n const postsById = new Map(allPosts.posts.map(post => {\n post.readonly = true;\n post.hasreplies = false;\n post.replies = [];\n return [post.id, post];\n }));\n\n let posts = [];\n allPosts.posts.forEach(post => {\n if (post.parentid) {\n const parent = postsById.get(post.parentid);\n if (parent) {\n post.parentauthorname = parent.author.fullname;\n parent.hasreplies = true;\n parent.replies.push(post);\n } else {\n posts.push(post);\n }\n } else {\n posts.push(post);\n }\n });\n\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n const relevantPost = modal.getRoot()[0].querySelector(`#p${postId}`);\n if (relevantPost) {\n relevantPost.scrollIntoView({behavior: \"smooth\"});\n }\n });\n\n modal.show();\n\n // Note: We do not use await here because it messes with the Modal transitions.\n const templatePromise = Templates.render('mod_forum/grades/grader/discussion/post_modal', {\n posts,\n experimentaldisplaymode: experimentalDisplayMode\n });\n modal.setBody(templatePromise);\n};\n\n/**\n * Register event listeners for the expand conversations button.\n *\n * @param {HTMLElement} rootNode The root to listen to.\n */\nexport const registerEventListeners = (rootNode) => {\n rootNode.addEventListener('click', (e) => {\n const rootNode = findGradableNode(e.target);\n\n if (rootNode) {\n e.preventDefault();\n\n try {\n showPostInContext(rootNode, {\n focusOnClose: e.target,\n });\n } catch (err) {\n showException(err);\n }\n }\n });\n\n // Each parent post can mark its own read state when it is clicked.\n const parentContent = document.querySelectorAll(ForumSelectors.showContent);\n parentContent.forEach(element => {\n element.addEventListener('click', (e) => {\n const target = e.currentTarget;\n const postId = parseInt(target.dataset.parentPostid, 10);\n const discussionId = parseInt(target.dataset.parentDiscussionid, 10);\n markPostsRead([postId], discussionId);\n });\n });\n};\n\n/**\n * Mark posts as read.\n *\n * @param {number[]} postIds The posts to mark as read\n * @param {number} discussionId The discussion the posts belong to\n */\nconst markPostsRead = (postIds, discussionId) => {\n Ajax.call([{\n methodname: 'mod_forum_mark_posts_read',\n args: {\n postids: postIds,\n discussionid: discussionId\n },\n }])[0].catch(showException.exception);\n};\n"],"names":["rootNode","addEventListener","e","target","closest","ForumSelectors","expandConversation","preventDefault","async","focusOnClose","postId","dataset","postid","discussionId","discussionid","discussionName","name","experimentalDisplayMode","allPosts","modal","Promise","all","Repository","getDiscussionPosts","parseInt","Modal","create","title","large","removeOnClose","returnElement","postsById","Map","posts","map","post","readonly","hasreplies","replies","id","forEach","parentid","parent","get","parentauthorname","author","fullname","push","getRoot","on","ModalEvents","bodyRendered","relevantPost","querySelector","scrollIntoView","behavior","show","templatePromise","Templates","render","experimentaldisplaymode","setBody","showPostInContext","err","document","querySelectorAll","showContent","element","currentTarget","parentPostid","parentDiscussionid","markPostsRead","postIds","call","methodname","args","postids","catch","showException","exception"],"mappings":";;;;;;;kaA+GuCA,WACnCA,SAASC,iBAAiB,SAAUC,UAC1BF,SAA4BE,EAAEC,OA7ENC,QAAQC,eAAeC,uBA+EjDN,SAAU,CACVE,EAAEK,sBAvEYC,eAAMR,cAAUS,aACtCA,aAAe,6DACf,SACMC,OAASV,SAASW,QAAQC,OAC1BC,aAAeb,SAASW,QAAQG,aAChCC,eAAiBf,SAASW,QAAQK,KAClCC,wBAAsE,KAA5CjB,SAASW,QAAQM,yBAG7CC,SACAC,aACMC,QAAQC,IAAI,CAClBC,oBAAWC,mBAAmBC,SAASX,eACvCY,sBAAMC,OAAO,CACTC,MAAOZ,eACPa,OAAO,EACPC,eAAe,EACfC,cAAerB,iBAIjBsB,UAAY,IAAIC,IAAId,SAASe,MAAMC,KAAIC,OACzCA,KAAKC,UAAW,EAChBD,KAAKE,YAAa,EAClBF,KAAKG,QAAU,GACR,CAACH,KAAKI,GAAIJ,cAGjBF,MAAQ,GACZf,SAASe,MAAMO,SAAQL,UACfA,KAAKM,SAAU,OACTC,OAASX,UAAUY,IAAIR,KAAKM,UAC9BC,QACAP,KAAKS,iBAAmBF,OAAOG,OAAOC,SACtCJ,OAAOL,YAAa,EACpBK,OAAOJ,QAAQS,KAAKZ,OAEpBF,MAAMc,KAAKZ,WAGfF,MAAMc,KAAKZ,SAInBhB,MAAM6B,UAAUC,GAAGC,YAAYC,cAAc,WACnCC,aAAejC,MAAM6B,UAAU,GAAGK,0BAAmB3C,SACvD0C,cACAA,aAAaE,eAAe,CAACC,SAAU,cAI/CpC,MAAMqC,aAGAC,gBAAkBC,mBAAUC,OAAO,gDAAiD,CACtF1B,MAAAA,MACA2B,wBAAyB3C,0BAE7BE,MAAM0C,QAAQJ,iBAgBFK,CAAkB9D,SAAU,CACxBS,aAAcP,EAAEC,SAEtB,MAAO4D,iCACSA,UAMJC,SAASC,iBAAiB5D,eAAe6D,aACjD1B,SAAQ2B,UAClBA,QAAQlE,iBAAiB,SAAUC,UACzBC,OAASD,EAAEkE,cACX1D,OAASc,SAASrB,OAAOQ,QAAQ0D,aAAc,IAC/CxD,aAAeW,SAASrB,OAAOQ,QAAQ2D,mBAAoB,IACjEC,cAAc,CAAC7D,QAASG,2BAW9B0D,cAAgB,CAACC,QAAS3D,8BACvB4D,KAAK,CAAC,CACPC,WAAY,4BACZC,KAAM,CACFC,QAASJ,QACT1D,aAAcD,iBAElB,GAAGgE,MAAMC,wBAAcC"} \ No newline at end of file diff --git a/public/mod/forum/amd/build/grades/grader/selectors.min.js b/public/mod/forum/amd/build/grades/grader/selectors.min.js index a0e248924b2..ed7c8a99563 100644 --- a/public/mod/forum/amd/build/grades/grader/selectors.min.js +++ b/public/mod/forum/amd/build/grades/grader/selectors.min.js @@ -1,3 +1,3 @@ -define("mod_forum/grades/grader/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={launch:'[data-grade-action="launch"]',gradableItem:"[data-gradable-itemtype]",gradableItems:{wholeForum:'[data-gradable-itemtype="forum"]'},expandConversation:'[data-action="view-context"]',posts:'[data-region="posts"]',viewGrade:'[data-grade-action="view"]'},_exports.default})); +define("mod_forum/grades/grader/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={launch:'[data-grade-action="launch"]',gradableItem:"[data-gradable-itemtype]",gradableItems:{wholeForum:'[data-gradable-itemtype="forum"]'},expandConversation:'[data-action="view-context"]',posts:'[data-region="posts"]',viewGrade:'[data-grade-action="view"]',showContent:".show-content-button"},_exports.default})); //# sourceMappingURL=selectors.min.js.map \ No newline at end of file diff --git a/public/mod/forum/amd/build/grades/grader/selectors.min.js.map b/public/mod/forum/amd/build/grades/grader/selectors.min.js.map index 5a27467ddef..6b56ba1cc6a 100644 --- a/public/mod/forum/amd/build/grades/grader/selectors.min.js.map +++ b/public/mod/forum/amd/build/grades/grader/selectors.min.js.map @@ -1 +1 @@ -{"version":3,"file":"selectors.min.js","sources":["../../../src/grades/grader/selectors.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 * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/grades/grader/selectors\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n launch: '[data-grade-action=\"launch\"]',\n gradableItem: '[data-gradable-itemtype]',\n gradableItems: {\n wholeForum: '[data-gradable-itemtype=\"forum\"]',\n },\n expandConversation: '[data-action=\"view-context\"]',\n posts: '[data-region=\"posts\"]',\n viewGrade: '[data-grade-action=\"view\"]',\n};\n"],"names":["launch","gradableItem","gradableItems","wholeForum","expandConversation","posts","viewGrade"],"mappings":"mLAsBe,CACXA,OAAQ,+BACRC,aAAc,2BACdC,cAAe,CACXC,WAAY,oCAEhBC,mBAAoB,+BACpBC,MAAO,wBACPC,UAAW"} \ No newline at end of file +{"version":3,"file":"selectors.min.js","sources":["../../../src/grades/grader/selectors.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 * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/grades/grader/selectors\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n launch: '[data-grade-action=\"launch\"]',\n gradableItem: '[data-gradable-itemtype]',\n gradableItems: {\n wholeForum: '[data-gradable-itemtype=\"forum\"]',\n },\n expandConversation: '[data-action=\"view-context\"]',\n posts: '[data-region=\"posts\"]',\n viewGrade: '[data-grade-action=\"view\"]',\n showContent: '.show-content-button',\n};\n"],"names":["launch","gradableItem","gradableItems","wholeForum","expandConversation","posts","viewGrade","showContent"],"mappings":"mLAsBe,CACXA,OAAQ,+BACRC,aAAc,2BACdC,cAAe,CACXC,WAAY,oCAEhBC,mBAAoB,+BACpBC,MAAO,wBACPC,UAAW,6BACXC,YAAa"} \ No newline at end of file diff --git a/public/mod/forum/amd/build/local/grades/grader.min.js b/public/mod/forum/amd/build/local/grades/grader.min.js index 1389248dacf..7b8ad7f3268 100644 --- a/public/mod/forum/amd/build/local/grades/grader.min.js +++ b/public/mod/forum/amd/build/local/grades/grader.min.js @@ -5,6 +5,6 @@ define("mod_forum/local/grades/grader",["exports","core/templates","./local/grad * @module mod_forum/local/grades/grader * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),Object.defineProperty(_exports,"getGradingPanelFunctions",{enumerable:!0,get:function(){return _gradingpanel.default}}),_exports.view=_exports.launch=void 0,_templates=_interopRequireDefault(_templates),_selectors=_interopRequireDefault(_selectors),_user_picker=_interopRequireDefault(_user_picker),_gradingpanel=_interopRequireDefault(_gradingpanel),_modal_cancel=_interopRequireDefault(_modal_cancel),_drawer_events=_interopRequireDefault(_drawer_events);const templateNames_grader={app:"mod_forum/local/grades/grader",gradingPanel:{error:"mod_forum/local/grades/local/grader/gradingpanel/error"},searchResults:"mod_forum/local/grades/local/grader/user_picker/user_search",status:"mod_forum/local/grades/local/grader/status"},displayUserPicker=(root,html)=>{const pickerRegion=root.querySelector(_selectors.default.regions.pickerRegion);_templates.default.replaceNodeContents(pickerRegion,html,"")},fetchContentFromRender=(html,js)=>[html,js],getUpdateUserContentFunction=(root,getContentForUser,getGradeForUser,saveGradeForUser)=>{let firstLoad=!0;return async user=>{const spinner=firstLoad?null:(0,_loadingicon.addIconToContainerWithPromise)(root),[[html,js],userGrade]=await Promise.all([getContentForUser(user.id).then(fetchContentFromRender),getGradeForUser(user.id)]);_templates.default.replaceNodeContents(root.querySelector(_selectors.default.regions.moduleReplace),html,js);const[gradingPanelHtml,gradingPanelJS]=await _templates.default.render(userGrade.templatename,userGrade.grade).then(fetchContentFromRender),panelContainer=root.querySelector(_selectors.default.regions.gradingPanelContainer),panel=panelContainer.querySelector(_selectors.default.regions.gradingPanel);_templates.default.replaceNodeContents(panel,gradingPanelHtml,gradingPanelJS);const form=panel.querySelector("form");return(0,_comparison.fillInitialValues)(form),form.addEventListener("submit",(event=>{saveGradeForUser(user),event.preventDefault()})),panelContainer.scrollTop=0,firstLoad=!1,spinner&&spinner.resolve(),userGrade}},hideSearchResultContainer=(bodyContainer,userPickerContainer,searchResultsContainer)=>{bodyContainer.classList.remove("hidden"),userPickerContainer.classList.remove("hidden"),searchResultsContainer.classList.add("hidden")},hideUserSearchInput=(toggleSearchButton,searchContainer,searchInput)=>{searchContainer.classList.add("collapsed"),toggleSearchButton.setAttribute("aria-expanded","false"),toggleSearchButton.classList.add("collapse"),toggleSearchButton.classList.remove("expand"),toggleSearchButton.focus();searchContainer.parentElement.querySelector(_selectors.default.regions.gradingInfoContainer).removeAttribute("aria-hidden");const collapseGradingDrawer=searchContainer.parentElement.querySelector(_selectors.default.buttons.collapseGradingDrawer);collapseGradingDrawer.removeAttribute("aria-hidden"),collapseGradingDrawer.setAttribute("tabindex","0"),searchInput.value=""},renderSearchResults=async(searchResultsContainer,users)=>{const{html:html,js:js}=await _templates.default.renderForPromise(templateNames_grader.searchResults,{users:users});_templates.default.replaceNodeContents(searchResultsContainer,html,js)},registerEventListeners=(graderLayout,userPicker,saveGradeFunction,userList)=>{const graderContainer=graderLayout.getContainer(),toggleSearchButton=graderContainer.querySelector(_selectors.default.buttons.toggleSearch),searchInputContainer=graderContainer.querySelector(_selectors.default.regions.userSearchContainer),searchInput=searchInputContainer.querySelector(_selectors.default.regions.userSearchInput),bodyContainer=graderContainer.querySelector(_selectors.default.regions.bodyContainer),userPickerContainer=graderContainer.querySelector(_selectors.default.regions.pickerRegion),searchResultsContainer=graderContainer.querySelector(_selectors.default.regions.searchResultsContainer);graderContainer.addEventListener("click",(e=>{if(e.target.closest(_selectors.default.buttons.toggleFullscreen))return e.stopImmediatePropagation(),e.preventDefault(),void graderLayout.toggleFullscreen();if(e.target.closest(_selectors.default.buttons.closeGrader))return e.stopImmediatePropagation(),e.preventDefault(),void graderLayout.close();if(e.target.closest(_selectors.default.buttons.saveGrade)&&saveGradeFunction(userPicker.currentUser),e.target.closest(_selectors.default.buttons.toggleSearch))return void("true"===toggleSearchButton.getAttribute("aria-expanded")?(hideUserSearchInput(toggleSearchButton,searchInputContainer,searchInput),hideSearchResultContainer(bodyContainer,userPickerContainer,searchResultsContainer),searchResultsContainer.innerHTML=""):(((toggleSearchButton,searchContainer,searchInput)=>{searchContainer.classList.remove("collapsed"),toggleSearchButton.setAttribute("aria-expanded","true"),toggleSearchButton.classList.add("expand"),toggleSearchButton.classList.remove("collapse"),searchContainer.parentElement.querySelector(_selectors.default.regions.gradingInfoContainer).setAttribute("aria-hidden","true");const collapseGradingDrawer=searchContainer.parentElement.querySelector(_selectors.default.buttons.collapseGradingDrawer);collapseGradingDrawer.setAttribute("aria-hidden","true"),collapseGradingDrawer.setAttribute("tabindex","-1"),searchInput.focus()})(toggleSearchButton,searchInputContainer,searchInput),((bodyContainer,userPickerContainer,searchResultsContainer)=>{bodyContainer.classList.add("hidden"),userPickerContainer.classList.add("hidden"),searchResultsContainer.classList.remove("hidden")})(bodyContainer,userPickerContainer,searchResultsContainer),renderSearchResults(searchResultsContainer,userList)));const selectUserButton=e.target.closest(_selectors.default.buttons.selectUser);if(selectUserButton){const userId=selectUserButton.getAttribute("data-userid"),user=userList.find((user=>user.id==userId));userPicker.setUserId(userId),userPicker.showUser(user),hideUserSearchInput(toggleSearchButton,searchInputContainer,searchInput),hideSearchResultContainer(bodyContainer,userPickerContainer,searchResultsContainer),searchResultsContainer.innerHTML=""}})),searchInput.addEventListener("input",(0,_utils.debounce)((()=>{const users=((userList,searchTerm)=>""===searchTerm?userList:(searchTerm=searchTerm.toLowerCase(),userList.filter((user=>user.fullname.toLowerCase().includes(searchTerm)))))(userList,searchInput.value);renderSearchResults(searchResultsContainer,users)}),300)),(0,_pubsub.subscribe)(_drawer_events.default.DRAWER_HIDDEN,(drawerRoot=>{drawerRoot[0].querySelector(_selectors.default.regions.gradingPanel)&&setContentContainerMargin(graderContainer,0)})),(0,_pubsub.subscribe)(_drawer_events.default.DRAWER_SHOWN,(drawerRoot=>{const gradingPanel=drawerRoot[0];gradingPanel.querySelector(_selectors.default.regions.gradingPanel)&&setContentContainerMargin(graderContainer,gradingPanel.offsetWidth)}))},setContentContainerMargin=(graderContainer,rightMargin)=>{const contentContainer=graderContainer.querySelector(_selectors.default.regions.moduleContainer);contentContainer&&(contentContainer.style.marginRight="".concat(rightMargin,"px"))},getSaveUserGradeFunction=(root,setGradeForUser)=>async user=>{try{root.querySelector(_selectors.default.regions.gradingPanelErrors).innerHTML="";const result=await setGradeForUser(user.id,root.querySelector(_selectors.default.values.sendStudentNotifications).value,root.querySelector(_selectors.default.regions.gradingPanel));return result.success&&(0,_toast.add)(await(0,_str.getString)("grades:gradesavedfor","mod_forum",user)),result.failed&&displayGradingError(root,user,result.error),result}catch(err){return displayGradingError(root,user,err),(0,_normalise.failedUpdate)(err)}},displayGradingError=async(root,user,err)=>{const[{html:html,js:js},errorString]=await Promise.all([_templates.default.renderForPromise(templateNames_grader.gradingPanel.error,{error:err}),await(0,_str.getString)("grades:gradesavefailed","mod_forum",{error:err.message,...user})]);_templates.default.replaceNodeContents(root.querySelector(_selectors.default.regions.gradingPanelErrors),html,js),(0,_toast.add)(errorString,{type:"warning"})};_exports.launch=async function(getListOfUsers,getContentForUser,getGradeForUser,setGradeForUser){let{initialUserId:initialUserId=null,moduleName:moduleName,courseName:courseName,courseUrl:courseUrl,sendStudentNotifications:sendStudentNotifications,focusOnClose:focusOnClose=null}=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{};const userList=await getListOfUsers();if(!userList.length)return void(0,_notification.addNotification)({message:await(0,_str.getString)("nouserstograde","core_grades"),type:"error"});const[graderLayout,{html:html,js:js}]=await Promise.all([(0,_fullscreen.createLayout)({fullscreen:!1,showLoader:!1,focusOnClose:focusOnClose}),_templates.default.renderForPromise(templateNames_grader.app,{moduleName:moduleName,courseName:courseName,courseUrl:courseUrl,drawer:{show:!0},defaultsendnotifications:sendStudentNotifications})]),graderContainer=graderLayout.getContainer(),saveGradeFunction=getSaveUserGradeFunction(graderContainer,setGradeForUser);_templates.default.replaceNodeContents(graderContainer,html,js);const updateUserContent=getUpdateUserContentFunction(graderContainer,getContentForUser,getGradeForUser,saveGradeFunction),userIds=userList.map((user=>user.id)),statusContainer=graderContainer.querySelector(_selectors.default.regions.statusContainer),userPicker=await(0,_user_picker.default)(userList,(async user=>{const renderContext={status:(await updateUserContent(user)).hasgrade,index:userIds.indexOf(user.id)+1,total:userList.length};_templates.default.render(templateNames_grader.status,renderContext).then((html=>(statusContainer.innerHTML=html,html))).catch()}),saveGradeFunction,{initialUserId:initialUserId});registerEventListeners(graderLayout,userPicker,saveGradeFunction,userList),displayUserPicker(graderContainer,userPicker.rootNode)};_exports.view=async function(getGradeForUser,userid,moduleName){let{focusOnClose:focusOnClose=null}=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const userGrade=await getGradeForUser(userid),[modal,gradeTemplateData]=await Promise.all([_modal_cancel.default.create({title:moduleName,large:!0,removeOnClose:!0,returnElement:focusOnClose,show:!0,body:_templates.default.render("mod_forum/local/grades/view_grade",userGrade)}),renderGradeTemplate(userGrade)]),bodyPromise=await modal.getBodyPromise(),gradeReplace=bodyPromise[0].querySelector('[data-region="grade-template"]');_templates.default.replaceNodeContents(gradeReplace,gradeTemplateData.html,gradeTemplateData.js)};const renderGradeTemplate=userGrade=>_templates.default.renderForPromise(userGrade.templatename,userGrade.grade)})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),Object.defineProperty(_exports,"getGradingPanelFunctions",{enumerable:!0,get:function(){return _gradingpanel.default}}),_exports.view=_exports.launch=void 0,_templates=_interopRequireDefault(_templates),_selectors=_interopRequireDefault(_selectors),_user_picker=_interopRequireDefault(_user_picker),_gradingpanel=_interopRequireDefault(_gradingpanel),_modal_cancel=_interopRequireDefault(_modal_cancel),_drawer_events=_interopRequireDefault(_drawer_events);const templateNames_grader={app:"mod_forum/local/grades/grader",gradingPanel:{error:"mod_forum/local/grades/local/grader/gradingpanel/error"},searchResults:"mod_forum/local/grades/local/grader/user_picker/user_search",status:"mod_forum/local/grades/local/grader/status"},displayUserPicker=(root,html)=>{const pickerRegion=root.querySelector(_selectors.default.regions.pickerRegion);_templates.default.replaceNodeContents(pickerRegion,html,"")},fetchContentFromRender=(html,js)=>[html,js],getUpdateUserContentFunction=(root,getContentForUser,getGradeForUser,saveGradeForUser)=>{let firstLoad=!0;return async user=>{const spinner=firstLoad?null:(0,_loadingicon.addIconToContainerWithPromise)(root),[[html,js],userGrade]=await Promise.all([getContentForUser(user.id).then(fetchContentFromRender),getGradeForUser(user.id)]);_templates.default.replaceNodeContents(root.querySelector(_selectors.default.regions.moduleReplace),html,js);const[gradingPanelHtml,gradingPanelJS]=await _templates.default.render(userGrade.templatename,userGrade.grade).then(fetchContentFromRender),panelContainer=root.querySelector(_selectors.default.regions.gradingPanelContainer),panel=panelContainer.querySelector(_selectors.default.regions.gradingPanel);_templates.default.replaceNodeContents(panel,gradingPanelHtml,gradingPanelJS);const form=panel.querySelector("form");return(0,_comparison.fillInitialValues)(form),form.addEventListener("submit",(event=>{saveGradeForUser(user),event.preventDefault()})),panelContainer.scrollTop=0,firstLoad=!1,spinner&&spinner.resolve(),userGrade}},hideSearchResultContainer=(bodyContainer,userPickerContainer,searchResultsContainer)=>{bodyContainer.classList.remove("hidden"),userPickerContainer.classList.remove("hidden"),searchResultsContainer.classList.add("hidden")},hideUserSearchInput=(toggleSearchButton,searchContainer,searchInput)=>{searchContainer.classList.add("collapsed"),toggleSearchButton.setAttribute("aria-expanded","false"),toggleSearchButton.classList.add("collapse"),toggleSearchButton.classList.remove("expand"),toggleSearchButton.focus();searchContainer.parentElement.querySelector(_selectors.default.regions.gradingInfoContainer).removeAttribute("aria-hidden");const collapseGradingDrawer=searchContainer.parentElement.querySelector(_selectors.default.buttons.collapseGradingDrawer);collapseGradingDrawer.removeAttribute("aria-hidden"),collapseGradingDrawer.setAttribute("tabindex","0"),searchInput.value=""},renderSearchResults=async(searchResultsContainer,users)=>{const{html:html,js:js}=await _templates.default.renderForPromise(templateNames_grader.searchResults,{users:users});_templates.default.replaceNodeContents(searchResultsContainer,html,js)},registerEventListeners=(graderLayout,userPicker,saveGradeFunction,userList)=>{const graderContainer=graderLayout.getContainer(),toggleSearchButton=graderContainer.querySelector(_selectors.default.buttons.toggleSearch),searchInputContainer=graderContainer.querySelector(_selectors.default.regions.userSearchContainer),searchInput=searchInputContainer.querySelector(_selectors.default.regions.userSearchInput),bodyContainer=graderContainer.querySelector(_selectors.default.regions.bodyContainer),userPickerContainer=graderContainer.querySelector(_selectors.default.regions.pickerRegion),searchResultsContainer=graderContainer.querySelector(_selectors.default.regions.searchResultsContainer);graderContainer.addEventListener("click",(e=>{if(e.target.closest(_selectors.default.buttons.toggleFullscreen))return e.stopImmediatePropagation(),e.preventDefault(),void graderLayout.toggleFullscreen();if(e.target.closest(_selectors.default.buttons.closeGrader))return e.stopImmediatePropagation(),e.preventDefault(),graderLayout.close(),void window.location.reload();if(e.target.closest(_selectors.default.buttons.saveGrade)&&saveGradeFunction(userPicker.currentUser),e.target.closest(_selectors.default.buttons.toggleSearch))return void("true"===toggleSearchButton.getAttribute("aria-expanded")?(hideUserSearchInput(toggleSearchButton,searchInputContainer,searchInput),hideSearchResultContainer(bodyContainer,userPickerContainer,searchResultsContainer),searchResultsContainer.innerHTML=""):(((toggleSearchButton,searchContainer,searchInput)=>{searchContainer.classList.remove("collapsed"),toggleSearchButton.setAttribute("aria-expanded","true"),toggleSearchButton.classList.add("expand"),toggleSearchButton.classList.remove("collapse"),searchContainer.parentElement.querySelector(_selectors.default.regions.gradingInfoContainer).setAttribute("aria-hidden","true");const collapseGradingDrawer=searchContainer.parentElement.querySelector(_selectors.default.buttons.collapseGradingDrawer);collapseGradingDrawer.setAttribute("aria-hidden","true"),collapseGradingDrawer.setAttribute("tabindex","-1"),searchInput.focus()})(toggleSearchButton,searchInputContainer,searchInput),((bodyContainer,userPickerContainer,searchResultsContainer)=>{bodyContainer.classList.add("hidden"),userPickerContainer.classList.add("hidden"),searchResultsContainer.classList.remove("hidden")})(bodyContainer,userPickerContainer,searchResultsContainer),renderSearchResults(searchResultsContainer,userList)));const selectUserButton=e.target.closest(_selectors.default.buttons.selectUser);if(selectUserButton){const userId=selectUserButton.getAttribute("data-userid"),user=userList.find((user=>user.id==userId));userPicker.setUserId(userId),userPicker.showUser(user),hideUserSearchInput(toggleSearchButton,searchInputContainer,searchInput),hideSearchResultContainer(bodyContainer,userPickerContainer,searchResultsContainer),searchResultsContainer.innerHTML=""}})),searchInput.addEventListener("input",(0,_utils.debounce)((()=>{const users=((userList,searchTerm)=>""===searchTerm?userList:(searchTerm=searchTerm.toLowerCase(),userList.filter((user=>user.fullname.toLowerCase().includes(searchTerm)))))(userList,searchInput.value);renderSearchResults(searchResultsContainer,users)}),300)),(0,_pubsub.subscribe)(_drawer_events.default.DRAWER_HIDDEN,(drawerRoot=>{drawerRoot[0].querySelector(_selectors.default.regions.gradingPanel)&&setContentContainerMargin(graderContainer,0)})),(0,_pubsub.subscribe)(_drawer_events.default.DRAWER_SHOWN,(drawerRoot=>{const gradingPanel=drawerRoot[0];gradingPanel.querySelector(_selectors.default.regions.gradingPanel)&&setContentContainerMargin(graderContainer,gradingPanel.offsetWidth)}))},setContentContainerMargin=(graderContainer,rightMargin)=>{const contentContainer=graderContainer.querySelector(_selectors.default.regions.moduleContainer);contentContainer&&(contentContainer.style.marginRight="".concat(rightMargin,"px"))},getSaveUserGradeFunction=(root,setGradeForUser)=>async user=>{try{root.querySelector(_selectors.default.regions.gradingPanelErrors).innerHTML="";const result=await setGradeForUser(user.id,root.querySelector(_selectors.default.values.sendStudentNotifications).value,root.querySelector(_selectors.default.regions.gradingPanel));return result.success&&(0,_toast.add)(await(0,_str.getString)("grades:gradesavedfor","mod_forum",user)),result.failed&&displayGradingError(root,user,result.error),result}catch(err){return displayGradingError(root,user,err),(0,_normalise.failedUpdate)(err)}},displayGradingError=async(root,user,err)=>{const[{html:html,js:js},errorString]=await Promise.all([_templates.default.renderForPromise(templateNames_grader.gradingPanel.error,{error:err}),await(0,_str.getString)("grades:gradesavefailed","mod_forum",{error:err.message,...user})]);_templates.default.replaceNodeContents(root.querySelector(_selectors.default.regions.gradingPanelErrors),html,js),(0,_toast.add)(errorString,{type:"warning"})};_exports.launch=async function(getListOfUsers,getContentForUser,getGradeForUser,setGradeForUser){let{initialUserId:initialUserId=null,moduleName:moduleName,courseName:courseName,courseUrl:courseUrl,sendStudentNotifications:sendStudentNotifications,focusOnClose:focusOnClose=null}=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{};const userList=await getListOfUsers();if(!userList.length)return void(0,_notification.addNotification)({message:await(0,_str.getString)("nouserstograde","core_grades"),type:"error"});const[graderLayout,{html:html,js:js}]=await Promise.all([(0,_fullscreen.createLayout)({fullscreen:!1,showLoader:!1,focusOnClose:focusOnClose}),_templates.default.renderForPromise(templateNames_grader.app,{moduleName:moduleName,courseName:courseName,courseUrl:courseUrl,drawer:{show:!0},defaultsendnotifications:sendStudentNotifications})]),graderContainer=graderLayout.getContainer(),saveGradeFunction=getSaveUserGradeFunction(graderContainer,setGradeForUser);_templates.default.replaceNodeContents(graderContainer,html,js);const updateUserContent=getUpdateUserContentFunction(graderContainer,getContentForUser,getGradeForUser,saveGradeFunction),userIds=userList.map((user=>user.id)),statusContainer=graderContainer.querySelector(_selectors.default.regions.statusContainer),userPicker=await(0,_user_picker.default)(userList,(async user=>{const renderContext={status:(await updateUserContent(user)).hasgrade,index:userIds.indexOf(user.id)+1,total:userList.length};_templates.default.render(templateNames_grader.status,renderContext).then((html=>(statusContainer.innerHTML=html,html))).catch()}),saveGradeFunction,{initialUserId:initialUserId});registerEventListeners(graderLayout,userPicker,saveGradeFunction,userList),displayUserPicker(graderContainer,userPicker.rootNode)};_exports.view=async function(getGradeForUser,userid,moduleName){let{focusOnClose:focusOnClose=null}=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const userGrade=await getGradeForUser(userid),[modal,gradeTemplateData]=await Promise.all([_modal_cancel.default.create({title:moduleName,large:!0,removeOnClose:!0,returnElement:focusOnClose,show:!0,body:_templates.default.render("mod_forum/local/grades/view_grade",userGrade)}),renderGradeTemplate(userGrade)]),bodyPromise=await modal.getBodyPromise(),gradeReplace=bodyPromise[0].querySelector('[data-region="grade-template"]');_templates.default.replaceNodeContents(gradeReplace,gradeTemplateData.html,gradeTemplateData.js)};const renderGradeTemplate=userGrade=>_templates.default.renderForPromise(userGrade.templatename,userGrade.grade)})); //# sourceMappingURL=grader.min.js.map \ No newline at end of file diff --git a/public/mod/forum/amd/build/local/grades/grader.min.js.map b/public/mod/forum/amd/build/local/grades/grader.min.js.map index 8c63830a456..aff07c089b7 100644 --- a/public/mod/forum/amd/build/local/grades/grader.min.js.map +++ b/public/mod/forum/amd/build/local/grades/grader.min.js.map @@ -1 +1 @@ -{"version":3,"file":"grader.min.js","sources":["../../../src/local/grades/grader.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 * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/grader\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';\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 {addNotification} from 'core/notification';\nimport {getString} from 'core/str';\nimport {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise';\nimport {addIconToContainerWithPromise} from 'core/loadingicon';\nimport {debounce} from 'core/utils';\nimport {fillInitialValues} from 'core_grades/grades/grader/gradingpanel/comparison';\nimport Modal from 'core/modal_cancel';\nimport {subscribe} from 'core/pubsub';\nimport DrawerEvents from 'core/drawer_events';\n\nconst templateNames = {\n grader: {\n app: 'mod_forum/local/grades/grader',\n gradingPanel: {\n error: 'mod_forum/local/grades/local/grader/gradingpanel/error',\n },\n searchResults: 'mod_forum/local/grades/local/grader/user_picker/user_search',\n status: 'mod_forum/local/grades/local/grader/status',\n },\n};\n\n/**\n * Helper function that replaces the user picker placeholder with what we get back from the user picker class.\n *\n * @param {HTMLElement} root\n * @param {String} html\n */\nconst displayUserPicker = (root, html) => {\n const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);\n Templates.replaceNodeContents(pickerRegion, html, '');\n};\n\n/**\n * To be removed, this is now done as a part of Templates.renderForPromise()\n *\n * @param {String} html\n * @param {String} js\n * @returns {array} An array containing the HTML, and JS.\n */\nconst fetchContentFromRender = (html, js) => {\n return [html, js];\n};\n\n/**\n * Here we build the function that is passed to the user picker that'll handle updating the user content area\n * of the grading interface.\n *\n * @param {HTMLElement} root\n * @param {Function} getContentForUser\n * @param {Function} getGradeForUser\n * @param {Function} saveGradeForUser\n * @return {Function}\n */\nconst getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser, saveGradeForUser) => {\n let firstLoad = true;\n\n return async(user) => {\n const spinner = firstLoad ? null : addIconToContainerWithPromise(root);\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 const panelContainer = root.querySelector(Selectors.regions.gradingPanelContainer);\n const panel = panelContainer.querySelector(Selectors.regions.gradingPanel);\n Templates.replaceNodeContents(panel, gradingPanelHtml, gradingPanelJS);\n\n const form = panel.querySelector('form');\n fillInitialValues(form);\n\n form.addEventListener('submit', event => {\n saveGradeForUser(user);\n event.preventDefault();\n });\n\n panelContainer.scrollTop = 0;\n firstLoad = false;\n\n if (spinner) {\n spinner.resolve();\n }\n return userGrade;\n };\n};\n\n/**\n * Show the search results container and hide the user picker and body content.\n *\n * @param {HTMLElement} bodyContainer The container element for the body content\n * @param {HTMLElement} userPickerContainer The container element for the user picker\n * @param {HTMLElement} searchResultsContainer The container element for the search results\n */\nconst showSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {\n bodyContainer.classList.add('hidden');\n userPickerContainer.classList.add('hidden');\n searchResultsContainer.classList.remove('hidden');\n};\n\n/**\n * Hide the search results container and show the user picker and body content.\n *\n * @param {HTMLElement} bodyContainer The container element for the body content\n * @param {HTMLElement} userPickerContainer The container element for the user picker\n * @param {HTMLElement} searchResultsContainer The container element for the search results\n */\nconst hideSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {\n bodyContainer.classList.remove('hidden');\n userPickerContainer.classList.remove('hidden');\n searchResultsContainer.classList.add('hidden');\n};\n\n/**\n * Toggles the visibility of the user search.\n *\n * @param {HTMLElement} toggleSearchButton The button that toggles the search\n * @param {HTMLElement} searchContainer The container element for the user search\n * @param {HTMLElement} searchInput The input element for searching\n */\nconst showUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {\n searchContainer.classList.remove('collapsed');\n toggleSearchButton.setAttribute('aria-expanded', 'true');\n toggleSearchButton.classList.add('expand');\n toggleSearchButton.classList.remove('collapse');\n\n // Hide the grading info container from screen reader.\n const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);\n gradingInfoContainer.setAttribute('aria-hidden', 'true');\n\n // Hide the collapse grading drawer button from screen reader.\n const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);\n collapseGradingDrawer.setAttribute('aria-hidden', 'true');\n collapseGradingDrawer.setAttribute('tabindex', '-1');\n\n searchInput.focus();\n};\n\n/**\n * Toggles the visibility of the user search.\n *\n * @param {HTMLElement} toggleSearchButton The button that toggles the search\n * @param {HTMLElement} searchContainer The container element for the user search\n * @param {HTMLElement} searchInput The input element for searching\n */\nconst hideUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {\n searchContainer.classList.add('collapsed');\n toggleSearchButton.setAttribute('aria-expanded', 'false');\n toggleSearchButton.classList.add('collapse');\n toggleSearchButton.classList.remove('expand');\n toggleSearchButton.focus();\n\n // Show the grading info container to screen reader.\n const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);\n gradingInfoContainer.removeAttribute('aria-hidden');\n\n // Show the collapse grading drawer button from screen reader.\n const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);\n collapseGradingDrawer.removeAttribute('aria-hidden');\n collapseGradingDrawer.setAttribute('tabindex', '0');\n\n searchInput.value = '';\n};\n\n/**\n * Find the list of users who's names include the given search term.\n *\n * @param {Array} userList List of users for the grader\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\nconst searchForUsers = (userList, searchTerm) => {\n if (searchTerm === '') {\n return userList;\n }\n\n searchTerm = searchTerm.toLowerCase();\n\n return userList.filter((user) => {\n return user.fullname.toLowerCase().includes(searchTerm);\n });\n};\n\n/**\n * Render the list of users in the search results area.\n *\n * @param {HTMLElement} searchResultsContainer The container element for search results\n * @param {Array} users The list of users to display\n */\nconst renderSearchResults = async(searchResultsContainer, users) => {\n const {html, js} = await Templates.renderForPromise(templateNames.grader.searchResults, {users});\n Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * Add click handlers to the buttons in the header of the grading interface.\n *\n * @param {HTMLElement} graderLayout\n * @param {Object} userPicker\n * @param {Function} saveGradeFunction\n * @param {Array} userList List of users for the grader.\n */\nconst registerEventListeners = (graderLayout, userPicker, saveGradeFunction, userList) => {\n const graderContainer = graderLayout.getContainer();\n const toggleSearchButton = graderContainer.querySelector(Selectors.buttons.toggleSearch);\n const searchInputContainer = graderContainer.querySelector(Selectors.regions.userSearchContainer);\n const searchInput = searchInputContainer.querySelector(Selectors.regions.userSearchInput);\n const bodyContainer = graderContainer.querySelector(Selectors.regions.bodyContainer);\n const userPickerContainer = graderContainer.querySelector(Selectors.regions.pickerRegion);\n const searchResultsContainer = graderContainer.querySelector(Selectors.regions.searchResultsContainer);\n\n graderContainer.addEventListener('click', (e) => {\n if (e.target.closest(Selectors.buttons.toggleFullscreen)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n graderLayout.toggleFullscreen();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.closeGrader)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n\n graderLayout.close();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.saveGrade)) {\n saveGradeFunction(userPicker.currentUser);\n }\n\n if (e.target.closest(Selectors.buttons.toggleSearch)) {\n if (toggleSearchButton.getAttribute('aria-expanded') === 'true') {\n // Search is open so let's close it.\n hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n searchResultsContainer.innerHTML = '';\n } else {\n // Search is closed so let's open it.\n showUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n showSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n renderSearchResults(searchResultsContainer, userList);\n }\n\n return;\n }\n\n const selectUserButton = e.target.closest(Selectors.buttons.selectUser);\n if (selectUserButton) {\n const userId = selectUserButton.getAttribute('data-userid');\n const user = userList.find(user => user.id == userId);\n userPicker.setUserId(userId);\n userPicker.showUser(user);\n hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n searchResultsContainer.innerHTML = '';\n }\n });\n\n // Debounce the search input so that it only executes 300 milliseconds after the user has finished typing.\n searchInput.addEventListener('input', debounce(() => {\n const users = searchForUsers(userList, searchInput.value);\n renderSearchResults(searchResultsContainer, users);\n }, 300));\n\n // Remove the right margin of the content container when the grading panel is hidden so that it expands to full-width.\n subscribe(DrawerEvents.DRAWER_HIDDEN, (drawerRoot) => {\n const gradingPanel = drawerRoot[0];\n if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {\n setContentContainerMargin(graderContainer, 0);\n }\n });\n\n // Bring back the right margin of the content container when the grading panel is shown to give space for the grading panel.\n subscribe(DrawerEvents.DRAWER_SHOWN, (drawerRoot) => {\n const gradingPanel = drawerRoot[0];\n if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {\n setContentContainerMargin(graderContainer, gradingPanel.offsetWidth);\n }\n });\n};\n\n/**\n * Adjusts the right margin of the content container.\n *\n * @param {HTMLElement} graderContainer The container for the grader app.\n * @param {Number} rightMargin The right margin value.\n */\nconst setContentContainerMargin = (graderContainer, rightMargin) => {\n const contentContainer = graderContainer.querySelector(Selectors.regions.moduleContainer);\n if (contentContainer) {\n contentContainer.style.marginRight = `${rightMargin}px`;\n }\n};\n\n/**\n * Get the function used to save a user grade.\n *\n * @param {HTMLElement} root The container for the grader\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 root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = '';\n const result = await setGradeForUser(\n user.id,\n root.querySelector(Selectors.values.sendStudentNotifications).value,\n root.querySelector(Selectors.regions.gradingPanel)\n );\n if (result.success) {\n addToast(await getString('grades:gradesavedfor', 'mod_forum', user));\n }\n if (result.failed) {\n displayGradingError(root, user, result.error);\n }\n\n return result;\n } catch (err) {\n displayGradingError(root, user, err);\n\n return failedUpdate(err);\n }\n };\n};\n\n/**\n * Display a grading error, typically from a failed save.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Object} user The user who was errored\n * @param {Object} err The details of the error\n */\nconst displayGradingError = async(root, user, err) => {\n const [\n {html, js},\n errorString\n ] = await Promise.all([\n Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}),\n await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}),\n ]);\n\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js);\n addToast(errorString, {type: 'warning'});\n};\n\n/**\n * Launch the grader interface with the specified parameters.\n *\n * @param {Function} getListOfUsers A function to get the list of users\n * @param {Function} getContentForUser A function to get the content for a specific user\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Function} setGradeForUser A function to set the grade for a specific user\n * @param {Object} preferences Preferences for the launch function\n * @param {Number} preferences.initialUserId\n * @param {string} preferences.moduleName\n * @param {string} preferences.courseName\n * @param {string} preferences.courseUrl\n * @param {boolean} preferences.sendStudentNotifications\n * @param {null|HTMLElement} preferences.focusOnClose\n */\nexport const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {\n initialUserId = null,\n moduleName,\n courseName,\n courseUrl,\n sendStudentNotifications,\n focusOnClose = null,\n} = {}) => {\n\n // We need all of these functions to be executed in series, if one step runs before another the interface\n // will not work.\n\n // We need this promise to resolve separately so that we can avoid loading the whole interface if there are no users.\n const userList = await getListOfUsers();\n if (!userList.length) {\n addNotification({\n message: await getString('nouserstograde', 'core_grades'),\n type: \"error\",\n });\n return;\n }\n\n // Now that we have confirmed there are at least some users let's boot up the grader interface.\n const [\n graderLayout,\n {html, js},\n ] = await Promise.all([\n createFullScreenWindow({\n fullscreen: false,\n showLoader: false,\n focusOnClose,\n }),\n Templates.renderForPromise(templateNames.grader.app, {\n moduleName,\n courseName,\n courseUrl,\n drawer: {show: true},\n defaultsendnotifications: sendStudentNotifications,\n }),\n ]);\n\n const graderContainer = graderLayout.getContainer();\n\n const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);\n\n Templates.replaceNodeContents(graderContainer, html, js);\n const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser, saveGradeFunction);\n\n const userIds = userList.map(user => user.id);\n const statusContainer = graderContainer.querySelector(Selectors.regions.statusContainer);\n // Fetch the userpicker for display.\n const userPicker = await getUserPicker(\n userList,\n async(user) => {\n const userGrade = await updateUserContent(user);\n const renderContext = {\n status: userGrade.hasgrade,\n index: userIds.indexOf(user.id) + 1,\n total: userList.length\n };\n Templates.render(templateNames.grader.status, renderContext).then(html => {\n statusContainer.innerHTML = html;\n return html;\n }).catch();\n },\n saveGradeFunction,\n {\n initialUserId,\n },\n );\n\n // Register all event listeners.\n registerEventListeners(graderLayout, userPicker, saveGradeFunction, userList);\n\n // Display the newly created user picker.\n displayUserPicker(graderContainer, userPicker.rootNode);\n};\n\n/**\n * Show the grade for a specific user.\n *\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Number} userid The ID of a specific user\n * @param {String} moduleName the name of the module\n * @param {object} param\n * @param {null|HTMLElement} param.focusOnClose\n */\nexport const view = async(getGradeForUser, userid, moduleName, {\n focusOnClose = null,\n} = {}) => {\n\n const userGrade = await getGradeForUser(userid);\n\n const [\n modal,\n gradeTemplateData\n ] = await Promise.all([\n Modal.create({\n title: moduleName,\n large: true,\n removeOnClose: true,\n returnElement: focusOnClose,\n show: true,\n body: Templates.render('mod_forum/local/grades/view_grade', userGrade),\n }),\n renderGradeTemplate(userGrade)\n ]);\n\n const bodyPromise = await modal.getBodyPromise();\n const gradeReplace = bodyPromise[0].querySelector('[data-region=\"grade-template\"]');\n Templates.replaceNodeContents(gradeReplace, gradeTemplateData.html, gradeTemplateData.js);\n};\n\nconst renderGradeTemplate = (userGrade) => Templates.renderForPromise(userGrade.templatename, userGrade.grade);\n\nexport {getGradingPanelFunctions};\n"],"names":["templateNames","app","gradingPanel","error","searchResults","status","displayUserPicker","root","html","pickerRegion","querySelector","Selectors","regions","replaceNodeContents","fetchContentFromRender","js","getUpdateUserContentFunction","getContentForUser","getGradeForUser","saveGradeForUser","firstLoad","async","spinner","userGrade","Promise","all","user","id","then","moduleReplace","gradingPanelHtml","gradingPanelJS","Templates","render","templatename","grade","panelContainer","gradingPanelContainer","panel","form","addEventListener","event","preventDefault","scrollTop","resolve","hideSearchResultContainer","bodyContainer","userPickerContainer","searchResultsContainer","classList","remove","add","hideUserSearchInput","toggleSearchButton","searchContainer","searchInput","setAttribute","focus","parentElement","gradingInfoContainer","removeAttribute","collapseGradingDrawer","buttons","value","renderSearchResults","users","renderForPromise","registerEventListeners","graderLayout","userPicker","saveGradeFunction","userList","graderContainer","getContainer","toggleSearch","searchInputContainer","userSearchContainer","userSearchInput","e","target","closest","toggleFullscreen","stopImmediatePropagation","closeGrader","close","saveGrade","currentUser","getAttribute","innerHTML","showUserSearchInput","showSearchResultContainer","selectUserButton","selectUser","userId","find","setUserId","showUser","searchTerm","toLowerCase","filter","fullname","includes","searchForUsers","DrawerEvents","DRAWER_HIDDEN","drawerRoot","setContentContainerMargin","DRAWER_SHOWN","offsetWidth","rightMargin","contentContainer","moduleContainer","style","marginRight","getSaveUserGradeFunction","setGradeForUser","gradingPanelErrors","result","values","sendStudentNotifications","success","failed","displayGradingError","err","errorString","message","type","getListOfUsers","initialUserId","moduleName","courseName","courseUrl","focusOnClose","length","fullscreen","showLoader","drawer","show","defaultsendnotifications","updateUserContent","userIds","map","statusContainer","renderContext","hasgrade","index","indexOf","total","catch","rootNode","userid","modal","gradeTemplateData","Modal","create","title","large","removeOnClose","returnElement","body","renderGradeTemplate","bodyPromise","getBodyPromise","gradeReplace"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"grader.min.js","sources":["../../../src/local/grades/grader.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 * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/grader\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';\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 {addNotification} from 'core/notification';\nimport {getString} from 'core/str';\nimport {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise';\nimport {addIconToContainerWithPromise} from 'core/loadingicon';\nimport {debounce} from 'core/utils';\nimport {fillInitialValues} from 'core_grades/grades/grader/gradingpanel/comparison';\nimport Modal from 'core/modal_cancel';\nimport {subscribe} from 'core/pubsub';\nimport DrawerEvents from 'core/drawer_events';\n\nconst templateNames = {\n grader: {\n app: 'mod_forum/local/grades/grader',\n gradingPanel: {\n error: 'mod_forum/local/grades/local/grader/gradingpanel/error',\n },\n searchResults: 'mod_forum/local/grades/local/grader/user_picker/user_search',\n status: 'mod_forum/local/grades/local/grader/status',\n },\n};\n\n/**\n * Helper function that replaces the user picker placeholder with what we get back from the user picker class.\n *\n * @param {HTMLElement} root\n * @param {String} html\n */\nconst displayUserPicker = (root, html) => {\n const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);\n Templates.replaceNodeContents(pickerRegion, html, '');\n};\n\n/**\n * To be removed, this is now done as a part of Templates.renderForPromise()\n *\n * @param {String} html\n * @param {String} js\n * @returns {array} An array containing the HTML, and JS.\n */\nconst fetchContentFromRender = (html, js) => {\n return [html, js];\n};\n\n/**\n * Here we build the function that is passed to the user picker that'll handle updating the user content area\n * of the grading interface.\n *\n * @param {HTMLElement} root\n * @param {Function} getContentForUser\n * @param {Function} getGradeForUser\n * @param {Function} saveGradeForUser\n * @return {Function}\n */\nconst getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser, saveGradeForUser) => {\n let firstLoad = true;\n\n return async(user) => {\n const spinner = firstLoad ? null : addIconToContainerWithPromise(root);\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 const panelContainer = root.querySelector(Selectors.regions.gradingPanelContainer);\n const panel = panelContainer.querySelector(Selectors.regions.gradingPanel);\n Templates.replaceNodeContents(panel, gradingPanelHtml, gradingPanelJS);\n\n const form = panel.querySelector('form');\n fillInitialValues(form);\n\n form.addEventListener('submit', event => {\n saveGradeForUser(user);\n event.preventDefault();\n });\n\n panelContainer.scrollTop = 0;\n firstLoad = false;\n\n if (spinner) {\n spinner.resolve();\n }\n return userGrade;\n };\n};\n\n/**\n * Show the search results container and hide the user picker and body content.\n *\n * @param {HTMLElement} bodyContainer The container element for the body content\n * @param {HTMLElement} userPickerContainer The container element for the user picker\n * @param {HTMLElement} searchResultsContainer The container element for the search results\n */\nconst showSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {\n bodyContainer.classList.add('hidden');\n userPickerContainer.classList.add('hidden');\n searchResultsContainer.classList.remove('hidden');\n};\n\n/**\n * Hide the search results container and show the user picker and body content.\n *\n * @param {HTMLElement} bodyContainer The container element for the body content\n * @param {HTMLElement} userPickerContainer The container element for the user picker\n * @param {HTMLElement} searchResultsContainer The container element for the search results\n */\nconst hideSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {\n bodyContainer.classList.remove('hidden');\n userPickerContainer.classList.remove('hidden');\n searchResultsContainer.classList.add('hidden');\n};\n\n/**\n * Toggles the visibility of the user search.\n *\n * @param {HTMLElement} toggleSearchButton The button that toggles the search\n * @param {HTMLElement} searchContainer The container element for the user search\n * @param {HTMLElement} searchInput The input element for searching\n */\nconst showUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {\n searchContainer.classList.remove('collapsed');\n toggleSearchButton.setAttribute('aria-expanded', 'true');\n toggleSearchButton.classList.add('expand');\n toggleSearchButton.classList.remove('collapse');\n\n // Hide the grading info container from screen reader.\n const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);\n gradingInfoContainer.setAttribute('aria-hidden', 'true');\n\n // Hide the collapse grading drawer button from screen reader.\n const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);\n collapseGradingDrawer.setAttribute('aria-hidden', 'true');\n collapseGradingDrawer.setAttribute('tabindex', '-1');\n\n searchInput.focus();\n};\n\n/**\n * Toggles the visibility of the user search.\n *\n * @param {HTMLElement} toggleSearchButton The button that toggles the search\n * @param {HTMLElement} searchContainer The container element for the user search\n * @param {HTMLElement} searchInput The input element for searching\n */\nconst hideUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {\n searchContainer.classList.add('collapsed');\n toggleSearchButton.setAttribute('aria-expanded', 'false');\n toggleSearchButton.classList.add('collapse');\n toggleSearchButton.classList.remove('expand');\n toggleSearchButton.focus();\n\n // Show the grading info container to screen reader.\n const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);\n gradingInfoContainer.removeAttribute('aria-hidden');\n\n // Show the collapse grading drawer button from screen reader.\n const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);\n collapseGradingDrawer.removeAttribute('aria-hidden');\n collapseGradingDrawer.setAttribute('tabindex', '0');\n\n searchInput.value = '';\n};\n\n/**\n * Find the list of users who's names include the given search term.\n *\n * @param {Array} userList List of users for the grader\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\nconst searchForUsers = (userList, searchTerm) => {\n if (searchTerm === '') {\n return userList;\n }\n\n searchTerm = searchTerm.toLowerCase();\n\n return userList.filter((user) => {\n return user.fullname.toLowerCase().includes(searchTerm);\n });\n};\n\n/**\n * Render the list of users in the search results area.\n *\n * @param {HTMLElement} searchResultsContainer The container element for search results\n * @param {Array} users The list of users to display\n */\nconst renderSearchResults = async(searchResultsContainer, users) => {\n const {html, js} = await Templates.renderForPromise(templateNames.grader.searchResults, {users});\n Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * Add click handlers to the buttons in the header of the grading interface.\n *\n * @param {HTMLElement} graderLayout\n * @param {Object} userPicker\n * @param {Function} saveGradeFunction\n * @param {Array} userList List of users for the grader.\n */\nconst registerEventListeners = (graderLayout, userPicker, saveGradeFunction, userList) => {\n const graderContainer = graderLayout.getContainer();\n const toggleSearchButton = graderContainer.querySelector(Selectors.buttons.toggleSearch);\n const searchInputContainer = graderContainer.querySelector(Selectors.regions.userSearchContainer);\n const searchInput = searchInputContainer.querySelector(Selectors.regions.userSearchInput);\n const bodyContainer = graderContainer.querySelector(Selectors.regions.bodyContainer);\n const userPickerContainer = graderContainer.querySelector(Selectors.regions.pickerRegion);\n const searchResultsContainer = graderContainer.querySelector(Selectors.regions.searchResultsContainer);\n\n graderContainer.addEventListener('click', (e) => {\n if (e.target.closest(Selectors.buttons.toggleFullscreen)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n graderLayout.toggleFullscreen();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.closeGrader)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n\n graderLayout.close();\n // Keep the underlying forum page up-to-date by forcing a refresh.\n window.location.reload();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.saveGrade)) {\n saveGradeFunction(userPicker.currentUser);\n }\n\n if (e.target.closest(Selectors.buttons.toggleSearch)) {\n if (toggleSearchButton.getAttribute('aria-expanded') === 'true') {\n // Search is open so let's close it.\n hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n searchResultsContainer.innerHTML = '';\n } else {\n // Search is closed so let's open it.\n showUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n showSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n renderSearchResults(searchResultsContainer, userList);\n }\n\n return;\n }\n\n const selectUserButton = e.target.closest(Selectors.buttons.selectUser);\n if (selectUserButton) {\n const userId = selectUserButton.getAttribute('data-userid');\n const user = userList.find(user => user.id == userId);\n userPicker.setUserId(userId);\n userPicker.showUser(user);\n hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n searchResultsContainer.innerHTML = '';\n }\n });\n\n // Debounce the search input so that it only executes 300 milliseconds after the user has finished typing.\n searchInput.addEventListener('input', debounce(() => {\n const users = searchForUsers(userList, searchInput.value);\n renderSearchResults(searchResultsContainer, users);\n }, 300));\n\n // Remove the right margin of the content container when the grading panel is hidden so that it expands to full-width.\n subscribe(DrawerEvents.DRAWER_HIDDEN, (drawerRoot) => {\n const gradingPanel = drawerRoot[0];\n if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {\n setContentContainerMargin(graderContainer, 0);\n }\n });\n\n // Bring back the right margin of the content container when the grading panel is shown to give space for the grading panel.\n subscribe(DrawerEvents.DRAWER_SHOWN, (drawerRoot) => {\n const gradingPanel = drawerRoot[0];\n if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {\n setContentContainerMargin(graderContainer, gradingPanel.offsetWidth);\n }\n });\n};\n\n/**\n * Adjusts the right margin of the content container.\n *\n * @param {HTMLElement} graderContainer The container for the grader app.\n * @param {Number} rightMargin The right margin value.\n */\nconst setContentContainerMargin = (graderContainer, rightMargin) => {\n const contentContainer = graderContainer.querySelector(Selectors.regions.moduleContainer);\n if (contentContainer) {\n contentContainer.style.marginRight = `${rightMargin}px`;\n }\n};\n\n/**\n * Get the function used to save a user grade.\n *\n * @param {HTMLElement} root The container for the grader\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 root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = '';\n const result = await setGradeForUser(\n user.id,\n root.querySelector(Selectors.values.sendStudentNotifications).value,\n root.querySelector(Selectors.regions.gradingPanel)\n );\n if (result.success) {\n addToast(await getString('grades:gradesavedfor', 'mod_forum', user));\n }\n if (result.failed) {\n displayGradingError(root, user, result.error);\n }\n\n return result;\n } catch (err) {\n displayGradingError(root, user, err);\n\n return failedUpdate(err);\n }\n };\n};\n\n/**\n * Display a grading error, typically from a failed save.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Object} user The user who was errored\n * @param {Object} err The details of the error\n */\nconst displayGradingError = async(root, user, err) => {\n const [\n {html, js},\n errorString\n ] = await Promise.all([\n Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}),\n await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}),\n ]);\n\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js);\n addToast(errorString, {type: 'warning'});\n};\n\n/**\n * Launch the grader interface with the specified parameters.\n *\n * @param {Function} getListOfUsers A function to get the list of users\n * @param {Function} getContentForUser A function to get the content for a specific user\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Function} setGradeForUser A function to set the grade for a specific user\n * @param {Object} preferences Preferences for the launch function\n * @param {Number} preferences.initialUserId\n * @param {string} preferences.moduleName\n * @param {string} preferences.courseName\n * @param {string} preferences.courseUrl\n * @param {boolean} preferences.sendStudentNotifications\n * @param {null|HTMLElement} preferences.focusOnClose\n */\nexport const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {\n initialUserId = null,\n moduleName,\n courseName,\n courseUrl,\n sendStudentNotifications,\n focusOnClose = null,\n} = {}) => {\n\n // We need all of these functions to be executed in series, if one step runs before another the interface\n // will not work.\n\n // We need this promise to resolve separately so that we can avoid loading the whole interface if there are no users.\n const userList = await getListOfUsers();\n if (!userList.length) {\n addNotification({\n message: await getString('nouserstograde', 'core_grades'),\n type: \"error\",\n });\n return;\n }\n\n // Now that we have confirmed there are at least some users let's boot up the grader interface.\n const [\n graderLayout,\n {html, js},\n ] = await Promise.all([\n createFullScreenWindow({\n fullscreen: false,\n showLoader: false,\n focusOnClose,\n }),\n Templates.renderForPromise(templateNames.grader.app, {\n moduleName,\n courseName,\n courseUrl,\n drawer: {show: true},\n defaultsendnotifications: sendStudentNotifications,\n }),\n ]);\n\n const graderContainer = graderLayout.getContainer();\n\n const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);\n\n Templates.replaceNodeContents(graderContainer, html, js);\n const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser, saveGradeFunction);\n\n const userIds = userList.map(user => user.id);\n const statusContainer = graderContainer.querySelector(Selectors.regions.statusContainer);\n // Fetch the userpicker for display.\n const userPicker = await getUserPicker(\n userList,\n async(user) => {\n const userGrade = await updateUserContent(user);\n const renderContext = {\n status: userGrade.hasgrade,\n index: userIds.indexOf(user.id) + 1,\n total: userList.length\n };\n Templates.render(templateNames.grader.status, renderContext).then(html => {\n statusContainer.innerHTML = html;\n return html;\n }).catch();\n },\n saveGradeFunction,\n {\n initialUserId,\n },\n );\n\n // Register all event listeners.\n registerEventListeners(graderLayout, userPicker, saveGradeFunction, userList);\n\n // Display the newly created user picker.\n displayUserPicker(graderContainer, userPicker.rootNode);\n};\n\n/**\n * Show the grade for a specific user.\n *\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Number} userid The ID of a specific user\n * @param {String} moduleName the name of the module\n * @param {object} param\n * @param {null|HTMLElement} param.focusOnClose\n */\nexport const view = async(getGradeForUser, userid, moduleName, {\n focusOnClose = null,\n} = {}) => {\n\n const userGrade = await getGradeForUser(userid);\n\n const [\n modal,\n gradeTemplateData\n ] = await Promise.all([\n Modal.create({\n title: moduleName,\n large: true,\n removeOnClose: true,\n returnElement: focusOnClose,\n show: true,\n body: Templates.render('mod_forum/local/grades/view_grade', userGrade),\n }),\n renderGradeTemplate(userGrade)\n ]);\n\n const bodyPromise = await modal.getBodyPromise();\n const gradeReplace = bodyPromise[0].querySelector('[data-region=\"grade-template\"]');\n Templates.replaceNodeContents(gradeReplace, gradeTemplateData.html, gradeTemplateData.js);\n};\n\nconst renderGradeTemplate = (userGrade) => Templates.renderForPromise(userGrade.templatename, userGrade.grade);\n\nexport {getGradingPanelFunctions};\n"],"names":["templateNames","app","gradingPanel","error","searchResults","status","displayUserPicker","root","html","pickerRegion","querySelector","Selectors","regions","replaceNodeContents","fetchContentFromRender","js","getUpdateUserContentFunction","getContentForUser","getGradeForUser","saveGradeForUser","firstLoad","async","spinner","userGrade","Promise","all","user","id","then","moduleReplace","gradingPanelHtml","gradingPanelJS","Templates","render","templatename","grade","panelContainer","gradingPanelContainer","panel","form","addEventListener","event","preventDefault","scrollTop","resolve","hideSearchResultContainer","bodyContainer","userPickerContainer","searchResultsContainer","classList","remove","add","hideUserSearchInput","toggleSearchButton","searchContainer","searchInput","setAttribute","focus","parentElement","gradingInfoContainer","removeAttribute","collapseGradingDrawer","buttons","value","renderSearchResults","users","renderForPromise","registerEventListeners","graderLayout","userPicker","saveGradeFunction","userList","graderContainer","getContainer","toggleSearch","searchInputContainer","userSearchContainer","userSearchInput","e","target","closest","toggleFullscreen","stopImmediatePropagation","closeGrader","close","window","location","reload","saveGrade","currentUser","getAttribute","innerHTML","showUserSearchInput","showSearchResultContainer","selectUserButton","selectUser","userId","find","setUserId","showUser","searchTerm","toLowerCase","filter","fullname","includes","searchForUsers","DrawerEvents","DRAWER_HIDDEN","drawerRoot","setContentContainerMargin","DRAWER_SHOWN","offsetWidth","rightMargin","contentContainer","moduleContainer","style","marginRight","getSaveUserGradeFunction","setGradeForUser","gradingPanelErrors","result","values","sendStudentNotifications","success","failed","displayGradingError","err","errorString","message","type","getListOfUsers","initialUserId","moduleName","courseName","courseUrl","focusOnClose","length","fullscreen","showLoader","drawer","show","defaultsendnotifications","updateUserContent","userIds","map","statusContainer","renderContext","hasgrade","index","indexOf","total","catch","rootNode","userid","modal","gradeTemplateData","Modal","create","title","large","removeOnClose","returnElement","body","renderGradeTemplate","bodyPromise","getBodyPromise","gradeReplace"],"mappings":"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"} \ No newline at end of file diff --git a/public/mod/forum/amd/src/grades/expandconversation.js b/public/mod/forum/amd/src/grades/expandconversation.js index 33ad99b81cf..76dc2f4f966 100644 --- a/public/mod/forum/amd/src/grades/expandconversation.js +++ b/public/mod/forum/amd/src/grades/expandconversation.js @@ -26,6 +26,7 @@ import {exception as showException} from "core/notification"; import Templates from 'core/templates'; import Modal from 'core/modal_cancel'; import * as ModalEvents from 'core/modal_events'; +import Ajax from 'core/ajax'; /** * Find the Node containing the gradable details from the provided node by searching up the tree. @@ -124,4 +125,31 @@ export const registerEventListeners = (rootNode) => { } } }); + + // Each parent post can mark its own read state when it is clicked. + const parentContent = document.querySelectorAll(ForumSelectors.showContent); + parentContent.forEach(element => { + element.addEventListener('click', (e) => { + const target = e.currentTarget; + const postId = parseInt(target.dataset.parentPostid, 10); + const discussionId = parseInt(target.dataset.parentDiscussionid, 10); + markPostsRead([postId], discussionId); + }); + }); +}; + +/** + * Mark posts as read. + * + * @param {number[]} postIds The posts to mark as read + * @param {number} discussionId The discussion the posts belong to + */ +const markPostsRead = (postIds, discussionId) => { + Ajax.call([{ + methodname: 'mod_forum_mark_posts_read', + args: { + postids: postIds, + discussionid: discussionId + }, + }])[0].catch(showException.exception); }; diff --git a/public/mod/forum/amd/src/grades/grader/selectors.js b/public/mod/forum/amd/src/grades/grader/selectors.js index 5069299e8f9..c12c8e16d8f 100644 --- a/public/mod/forum/amd/src/grades/grader/selectors.js +++ b/public/mod/forum/amd/src/grades/grader/selectors.js @@ -29,4 +29,5 @@ export default { expandConversation: '[data-action="view-context"]', posts: '[data-region="posts"]', viewGrade: '[data-grade-action="view"]', + showContent: '.show-content-button', }; diff --git a/public/mod/forum/amd/src/local/grades/grader.js b/public/mod/forum/amd/src/local/grades/grader.js index a7948f2048a..b24c8728f05 100644 --- a/public/mod/forum/amd/src/local/grades/grader.js +++ b/public/mod/forum/amd/src/local/grades/grader.js @@ -257,6 +257,8 @@ const registerEventListeners = (graderLayout, userPicker, saveGradeFunction, use e.preventDefault(); graderLayout.close(); + // Keep the underlying forum page up-to-date by forcing a refresh. + window.location.reload(); return; } diff --git a/public/mod/forum/db/services.php b/public/mod/forum/db/services.php index 986cf7413ef..a4a016cbe81 100644 --- a/public/mod/forum/db/services.php +++ b/public/mod/forum/db/services.php @@ -206,7 +206,6 @@ $functions = array( 'ajax' => true, 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], - 'mod_forum_set_forum_tracking' => [ 'classname' => 'mod_forum\external\set_forum_tracking', 'methodname' => 'execute', @@ -215,4 +214,14 @@ $functions = array( 'ajax' => true, 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], + 'mod_forum_mark_posts_read' => [ + 'classname' => 'mod_forum_external', + 'methodname' => 'mark_posts_read', + 'classpath' => 'mod/forum/externallib.php', + 'description' => 'Mark forum posts as read.', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'mod/forum:viewdiscussion', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], ); diff --git a/public/mod/forum/externallib.php b/public/mod/forum/externallib.php index 26e632e7aac..d44fefc3d3d 100644 --- a/public/mod/forum/externallib.php +++ b/public/mod/forum/externallib.php @@ -191,7 +191,7 @@ class mod_forum_external extends external_api { * @return array */ public static function get_discussion_posts(int $discussionid, ?string $sortby, ?string $sortdirection, bool $includeinlineattachments = false) { - global $USER; + global $USER, $CFG; // Validate the parameter. $params = self::validate_parameters(self::get_discussion_posts_parameters(), [ 'discussionid' => $discussionid, @@ -247,8 +247,20 @@ class mod_forum_external extends external_api { $legacydatamapper = mod_forum\local\container::get_legacy_data_mapper_factory(); + // Export the posts for the template before we mark them as read. + // This allows the template to indicate which are the new/unread ones + // whilst marking them as read right here and now. + $exportedposts = $postbuilder->build($USER, [$forum], [$discussion], $posts, $includeinlineattachments); + + // Mark these posts as read. + if (!empty($posts)) { + if (!$CFG->forum_usermarksread && forum_tp_is_tracked($forum->get_id())) { + forum_tp_mark_posts_read($USER, array_keys($posts)); + } + } + return [ - 'posts' => $postbuilder->build($USER, [$forum], [$discussion], $posts, $includeinlineattachments), + 'posts' => $exportedposts, 'forumid' => $discussion->get_forum_id(), 'courseid' => $discussion->get_course_id(), 'ratinginfo' => \core_rating\external\util::get_rating_info( @@ -1720,7 +1732,7 @@ class mod_forum_external extends external_api { * @return array */ public static function get_discussion_posts_by_userid(int $userid, int $cmid, ?string $sortby, ?string $sortdirection) { - global $USER, $DB; + global $USER, $DB, $CFG; // Validate the parameter. $params = self::validate_parameters(self::get_discussion_posts_by_userid_parameters(), [ 'userid' => $userid, @@ -1817,6 +1829,13 @@ class mod_forum_external extends external_api { 'parentposts' => $parentposts, ], ]; + + // Mark these posts as read. + if (!empty($posts)) { + if (!$CFG->forum_usermarksread && forum_tp_is_tracked($forum->get_id())) { + forum_tp_mark_posts_read($USER, array_keys($posts)); + } + } } return [ @@ -2314,4 +2333,83 @@ class mod_forum_external extends external_api { ] ); } + + /** + * Returns description of mark_posts_read parameters. + * + * @return external_function_parameters + */ + public static function mark_posts_read_parameters(): external_function_parameters { + return new external_function_parameters([ + 'postids' => new external_multiple_structure( + new external_value(PARAM_INT, 'The post ID to mark as read'), + 'List of post IDs to mark as read', + ), + 'discussionid' => new external_value(PARAM_INT, 'The discussion the posts belong to'), + ]); + } + + /** + * Trigger the posts viewed event. + * + * This method differs from view_forum_discussion in that it is only + * interested in marking certain posts as viewed, and not an + * entire discussion. + * + * @param array $postids The post ids + * @param int $discussionid The discussion id + * @return bool True on success + */ + public static function mark_posts_read(array $postids, int $discussionid): bool { + global $CFG, $DB, $USER; + require_once($CFG->dirroot . "/mod/forum/lib.php"); + + $params = self::validate_parameters( + self::mark_posts_read_parameters(), + [ + 'postids' => $postids, + 'discussionid' => $discussionid, + ] + ); + + // Check permissions. + $discussion = $DB->get_record('forum_discussions', ['id' => $discussionid], '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('forum', $discussion->forum, $discussion->course, false, MUST_EXIST); + $modcontext = context_module::instance($cm->id); + self::validate_context($modcontext); + + require_capability('mod/forum:viewdiscussion', $modcontext, null, true, 'noviewdiscussionspermission', 'forum'); + + // Check the posts belong to the discussion. + [$insql, $params] = $DB->get_in_or_equal($postids, SQL_PARAMS_NAMED); + $params['discussionid'] = $discussionid; + $validposts = $DB->get_records_select( + 'forum_posts', + "id $insql AND discussion = :discussionid", + $params, + '', + 'id' + ); + + // Keep only valid posts. + $postids = array_intersect($postids, array_keys($validposts)); + if (empty($postids)) { + return false; + } + + // Mark as read if required. + $forum = $DB->get_record('forum', ['id' => $discussion->forum], '*', MUST_EXIST); + if (!$CFG->forum_usermarksread && forum_tp_is_tracked($forum)) { + return forum_tp_mark_posts_read($USER, $postids); + } + + return false; + } + + /** + * Describes the data returned from mark_posts_read. + */ + public static function mark_posts_read_returns(): external_value { + return new external_value(PARAM_BOOL, 'Returns true on success'); + } } diff --git a/public/mod/forum/templates/grades/grader/discussion/posts.mustache b/public/mod/forum/templates/grades/grader/discussion/posts.mustache index e2219f218e5..d6adf8a2235 100644 --- a/public/mod/forum/templates/grades/grader/discussion/posts.mustache +++ b/public/mod/forum/templates/grades/grader/discussion/posts.mustache @@ -53,6 +53,8 @@ aria-expanded="false" aria-controls="parent-post-content-{{id}}" data-bs-toggle="collapse" + data-parent-postid="{{id}}" + data-parent-discussionid="{{discussionid}}" > {{#str}} viewparentpost, mod_forum {{/str}} diff --git a/public/mod/forum/tests/behat/track_read_posts.feature b/public/mod/forum/tests/behat/track_read_posts.feature index dd438fa2034..064f7be4c8f 100644 --- a/public/mod/forum/tests/behat/track_read_posts.feature +++ b/public/mod/forum/tests/behat/track_read_posts.feature @@ -6,16 +6,18 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos Background: Given the following "users" exist: - | username | firstname | lastname | email | trackforums | - | student1 | Student | 1 | student1@example.com | 1 | - | student2 | Student | 2 | student2@example.com | 0 | + | username | firstname | lastname | email | trackforums | + | student1 | Student | 1 | student1@example.com | 1 | + | student2 | Student | 2 | student2@example.com | 0 | + | teacher1 | Teacher | 1 | teacher1@example.com | 1 | And the following "courses" exist: | fullname | shortname | category | - | Course 1 | C1 | 0 | + | Course 1 | C1 | 0 | And the following "course enrolments" exist: - | user | course | role | - | student1 | C1 | student | - | student2 | C1 | student | + | user | course | role | + | student1 | C1 | student | + | student2 | C1 | student | + | teacher1 | C1 | editingteacher | Scenario: Tracking forum posts off Given the following "activity" exists: @@ -185,3 +187,44 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos Then I should not see "1 unread post" And I follow "Test forum name" And I should not see "Track unread posts" + + @javascript + Scenario: Forum grader panel marks posts as read + Given the following "activity" exists: + | course | C1 | + | activity | forum | + | name | Test forum name | + | idnumber | forum | + | grade_forum | 100 | + | scale | 100 | + And the following "mod_forum > discussions" exist: + | user | forum | name | subject | message | + | student1 | forum | Discussion 1 | Discussion 1 | student1's topic | + | student2 | forum | Discussion 2 | Discussion 2 | student2's topic | + And the following "mod_forum > posts" exist: + | user | parentsubject | subject | message | + | student2 | Discussion 1 | Reply to discussion 1 | student2's reply | + | student1 | Discussion 2 | Reply to discussion 2 | student1's reply | + When I am on the "Course 1" course page logged in as teacher1 + # We have a discussion created by each student, and each has replied to the other. + Then I should see "4 unread posts" + And I am on the "Test forum name" "forum activity" page + And I press "Grade users" + And I press "Close grader" + And I am on the "Course 1" course page + # 4 posts minus the 2 viewed in the grader. + And I should see "2 unread posts" + And I am on the "Test forum name" "forum activity" page + And I press "Grade users" + # Let's look at a discussion in student1's grader that features a post from student2. + And I press "View discussion" + And I click on "Cancel" "button" in the "Discussion 1" "dialogue" + And I am on the "Course 1" course page + # Remaining 2 unread posts minus the 1 viewed in the discussion context. + And I should see "1 unread post" + And I am on the "Test forum name" "forum activity" page + And I press "Grade users" + # Let's look at the parent post that student1 replied to. + And I press "View parent post" + And I am on the "Course 1" course page + And I should not see "1 unread post" diff --git a/public/mod/forum/tests/externallib_test.php b/public/mod/forum/tests/externallib_test.php index 19962aade9a..90d0d7b856a 100644 --- a/public/mod/forum/tests/externallib_test.php +++ b/public/mod/forum/tests/externallib_test.php @@ -3224,4 +3224,54 @@ final class externallib_test extends \core_external\tests\externallib_testcase { 'messageformat' => FORMAT_MOODLE, ], $updatedpost); } + + /** + * Test marking individual posts as read. + */ + public function test_mark_posts_read(): void { + global $DB; + $this->resetAfterTest(true); + + // Setup test data. + $course = $this->getDataGenerator()->create_course(); + $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $user = $this->getDataGenerator()->create_user(['trackforums' => 1]); + $role = $DB->get_record('role', ['shortname' => 'student'], '*', MUST_EXIST); + self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id); + + // Add a discussion. + $record = new \stdClass(); + $record->course = $course->id; + $record->userid = $user->id; + $record->forum = $forum->id; + $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); + $parentpost = $DB->get_record('forum_posts', ['discussion' => $discussion->id]); + + // Generate some posts. + $record = new \stdClass(); + $record->course = $course->id; + $record->userid = $user->id; + $record->forum = $forum->id; + $record->discussion = $discussion->id; + $record->parent = $parentpost->id; + $post1 = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); + $post2 = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); + $post3 = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); + + // Track the forum. + $this->setUser($user); + forum_tp_start_tracking($forum->id, $user->id); + + // There should be 4 unread posts (1 parent + 3 child posts). + $unreadposts = forum_tp_get_course_unread_posts($user->id, $course->id); + $this->assertEquals(4, $unreadposts[$forum->id]->unread); + + // Marks some posts as read for this user. + $result = mod_forum_external::mark_posts_read([$post1->id, $post2->id], $discussion->id); + $this->assertTrue($result); + + // There should be 2 unread posts left (1 parent + 1 child post). + $unreadposts = forum_tp_get_course_unread_posts($user->id, $course->id); + $this->assertEquals(2, $unreadposts[$forum->id]->unread); + } } diff --git a/public/mod/forum/version.php b/public/mod/forum/version.php index ef0bb3e2fba..6df65f855e7 100644 --- a/public/mod/forum/version.php +++ b/public/mod/forum/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025100601; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2025100602; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2025092600; // Requires this Moodle version. $plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics)