diff --git a/amd/build/dashboard.min.js b/amd/build/dashboard.min.js
index 9b91061..61c1af0 100644
--- a/amd/build/dashboard.min.js
+++ b/amd/build/dashboard.min.js
@@ -1,3 +1,3 @@
-define("local_sitsgradepush/dashboard",["exports","./sitsgradepush_helper","core/notification"],(function(_exports,_sitsgradepush_helper,_notification){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};async function pushMarks(button){try{let assessmentmappingid=button.getAttribute("data-assessmentmappingid"),result=await(0,_sitsgradepush_helper.schedulePushTask)(assessmentmappingid);if(result.success){button.parentNode.parentNode.querySelector("span:last-child").innerHTML='',button.disabled=!0;let tooltipid=button.getAttribute("aria-describedby");null!==tooltipid&&null!==document.getElementById(tooltipid)&&document.getElementById(tooltipid).remove()}else{let errormessagerow=document.createElement("tr");errormessagerow.setAttribute("class","error-message-row"),errormessagerow.innerHTML='
'+result.message+" | ";let currentrow=button.closest("tr");null!==currentrow.nextElementSibling&¤trow.nextElementSibling.classList.contains("error-message-row")&¤trow.nextElementSibling.remove(),currentrow.insertAdjacentElement("afterend",errormessagerow)}return result.success}catch(error){return window.console.error(error),!1}}_exports.init=()=>{!function(){let successMessage=localStorage.getItem("successMessage");successMessage&&(_notification.default.addNotification({message:successMessage,type:"success"}),localStorage.removeItem("successMessage"))}();let page=document.getElementById("page"),tableSelector=document.getElementById("module-delivery-selector");tableSelector.addEventListener("change",(function(){let selectedTable=document.getElementById(tableSelector.value);if(selectedTable){let offset=-100,tablePosition=selectedTable.getBoundingClientRect().top,scrollPosition=page.scrollTop+tablePosition+offset;page.scrollTo({top:scrollPosition,behavior:"smooth"})}}));let backToTopButton=document.getElementById("backToTopButton");page.addEventListener("scroll",(function(){page.scrollTop>=100?backToTopButton.style.display="block":backToTopButton.style.display="none"})),backToTopButton.addEventListener("click",(function(){page.scrollTo({top:0,behavior:"smooth"}),tableSelector.selectedIndex=0}));let changesourcebuttons=document.querySelectorAll(".change-source-button:not([disabled])");changesourcebuttons.length>0&&changesourcebuttons.forEach((function(button){button.addEventListener("click",(function(){window.location.href=button.getAttribute("data-url")}))}));let mabpushbuttons=document.querySelectorAll(".push-mark-button:not([disabled])");mabpushbuttons.length>0&&mabpushbuttons.forEach((function(button){button.addEventListener("click",(function(){pushMarks(this)}))})),document.getElementById("push-all-button").addEventListener("click",(async function(){let mabpushbuttons=document.querySelectorAll(".push-mark-button:not([disabled])"),total=mabpushbuttons.length,count=0,promises=[];mabpushbuttons.forEach((function(button){let promise=pushMarks(button).then((function(result){return result&&(count+=1),result})).catch((function(error){window.console.error(error)}));promises.push(promise)})),await Promise.all(promises),page.scrollTo({top:0,behavior:"instant"}),_notification.default.addNotification({message:count+" of "+total+" push tasks have been scheduled.",type:count===total?"success":"warning"})}))}}));
+define("local_sitsgradepush/dashboard",["exports","./sitsgradepush_helper","./progress","core/notification","core/modal_factory","core/modal_events"],(function(_exports,_sitsgradepush_helper,_progress,_notification,_modal_factory,_modal_events){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_modal_factory=_interopRequireDefault(_modal_factory),_modal_events=_interopRequireDefault(_modal_events);let updatePageIntervalId=null,syncThreshold=30,globalCourseid=null,async=null;async function pushMarks(button){try{let assessmentmappingid=button.getAttribute("data-assessmentmappingid"),result=await(0,_sitsgradepush_helper.schedulePushTask)(assessmentmappingid);if(result.success){let tooltipid=button.getAttribute("aria-describedby");null!==tooltipid&&null!==document.getElementById(tooltipid)&&document.getElementById(tooltipid).remove()}else{let errormessagerow=document.createElement("tr");errormessagerow.setAttribute("class","error-message-row"),errormessagerow.innerHTML=''+result.message+" | ";let currentrow=button.closest("tr");null!==currentrow.nextElementSibling&¤trow.nextElementSibling.classList.contains("error-message-row")&¤trow.nextElementSibling.remove(),currentrow.insertAdjacentElement("afterend",errormessagerow)}return result}catch(error){return window.console.error(error),!1}}async function updateAssessments(courseid){let update=await(0,_sitsgradepush_helper.getAssessmentsUpdate)(courseid);if(update.success){let assessments=JSON.parse(update.assessments);assessments.length>0&&(function(assessments){let assessmentsHasTasks=assessments.filter((assessment=>null!==assessment.task)),assessmentIds=new Set(assessmentsHasTasks.map((item=>item.assessmentmappingid)));(function(assessmentsHasTasks,assessmentIds){document.querySelectorAll(".progress.async").forEach((progressBar=>{assessmentIds.has(progressBar.getAttribute("data-assessmentmappingid"))||progressBar.remove()})),assessmentsHasTasks.forEach((assessment=>{let progressBarId="progress-bar-"+assessment.task.assessmentmappingid,progressBar=document.getElementById(progressBarId),task=assessment.task;if(progressBar)(0,_progress.updateProgressBar)(progressBar,task.progress);else{progressBar=(0,_progress.createProgressBar)(progressBarId,"async",task.assessmentmappingid,task.progress),document.querySelector('.push-mark-button[data-assessmentmappingid="'+task.assessmentmappingid+'"]').parentNode.parentNode.insertAdjacentElement("afterend",progressBar)}}))})(assessmentsHasTasks,assessmentIds),function(assessmentIds){document.querySelectorAll(".push-mark-button").forEach((function(pushButton){let assessmentmappingid=pushButton.getAttribute("data-assessmentmappingid");if(assessmentIds.has(assessmentmappingid)){let spinner=(0,_progress.createSpinner)("text-light","spinner-border-sm");pushButton.innerHTML=spinner.outerHTML,pushButton.disabled=!0}else pushButton.innerHTML='',pushButton.disabled=!1}))}(assessmentIds)}(assessments),function(assessments){let icons=document.querySelectorAll(".records-icon"),assessmentsHasTransferRecords=assessments.filter((update=>1===update.transferrecords)),assessmentIds=new Set(assessmentsHasTransferRecords.map((assessment=>assessment.assessmentmappingid)));icons.forEach((function(icon){let assessmentmappingid=icon.getAttribute("data-assessmentmappingid");assessmentIds.has(assessmentmappingid)&&icon.classList.contains("fa-circle-info")&&(icon.classList.replace("fa-solid","fa-regular"),icon.classList.replace("fa-circle-info","fa-file-lines"))}))}(assessments))}else clearInterval(updatePageIntervalId),window.console.error(update.message)}_exports.init=(courseid,syncThresholdConfig,asyncConfig)=>{!function(){let successMessage=localStorage.getItem("successMessage");successMessage&&(_notification.default.addNotification({message:successMessage,type:"success"}),localStorage.removeItem("successMessage"))}(),syncThreshold=syncThresholdConfig,globalCourseid=courseid,async=asyncConfig;let page=document.getElementById("page"),tableSelector=function(page){let tableSelector=document.getElementById("module-delivery-selector");return tableSelector.addEventListener("change",(function(){let selectedTable=document.getElementById(tableSelector.value);if(selectedTable){let offset=-100,tablePosition=selectedTable.getBoundingClientRect().top,scrollPosition=page.scrollTop+tablePosition+offset;page.scrollTo({top:scrollPosition,behavior:"smooth"})}})),tableSelector}(page);!function(page,tableSelector){let backToTopButton=document.getElementById("backToTopButton");page.addEventListener("scroll",(function(){page.scrollTop>=100?backToTopButton.style.display="block":backToTopButton.style.display="none"})),backToTopButton.addEventListener("click",(function(){page.scrollTo({top:0,behavior:"smooth"}),tableSelector.selectedIndex=0}))}(page,tableSelector),function(){let changesourcebuttons=document.querySelectorAll(".change-source-button:not([disabled])");changesourcebuttons.length>0&&changesourcebuttons.forEach((function(button){button.addEventListener("click",(function(){window.location.href=button.getAttribute("data-url")}))}))}(),function(page,courseid){let mabpushbuttons=document.querySelectorAll(".push-mark-button:not([disabled])");mabpushbuttons.length>0&&mabpushbuttons.forEach((function(button){button.addEventListener("click",(async function(){let studentcount=button.getAttribute("data-numberofstudents"),assessmentmappingid=button.getAttribute("data-assessmentmappingid");if(studentcount&&studentcount0){let progressbar=(0,_progress.createProgressBar)("dashboard-progress-bar-sync","sync",assessmentmappingid,0,!0),modal=await _modal_factory.default.create({type:_modal_factory.default.types.ALERT,title:"Transferring Marks",body:''+progressbar.outerHTML,buttons:{cancel:"Cancel"}});await modal.show();let isModalVisible=!0,modalProgressbar=document.getElementById("dashboard-progress-bar-sync");modal.getRoot().on(_modal_events.default.hidden,(()=>{modal.destroy(),isModalVisible=!1}));let students=JSON.parse(result.students),studentcount=students.length,count=0,promises=[];for(const student of students){if(!isModalVisible)break;let promise=await(0,_sitsgradepush_helper.transferMarkForStudent)(assessmentmappingid,student.userid);if(promise.success)count+=1;else{document.getElementById("error-message-modal-sync").innerHTML=''+promise.message+"
",window.console.error(promise.message)}promises.push(promise);let progress=Math.round(count/studentcount*100);(0,_progress.updateProgressBar)(modalProgressbar,progress,!0)}await Promise.all(promises),await modal.setButtonText("cancel","Close")}updatePageIntervalId=setInterval((()=>{updateAssessments(globalCourseid)}),15e3)}(assessmentmappingid);else{(await pushMarks(this)).success&&updateAssessments(courseid)}}))}))}(0,courseid),function(page,courseid){document.getElementById("push-all-button").addEventListener("click",(async function(){let mabpushbuttons=document.querySelectorAll(".push-mark-button:not([disabled])[data-assessmentmappingid]"),total=mabpushbuttons.length,count=0,promises=[];mabpushbuttons.forEach((function(button){let promise=pushMarks(button).then((function(result){return result.success&&(count+=1),result})).catch((function(error){window.console.error(error)}));promises.push(promise)})),await Promise.all(promises),page.scrollTo({top:0,behavior:"instant"}),await _notification.default.addNotification({message:count+" of "+total+" push tasks have been scheduled.",type:count===total?"success":"warning"}),updateAssessments(courseid)}))}(page,courseid),updateAssessments(courseid),updatePageIntervalId=setInterval((()=>{updateAssessments(globalCourseid)}),15e3)}}));
//# sourceMappingURL=dashboard.min.js.map
\ No newline at end of file
diff --git a/amd/build/dashboard.min.js.map b/amd/build/dashboard.min.js.map
index 0396921..4b8ff87 100644
--- a/amd/build/dashboard.min.js.map
+++ b/amd/build/dashboard.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"dashboard.min.js","sources":["../src/dashboard.js"],"sourcesContent":["import {schedulePushTask} from \"./sitsgradepush_helper\";\nimport notification from \"core/notification\";\n\nexport const init = () => {\n // If there is a saved message by successfully mapped an assessment in localStorage, display it.\n displayNotification();\n\n // Find the page element.\n let page = document.getElementById(\"page\");\n\n // Find the module delivery table selector.\n let tableSelector = document.getElementById(\"module-delivery-selector\");\n\n // Jump to the selected module delivery table when the user selects a module delivery.\n tableSelector.addEventListener(\"change\", function() {\n // Find the selected table by ID.\n let selectedTable = document.getElementById(tableSelector.value);\n\n // Calculate the scroll position to be 100 pixels above the table.\n if (selectedTable) {\n let offset = -100;\n let tablePosition = selectedTable.getBoundingClientRect().top;\n let scrollPosition = page.scrollTop + tablePosition + offset;\n\n // Scroll to the calculated position.\n page.scrollTo({\n top: scrollPosition,\n behavior: \"smooth\"\n });\n }\n });\n\n // Find the back to top button.\n let backToTopButton = document.getElementById(\"backToTopButton\");\n\n // Show the button when the user scrolls down 100 pixels from the top of the page.\n page.addEventListener(\"scroll\", function() {\n if (page.scrollTop >= 100) {\n backToTopButton.style.display = \"block\";\n } else {\n backToTopButton.style.display = \"none\";\n }\n });\n\n // Scroll to the top of the page when the button is clicked.\n backToTopButton.addEventListener(\"click\", function() {\n page.scrollTo({top: 0, behavior: \"smooth\"});\n tableSelector.selectedIndex = 0;\n });\n\n // Get all change source buttons.\n let changesourcebuttons = document.querySelectorAll(\".change-source-button:not([disabled])\");\n\n // Add event listener to each change source button.\n // When the user clicks on each change source button, redirect to the select source page.\n if (changesourcebuttons.length > 0) {\n changesourcebuttons.forEach(function(button) {\n button.addEventListener(\"click\", function() {\n // Redirect to the change source page.\n window.location.href = button.getAttribute(\"data-url\");\n });\n });\n }\n\n // Get all the push buttons that are not disabled.\n let mabpushbuttons = document.querySelectorAll(\".push-mark-button:not([disabled])\");\n\n if (mabpushbuttons.length > 0) {\n // Push grades when the user clicks on each enabled push button.\n mabpushbuttons.forEach(function(button) {\n button.addEventListener(\"click\", function() {\n pushMarks(this);\n });\n });\n }\n\n // Get the push all button.\n let pushallbutton = document.getElementById(\"push-all-button\");\n\n // Push grades for all the not disabled push buttons when the user clicks on the push all button.\n pushallbutton.addEventListener(\"click\", async function() {\n // Get the updated not disabled push buttons.\n let mabpushbuttons = document.querySelectorAll(\".push-mark-button:not([disabled])\");\n\n // Number of not disabled push buttons.\n let total = mabpushbuttons.length;\n let count = 0;\n\n // Create an array to hold all the Promises.\n let promises = [];\n\n // Push grades to SITS for each component grade.\n mabpushbuttons.forEach(function(button) {\n // Create a Promise for each button and push it into the array.\n let promise = pushMarks(button)\n .then(function(result) {\n if (result) {\n count = count + 1;\n }\n return result;\n }).catch(function(error) {\n window.console.error(error);\n });\n\n promises.push(promise);\n });\n\n // Wait for all Promises to resolve.\n await Promise.all(promises);\n\n // Scroll to the top of the page so that the user can see the notification.\n page.scrollTo({top: 0, behavior: \"instant\"});\n\n // Show the notification.\n notification.addNotification({\n message: count + ' of ' + total + ' push tasks have been scheduled.',\n type: (count === total) ? 'success' : 'warning'\n });\n });\n};\n\n/**\n * Schedule a push task when the user clicks on a push button.\n *\n * @param {HTMLElement} button The button element.\n * @return {Promise} Promise.\n */\nasync function pushMarks(button) {\n try {\n // Get the assessment mapping ID from the button.\n let assessmentmappingid = button.getAttribute(\"data-assessmentmappingid\");\n\n // Schedule a push task.\n let result = await schedulePushTask(assessmentmappingid);\n\n // Check if the push task is successfully scheduled.\n if (result.success) {\n // Update the icon.\n let icon = button.parentNode.parentNode.querySelector(\"span:last-child\");\n icon.innerHTML = '';\n\n // Disable the push button after scheduled push task successfully.\n button.disabled = true;\n\n // Remove the tooltip (for Firefox and Safari).\n let tooltipid = button.getAttribute(\"aria-describedby\");\n if (tooltipid !== null && document.getElementById(tooltipid) !== null) {\n document.getElementById(tooltipid).remove();\n }\n } else {\n // Create an error message row.\n let errormessagerow = document.createElement(\"tr\");\n\n // Set the class and content of the error message row.\n errormessagerow.setAttribute(\"class\", \"error-message-row\");\n errormessagerow.innerHTML =\n '\">' +\n ' ' + result.message + ' ' +\n ' | ';\n\n // Find the closest row to the button.\n let currentrow = button.closest(\"tr\");\n\n // Remove the existing error message row if it exists.\n if (currentrow.nextElementSibling !== null &&\n currentrow.nextElementSibling.classList.contains(\"error-message-row\")) {\n currentrow.nextElementSibling.remove();\n }\n\n // Insert the error message row after the current row.\n currentrow.insertAdjacentElement(\"afterend\", errormessagerow);\n }\n\n return result.success;\n } catch (error) {\n window.console.error(error);\n return false;\n }\n}\n\n/**\n * Display a notification if a success message is available in localStorage.\n */\nfunction displayNotification() {\n // Retrieve the success message from localStorage.\n let successMessage = localStorage.getItem('successMessage');\n\n // Check if a success message is available.\n if (successMessage) {\n // Display the success message using a notification library or other means.\n notification.addNotification({\n message: successMessage,\n type: 'success'\n });\n\n // Remove the success message from localStorage to avoid showing it again.\n localStorage.removeItem('successMessage');\n }\n}\n"],"names":["pushMarks","button","assessmentmappingid","getAttribute","result","success","parentNode","querySelector","innerHTML","status","disabled","tooltipid","document","getElementById","remove","errormessagerow","createElement","setAttribute","message","currentrow","closest","nextElementSibling","classList","contains","insertAdjacentElement","error","window","console","successMessage","localStorage","getItem","addNotification","type","removeItem","displayNotification","page","tableSelector","addEventListener","selectedTable","value","offset","tablePosition","getBoundingClientRect","top","scrollPosition","scrollTop","scrollTo","behavior","backToTopButton","style","display","selectedIndex","changesourcebuttons","querySelectorAll","length","forEach","location","href","mabpushbuttons","this","async","total","count","promises","promise","then","catch","push","Promise","all"],"mappings":"gUA+HeA,UAAUC,gBAGbC,oBAAsBD,OAAOE,aAAa,4BAG1CC,aAAe,0CAAiBF,wBAGhCE,OAAOC,QAAS,CAELJ,OAAOK,WAAWA,WAAWC,cAAc,mBACjDC,UAAY,4FACDJ,OAAOK,OAAS,SAGhCR,OAAOS,UAAW,MAGdC,UAAYV,OAAOE,aAAa,oBAClB,OAAdQ,WAA6D,OAAvCC,SAASC,eAAeF,YAC9CC,SAASC,eAAeF,WAAWG,aAEpC,KAECC,gBAAkBH,SAASI,cAAc,MAG7CD,gBAAgBE,aAAa,QAAS,qBACtCF,gBAAgBP,UACZ,iEACkDJ,OAAOc,QADzD,kBAKAC,WAAalB,OAAOmB,QAAQ,MAGM,OAAlCD,WAAWE,oBACXF,WAAWE,mBAAmBC,UAAUC,SAAS,sBACjDJ,WAAWE,mBAAmBP,SAIlCK,WAAWK,sBAAsB,WAAYT,wBAG1CX,OAAOC,QAChB,MAAOoB,cACLC,OAAOC,QAAQF,MAAMA,QACd,iBA9KK,qBAuLZG,eAAiBC,aAAaC,QAAQ,kBAGtCF,uCAEaG,gBAAgB,CACzBb,QAASU,eACTI,KAAM,YAIVH,aAAaI,WAAW,mBAhM5BC,OAGIC,KAAOvB,SAASC,eAAe,QAG/BuB,cAAgBxB,SAASC,eAAe,4BAG5CuB,cAAcC,iBAAiB,UAAU,eAEjCC,cAAgB1B,SAASC,eAAeuB,cAAcG,UAGtDD,cAAe,KACXE,QAAU,IACVC,cAAgBH,cAAcI,wBAAwBC,IACtDC,eAAiBT,KAAKU,UAAYJ,cAAgBD,OAGtDL,KAAKW,SAAS,CACVH,IAAKC,eACLG,SAAU,mBAMlBC,gBAAkBpC,SAASC,eAAe,mBAG9CsB,KAAKE,iBAAiB,UAAU,WACxBF,KAAKU,WAAa,IAClBG,gBAAgBC,MAAMC,QAAU,QAEhCF,gBAAgBC,MAAMC,QAAU,UAKxCF,gBAAgBX,iBAAiB,SAAS,WACtCF,KAAKW,SAAS,CAACH,IAAK,EAAGI,SAAU,WACjCX,cAAce,cAAgB,SAI9BC,oBAAsBxC,SAASyC,iBAAiB,yCAIhDD,oBAAoBE,OAAS,GAC7BF,oBAAoBG,SAAQ,SAAStD,QACjCA,OAAOoC,iBAAiB,SAAS,WAE7BX,OAAO8B,SAASC,KAAOxD,OAAOE,aAAa,sBAMnDuD,eAAiB9C,SAASyC,iBAAiB,qCAE3CK,eAAeJ,OAAS,GAExBI,eAAeH,SAAQ,SAAStD,QAC5BA,OAAOoC,iBAAiB,SAAS,WAC7BrC,UAAU2D,YAMF/C,SAASC,eAAe,mBAG9BwB,iBAAiB,SAASuB,qBAEhCF,eAAiB9C,SAASyC,iBAAiB,qCAG3CQ,MAAQH,eAAeJ,OACvBQ,MAAQ,EAGRC,SAAW,GAGfL,eAAeH,SAAQ,SAAStD,YAExB+D,QAAUhE,UAAUC,QACnBgE,MAAK,SAAS7D,eACPA,SACA0D,OAAgB,GAEb1D,UACR8D,OAAM,SAASzC,OACdC,OAAOC,QAAQF,MAAMA,UAG7BsC,SAASI,KAAKH,kBAIZI,QAAQC,IAAIN,UAGlB5B,KAAKW,SAAS,CAACH,IAAK,EAAGI,SAAU,kCAGpBhB,gBAAgB,CACzBb,QAAS4C,MAAQ,OAASD,MAAQ,mCAClC7B,KAAO8B,QAAUD,MAAS,UAAY"}
\ No newline at end of file
+{"version":3,"file":"dashboard.min.js","sources":["../src/dashboard.js"],"sourcesContent":["import {\n schedulePushTask,\n getTransferStudents,\n transferMarkForStudent,\n getAssessmentsUpdate\n} from \"./sitsgradepush_helper\";\nimport {createProgressBar, updateProgressBar, createSpinner} from \"./progress\";\nimport notification from \"core/notification\";\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\n\nlet updatePageIntervalId = null; // The interval ID for updating the progress.\nlet syncThreshold = 30; // The threshold which determines whether it is a sync or async marks transfer.\nlet globalCourseid = null; // The global variable for course ID.\nlet updatePageDelay = 15000; // The delay for updating the page.\nlet async = null; // The async config.\n\n/**\n * Initialize the dashboard page.\n *\n * @param {int} courseid\n * @param {int} syncThresholdConfig\n * @param {int} asyncConfig\n */\nexport const init = (courseid, syncThresholdConfig, asyncConfig) => {\n // If there is a saved message by successfully mapped an assessment in localStorage, display it.\n displayNotification();\n\n // Set the sync threshold from the plugin config.\n syncThreshold = syncThresholdConfig;\n\n // Set the global variable course ID.\n globalCourseid = courseid;\n\n // Set the async config.\n async = asyncConfig;\n\n // Find the page element.\n let page = document.getElementById(\"page\");\n\n // Initialize the module delivery dropdown list.\n let tableSelector = initModuleDeliverySelector(page);\n\n // Initialize the back to top button.\n initBackToTopButton(page, tableSelector);\n\n // Initialize the change source buttons.\n initChangeSourceButtons();\n\n // Initialize the push buttons.\n initPushMarkButtons(page, courseid);\n\n // Initialize the push all button.\n initPushAllButton(page, courseid);\n\n // Update the dashboard page with the latest information.\n // E.g. progress bars, push buttons, records icons.\n updateAssessments(courseid);\n\n // Update the page every 15 seconds.\n updatePageIntervalId = setInterval(() => {\n updateAssessments(globalCourseid);\n }, updatePageDelay);\n\n};\n\n/**\n * Initialize the module delivery dropdown list.\n *\n * @param {HTMLElement} page\n */\nfunction initModuleDeliverySelector(page) {\n // Find the module delivery table selector.\n let tableSelector = document.getElementById(\"module-delivery-selector\");\n\n // Jump to the selected module delivery table when the user selects a module delivery.\n tableSelector.addEventListener(\"change\", function() {\n // Find the selected table by ID.\n let selectedTable = document.getElementById(tableSelector.value);\n\n // Calculate the scroll position to be 100 pixels above the table.\n if (selectedTable) {\n let offset = -100;\n let tablePosition = selectedTable.getBoundingClientRect().top;\n let scrollPosition = page.scrollTop + tablePosition + offset;\n\n // Scroll to the calculated position.\n page.scrollTo({\n top: scrollPosition,\n behavior: \"smooth\"\n });\n }\n });\n\n return tableSelector;\n}\n\n/**\n * Initialize the back to top button.\n *\n * @param {HTMLElement} page\n * @param {HTMLElement} tableSelector\n */\nfunction initBackToTopButton(page, tableSelector) {\n // Find the back to top button.\n let backToTopButton = document.getElementById(\"backToTopButton\");\n\n // Show the button when the user scrolls down 100 pixels from the top of the page.\n page.addEventListener(\"scroll\", function() {\n if (page.scrollTop >= 100) {\n backToTopButton.style.display = \"block\";\n } else {\n backToTopButton.style.display = \"none\";\n }\n });\n\n // Scroll to the top of the page when the button is clicked.\n backToTopButton.addEventListener(\"click\", function() {\n page.scrollTo({top: 0, behavior: \"smooth\"});\n tableSelector.selectedIndex = 0;\n });\n}\n\n/**\n * Initialize the change source buttons.\n *\n */\nfunction initChangeSourceButtons() {\n // Get all change source buttons.\n let changesourcebuttons = document.querySelectorAll(\".change-source-button:not([disabled])\");\n\n // Add event listener to each change source button.\n // When the user clicks on each change source button, redirect to the select source page.\n if (changesourcebuttons.length > 0) {\n changesourcebuttons.forEach(function(button) {\n button.addEventListener(\"click\", function() {\n // Redirect to the change source page.\n window.location.href = button.getAttribute(\"data-url\");\n });\n });\n }\n}\n\n/**\n * Initialize the push mark buttons.\n *\n * @param {HTMLElement} page\n * @param {int} courseid\n */\nfunction initPushMarkButtons(page, courseid) {\n // Get all the push buttons that are not disabled.\n let mabpushbuttons = document.querySelectorAll(\".push-mark-button:not([disabled])\");\n\n if (mabpushbuttons.length > 0) {\n // Push grades when the user clicks on each enabled push button.\n mabpushbuttons.forEach(function(button) {\n button.addEventListener(\"click\", async function() {\n // Find the number of students to push grades.\n let studentcount = button.getAttribute(\"data-numberofstudents\");\n let assessmentmappingid = button.getAttribute(\"data-assessmentmappingid\");\n\n // Do synchronous marks transfer if the number of marks to be transferred is less than the sync threshold.\n // Or if the async config is disabled.\n if ((studentcount && studentcount < syncThreshold) || async === '0') {\n await syncMarksTransfer(assessmentmappingid);\n } else {\n // Schedule an asynchronous marks transfer task.\n let result = await pushMarks(this);\n if (result.success) {\n // Update the page after scheduling a marks transfer task.\n updateAssessments(courseid);\n }\n }\n });\n });\n }\n}\n\n/**\n * Initialize the push all button.\n *\n * @param {HTMLElement} page\n * @param {int} courseid\n */\nfunction initPushAllButton(page, courseid) {\n // Get the push all button.\n let pushallbutton = document.getElementById(\"push-all-button\");\n\n // Push grades for all the not disabled push buttons when the user clicks on the push all button.\n pushallbutton.addEventListener(\"click\", async function() {\n // Get the updated not disabled push buttons and has assessment ID.\n let mabpushbuttons = document.querySelectorAll(\".push-mark-button:not([disabled])[data-assessmentmappingid]\");\n\n // Number of not disabled push buttons.\n let total = mabpushbuttons.length;\n let count = 0;\n\n // Create an array to hold all the Promises.\n let promises = [];\n\n // Push grades to SITS for each component grade.\n mabpushbuttons.forEach(function(button) {\n // Create a Promise for each button and push it into the array.\n let promise = pushMarks(button)\n .then(function(result) {\n if (result.success) {\n count = count + 1;\n }\n return result;\n }).catch(function(error) {\n window.console.error(error);\n });\n\n promises.push(promise);\n });\n\n // Wait for all Promises to resolve.\n await Promise.all(promises);\n\n // Scroll to the top of the page so that the user can see the notification.\n page.scrollTo({top: 0, behavior: \"instant\"});\n\n // Show the notification.\n await notification.addNotification({\n message: count + ' of ' + total + ' push tasks have been scheduled.',\n type: (count === total) ? 'success' : 'warning'\n });\n\n // Update the page information.\n updateAssessments(courseid);\n });\n}\n\n/**\n * Schedule a push task when the user clicks on a push button.\n *\n * @param {HTMLElement} button The button element.\n * @return {Promise} Promise.\n */\nasync function pushMarks(button) {\n try {\n // Get the assessment mapping ID from the button.\n let assessmentmappingid = button.getAttribute(\"data-assessmentmappingid\");\n\n // Schedule a push task.\n let result = await schedulePushTask(assessmentmappingid);\n\n // Check if the push task is successfully scheduled.\n if (result.success) {\n // Remove the tooltip (for Firefox and Safari).\n let tooltipid = button.getAttribute(\"aria-describedby\");\n if (tooltipid !== null && document.getElementById(tooltipid) !== null) {\n document.getElementById(tooltipid).remove();\n }\n } else {\n // Create an error message row.\n let errormessagerow = document.createElement(\"tr\");\n\n // Set the class and content of the error message row.\n errormessagerow.setAttribute(\"class\", \"error-message-row\");\n errormessagerow.innerHTML =\n '\">' +\n ' ' + result.message + ' ' +\n ' | ';\n\n // Find the closest row to the button.\n let currentrow = button.closest(\"tr\");\n\n // Remove the existing error message row if it exists.\n if (currentrow.nextElementSibling !== null &&\n currentrow.nextElementSibling.classList.contains(\"error-message-row\")) {\n currentrow.nextElementSibling.remove();\n }\n\n // Insert the error message row after the current row.\n currentrow.insertAdjacentElement(\"afterend\", errormessagerow);\n }\n\n return result;\n } catch (error) {\n window.console.error(error);\n return false;\n }\n}\n\n/**\n * Update the dashboard page with the latest information.\n * e.g. progress bars, push buttons, records icons.\n *\n * @param {int} courseid\n * @return void\n */\nasync function updateAssessments(courseid) {\n // Get latest assessments information for the dashboard page.\n let update = await getAssessmentsUpdate(courseid);\n\n if (update.success) {\n // Parse the JSON string.\n let assessments = JSON.parse(update.assessments);\n\n if (assessments.length > 0) {\n // Update the all the progress bars and push buttons.\n updateTasksProgresses(assessments);\n\n // Update the records icons.\n updateIcon(assessments);\n }\n } else {\n // Stop update the page if error occurred.\n clearInterval(updatePageIntervalId);\n window.console.error(update.message);\n }\n}\n\n/**\n * Update all the progress bars and push buttons in the dashboard page.\n *\n * @param {object[]} assessments\n */\nfunction updateTasksProgresses(assessments) {\n // Filter assessments that are having task in progress.\n let assessmentsHasTasks = assessments.filter(assessment => assessment.task !== null);\n\n // The assessment mapping IDs having task in progress.\n let assessmentIds = new Set(assessmentsHasTasks.map(item => item.assessmentmappingid));\n\n // Update the progress bars.\n updateProgressBars(assessmentsHasTasks, assessmentIds);\n\n // Update the push buttons.\n updatePushButtons(assessmentIds);\n}\n\n/**\n * Update all the progress bars in the dashboard page.\n *\n * @param {object[]} assessmentsHasTasks\n * @param {Set} assessmentIds\n */\nfunction updateProgressBars(assessmentsHasTasks, assessmentIds) {\n let progressBars = document.querySelectorAll('.progress.async');\n\n // Remove the progress bars that are not in the assessmentIds.\n progressBars.forEach(progressBar => {\n if (!assessmentIds.has(progressBar.getAttribute('data-assessmentmappingid'))) {\n progressBar.remove();\n }\n });\n\n assessmentsHasTasks.forEach(assessment => {\n let progressBarId = 'progress-bar-' + assessment.task.assessmentmappingid;\n let progressBar = document.getElementById(progressBarId);\n let task = assessment.task;\n\n // If the progress bar not exists, create a new one, otherwise update the progress.\n if (!progressBar) {\n progressBar = createProgressBar(progressBarId, 'async', task.assessmentmappingid, task.progress);\n let button = document.querySelector('.push-mark-button[data-assessmentmappingid=\"' + task.assessmentmappingid + '\"]');\n button.parentNode.parentNode.insertAdjacentElement('afterend', progressBar);\n } else {\n updateProgressBar(progressBar, task.progress);\n }\n });\n}\n\n/**\n * Update all the push buttons in the dashboard page.\n *\n * @param {Set} assessmentIds\n */\nfunction updatePushButtons(assessmentIds) {\n // Find all push buttons.\n let pushButtons = document.querySelectorAll('.push-mark-button');\n\n pushButtons.forEach(function(pushButton) {\n let assessmentmappingid = pushButton.getAttribute('data-assessmentmappingid');\n\n if (assessmentIds.has(assessmentmappingid)) {\n // If the task ID is found, show spinner and disable button.\n let spinner = createSpinner('text-light', 'spinner-border-sm');\n pushButton.innerHTML = spinner.outerHTML;\n pushButton.disabled = true;\n } else {\n // Reset to original state if not in taskIds.\n pushButton.innerHTML = '';\n pushButton.disabled = false;\n }\n });\n}\n\n/**\n * Update the icons to show that there are transfer records.\n *\n * @param {object[]} assessments\n */\nfunction updateIcon(assessments) {\n let icons = document.querySelectorAll('.records-icon');\n\n // Get assessment mappings that have transfer records.\n let assessmentsHasTransferRecords = assessments.filter(update => update.transferrecords === 1);\n\n // Extract the assessment mapping IDs.\n let assessmentIds = new Set(assessmentsHasTransferRecords.map(assessment => assessment.assessmentmappingid));\n\n // Update the icons to show that there are transfer records.\n icons.forEach(function(icon) {\n let assessmentmappingid = icon.getAttribute('data-assessmentmappingid');\n if (assessmentIds.has(assessmentmappingid)) {\n if (icon.classList.contains('fa-circle-info')) {\n icon.classList.replace('fa-solid', 'fa-regular');\n icon.classList.replace('fa-circle-info', 'fa-file-lines');\n }\n }\n });\n}\n\n/**\n * Transfer marks for all the students in the assessment mapping synchronously.\n *\n * @param {int} assessmentmappingid\n * @return {Promise}\n */\nasync function syncMarksTransfer(assessmentmappingid) {\n // Stop the page update while transferring marks.\n clearInterval(updatePageIntervalId);\n\n // Get the students to transfer marks.\n let result = await getTransferStudents(assessmentmappingid);\n\n if (result.success) {\n if (result.students.length > 0) {\n let progressbar =\n createProgressBar('dashboard-progress-bar-sync', 'sync', assessmentmappingid, 0, true);\n\n // Create a modal to show the progress.\n let modal = await ModalFactory.create({\n type: ModalFactory.types.ALERT,\n title: 'Transferring Marks',\n body: '' + progressbar.outerHTML,\n buttons: {'cancel': 'Cancel'}\n });\n\n await modal.show();\n let isModalVisible = true;\n let modalProgressbar = document.getElementById('dashboard-progress-bar-sync');\n\n // Destroy the modal when it is hidden.\n modal.getRoot().on(ModalEvents.hidden, () => {\n modal.destroy();\n isModalVisible = false;\n });\n\n let students = JSON.parse(result.students);\n\n let studentcount = students.length;\n let count = 0;\n let promises = [];\n for (const student of students) {\n // Stop the progress if the modal is closed.\n if (!isModalVisible) {\n break;\n }\n\n // Transfer mark for each student.\n let promise = await transferMarkForStudent(assessmentmappingid, student.userid);\n if (promise.success) {\n count = count + 1;\n } else {\n let errormessage = document.getElementById('error-message-modal-sync');\n errormessage.innerHTML = '' + promise.message + '
';\n window.console.error(promise.message);\n }\n\n promises.push(promise);\n\n // Calculate the progress.\n let progress = Math.round((count / studentcount) * 100);\n updateProgressBar(modalProgressbar, progress, true);\n }\n await Promise.all(promises);\n await modal.setButtonText('cancel', 'Close');\n }\n }\n\n // Resume the page update.\n updatePageIntervalId = setInterval(() => {\n updateAssessments(globalCourseid);\n }, updatePageDelay);\n}\n\n/**\n * Display a notification if a success message is available in localStorage.\n */\nfunction displayNotification() {\n // Retrieve the success message from localStorage.\n let successMessage = localStorage.getItem('successMessage');\n\n // Check if a success message is available.\n if (successMessage) {\n // Display the success message using a notification library or other means.\n notification.addNotification({\n message: successMessage,\n type: 'success'\n });\n\n // Remove the success message from localStorage to avoid showing it again.\n localStorage.removeItem('successMessage');\n }\n}\n"],"names":["updatePageIntervalId","syncThreshold","globalCourseid","async","pushMarks","button","assessmentmappingid","getAttribute","result","success","tooltipid","document","getElementById","remove","errormessagerow","createElement","setAttribute","innerHTML","message","currentrow","closest","nextElementSibling","classList","contains","insertAdjacentElement","error","window","console","updateAssessments","courseid","update","assessments","JSON","parse","length","assessmentsHasTasks","filter","assessment","task","assessmentIds","Set","map","item","querySelectorAll","forEach","progressBar","has","progressBarId","progress","querySelector","parentNode","updateProgressBars","pushButton","spinner","outerHTML","disabled","updatePushButtons","updateTasksProgresses","icons","assessmentsHasTransferRecords","transferrecords","icon","replace","updateIcon","clearInterval","syncThresholdConfig","asyncConfig","successMessage","localStorage","getItem","addNotification","type","removeItem","displayNotification","page","tableSelector","addEventListener","selectedTable","value","offset","tablePosition","getBoundingClientRect","top","scrollPosition","scrollTop","scrollTo","behavior","initModuleDeliverySelector","backToTopButton","style","display","selectedIndex","initBackToTopButton","changesourcebuttons","location","href","initChangeSourceButtons","mabpushbuttons","studentcount","students","progressbar","modal","ModalFactory","create","types","ALERT","title","body","buttons","show","isModalVisible","modalProgressbar","getRoot","on","ModalEvents","hidden","destroy","count","promises","student","promise","userid","push","Math","round","Promise","all","setButtonText","setInterval","syncMarksTransfer","this","initPushMarkButtons","total","then","catch","notification","initPushAllButton"],"mappings":"sjBAWIA,qBAAuB,KACvBC,cAAgB,GAChBC,eAAiB,KAEjBC,MAAQ,oBAgOGC,UAAUC,gBAGbC,oBAAsBD,OAAOE,aAAa,4BAG1CC,aAAe,0CAAiBF,wBAGhCE,OAAOC,QAAS,KAEZC,UAAYL,OAAOE,aAAa,oBAClB,OAAdG,WAA6D,OAAvCC,SAASC,eAAeF,YAC9CC,SAASC,eAAeF,WAAWG,aAEpC,KAECC,gBAAkBH,SAASI,cAAc,MAG7CD,gBAAgBE,aAAa,QAAS,qBACtCF,gBAAgBG,UACZ,iEACkDT,OAAOU,QADzD,kBAKAC,WAAad,OAAOe,QAAQ,MAGM,OAAlCD,WAAWE,oBACXF,WAAWE,mBAAmBC,UAAUC,SAAS,sBACjDJ,WAAWE,mBAAmBR,SAIlCM,WAAWK,sBAAsB,WAAYV,wBAG1CN,OACT,MAAOiB,cACLC,OAAOC,QAAQF,MAAMA,QACd,kBAWAG,kBAAkBC,cAEzBC,aAAe,8CAAqBD,aAEpCC,OAAOrB,QAAS,KAEZsB,YAAcC,KAAKC,MAAMH,OAAOC,aAEhCA,YAAYG,OAAS,aAmBFH,iBAEvBI,oBAAsBJ,YAAYK,QAAOC,YAAkC,OAApBA,WAAWC,OAGlEC,cAAgB,IAAIC,IAAIL,oBAAoBM,KAAIC,MAAQA,KAAKpC,iCAezC6B,oBAAqBI,eAC1B5B,SAASgC,iBAAiB,mBAGhCC,SAAQC,cACZN,cAAcO,IAAID,YAAYtC,aAAa,8BAC5CsC,YAAYhC,YAIpBsB,oBAAoBS,SAAQP,iBACpBU,cAAgB,gBAAkBV,WAAWC,KAAKhC,oBAClDuC,YAAclC,SAASC,eAAemC,eACtCT,KAAOD,WAAWC,QAGjBO,4CAKiBA,YAAaP,KAAKU,cALtB,CACdH,aAAc,+BAAkBE,cAAe,QAAST,KAAKhC,oBAAqBgC,KAAKU,UAC1ErC,SAASsC,cAAc,+CAAiDX,KAAKhC,oBAAsB,MACzG4C,WAAWA,WAAW1B,sBAAsB,WAAYqB,kBA/BvEM,CAAmBhB,oBAAqBI,wBA2CjBA,eAEL5B,SAASgC,iBAAiB,qBAEhCC,SAAQ,SAASQ,gBACrB9C,oBAAsB8C,WAAW7C,aAAa,+BAE9CgC,cAAcO,IAAIxC,qBAAsB,KAEpC+C,SAAU,2BAAc,aAAc,qBAC1CD,WAAWnC,UAAYoC,QAAQC,UAC/BF,WAAWG,UAAW,OAGtBH,WAAWnC,UAAY,qCACvBmC,WAAWG,UAAW,KAvD9BC,CAAkBjB,eA5BVkB,CAAsB1B,sBA6FdA,iBACZ2B,MAAQ/C,SAASgC,iBAAiB,iBAGlCgB,8BAAgC5B,YAAYK,QAAON,QAAqC,IAA3BA,OAAO8B,kBAGpErB,cAAgB,IAAIC,IAAImB,8BAA8BlB,KAAIJ,YAAcA,WAAW/B,uBAGvFoD,MAAMd,SAAQ,SAASiB,UACfvD,oBAAsBuD,KAAKtD,aAAa,4BACxCgC,cAAcO,IAAIxC,sBACduD,KAAKvC,UAAUC,SAAS,oBACxBsC,KAAKvC,UAAUwC,QAAQ,WAAY,cACnCD,KAAKvC,UAAUwC,QAAQ,iBAAkB,qBAzG7CC,CAAWhC,mBAIfiC,cAAchE,sBACd0B,OAAOC,QAAQF,MAAMK,OAAOZ,uBA9RhB,CAACW,SAAUoC,oBAAqBC,+BAud5CC,eAAiBC,aAAaC,QAAQ,kBAGtCF,uCAEaG,gBAAgB,CACzBpD,QAASiD,eACTI,KAAM,YAIVH,aAAaI,WAAW,mBAhe5BC,GAGAxE,cAAgBgE,oBAGhB/D,eAAiB2B,SAGjB1B,MAAQ+D,gBAGJQ,KAAO/D,SAASC,eAAe,QAG/B+D,uBA8B4BD,UAE5BC,cAAgBhE,SAASC,eAAe,mCAG5C+D,cAAcC,iBAAiB,UAAU,eAEjCC,cAAgBlE,SAASC,eAAe+D,cAAcG,UAGtDD,cAAe,KACXE,QAAU,IACVC,cAAgBH,cAAcI,wBAAwBC,IACtDC,eAAiBT,KAAKU,UAAYJ,cAAgBD,OAGtDL,KAAKW,SAAS,CACVH,IAAKC,eACLG,SAAU,eAKfX,cArDaY,CAA2Bb,gBA8DtBA,KAAMC,mBAE3Ba,gBAAkB7E,SAASC,eAAe,mBAG9C8D,KAAKE,iBAAiB,UAAU,WACxBF,KAAKU,WAAa,IAClBI,gBAAgBC,MAAMC,QAAU,QAEhCF,gBAAgBC,MAAMC,QAAU,UAKxCF,gBAAgBZ,iBAAiB,SAAS,WACtCF,KAAKW,SAAS,CAACH,IAAK,EAAGI,SAAU,WACjCX,cAAcgB,cAAgB,KA3ElCC,CAAoBlB,KAAMC,8BAqFtBkB,oBAAsBlF,SAASgC,iBAAiB,yCAIhDkD,oBAAoB3D,OAAS,GAC7B2D,oBAAoBjD,SAAQ,SAASvC,QACjCA,OAAOuE,iBAAiB,SAAS,WAE7BlD,OAAOoE,SAASC,KAAO1F,OAAOE,aAAa,kBA1FvDyF,YAsGyBtB,KAAM7C,cAE3BoE,eAAiBtF,SAASgC,iBAAiB,qCAE3CsD,eAAe/D,OAAS,GAExB+D,eAAerD,SAAQ,SAASvC,QAC5BA,OAAOuE,iBAAiB,SAASzE,qBAEzB+F,aAAe7F,OAAOE,aAAa,yBACnCD,oBAAsBD,OAAOE,aAAa,+BAIzC2F,cAAgBA,aAAejG,eAA4B,MAAVE,2BAmQrCG,qBAE7B0D,cAAchE,0BAGVQ,aAAe,6CAAoBF,wBAEnCE,OAAOC,SACHD,OAAO2F,SAASjE,OAAS,EAAG,KACxBkE,aACA,+BAAkB,8BAA+B,OAAQ9F,oBAAqB,GAAG,GAGjF+F,YAAcC,uBAAaC,OAAO,CAClChC,KAAM+B,uBAAaE,MAAMC,MACzBC,MAAO,qBACPC,KAAM,4CAA8CP,YAAY9C,UAChEsD,QAAS,QAAW,kBAGlBP,MAAMQ,WACRC,gBAAiB,EACjBC,iBAAmBpG,SAASC,eAAe,+BAG/CyF,MAAMW,UAAUC,GAAGC,sBAAYC,QAAQ,KACnCd,MAAMe,UACNN,gBAAiB,SAGjBX,SAAWnE,KAAKC,MAAMzB,OAAO2F,UAE7BD,aAAeC,SAASjE,OACxBmF,MAAQ,EACRC,SAAW,OACV,MAAMC,WAAWpB,SAAU,KAEvBW,yBAKDU,cAAgB,gDAAuBlH,oBAAqBiH,QAAQE,WACpED,QAAQ/G,QACR4G,OAAgB,MACb,CACgB1G,SAASC,eAAe,4BAC9BK,UAAY,gDAAkDuG,QAAQtG,QAAU,SAC7FQ,OAAOC,QAAQF,MAAM+F,QAAQtG,SAGjCoG,SAASI,KAAKF,aAGVxE,SAAW2E,KAAKC,MAAOP,MAAQnB,aAAgB,qCACjCa,iBAAkB/D,UAAU,SAE5C6E,QAAQC,IAAIR,gBACZjB,MAAM0B,cAAc,SAAU,SAK5C/H,qBAAuBgI,aAAY,KAC/BpG,kBAAkB1B,kBAxdJ,MAsJI+H,CAAkB3H,yBACrB,QAEgBF,UAAU8H,OAClBzH,SAEPmB,kBAAkBC,iBAxHtCsG,CAAoBzD,EAAM7C,mBAsIH6C,KAAM7C,UAETlB,SAASC,eAAe,mBAG9BgE,iBAAiB,SAASzE,qBAEhC8F,eAAiBtF,SAASgC,iBAAiB,+DAG3CyF,MAAQnC,eAAe/D,OACvBmF,MAAQ,EAGRC,SAAW,GAGfrB,eAAerD,SAAQ,SAASvC,YAExBmH,QAAUpH,UAAUC,QACnBgI,MAAK,SAAS7H,eACPA,OAAOC,UACP4G,OAAgB,GAEb7G,UACR8H,OAAM,SAAS7G,OACdC,OAAOC,QAAQF,MAAMA,UAG7B6F,SAASI,KAAKF,kBAIZK,QAAQC,IAAIR,UAGlB5C,KAAKW,SAAS,CAACH,IAAK,EAAGI,SAAU,kBAG3BiD,sBAAajE,gBAAgB,CAC/BpD,QAASmG,MAAQ,OAASe,MAAQ,mCAClC7D,KAAO8C,QAAUe,MAAS,UAAY,YAI1CxG,kBAAkBC,aAhLtB2G,CAAkB9D,KAAM7C,UAIxBD,kBAAkBC,UAGlB7B,qBAAuBgI,aAAY,KAC/BpG,kBAAkB1B,kBA/CJ"}
\ No newline at end of file
diff --git a/amd/build/progress.min.js b/amd/build/progress.min.js
new file mode 100644
index 0000000..eee13bc
--- /dev/null
+++ b/amd/build/progress.min.js
@@ -0,0 +1,3 @@
+define("local_sitsgradepush/progress",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateProgressBar=_exports.createSpinner=_exports.createProgressBar=void 0;_exports.createProgressBar=function(id,classname,assessmentmappingid,progress){let showPercentage=arguments.length>4&&void 0!==arguments[4]&&arguments[4],progressBar=document.createElement("div");return progressBar.classList.add("progress",classname),progressBar.setAttribute("id",id),progressBar.setAttribute("data-assessmentmappingid",assessmentmappingid),updateProgressBar(progressBar,progress,showPercentage),progressBar};const updateProgressBar=function(progressBar,progress){let showPercentage=arguments.length>2&&void 0!==arguments[2]&&arguments[2],progressLabel="";showPercentage&&(progressLabel=progress+"%"),progressBar.innerHTML=''+progressLabel+"
"};_exports.updateProgressBar=updateProgressBar;_exports.createSpinner=(color,size)=>{let spinner=document.createElement("div");return spinner.setAttribute("role","status"),spinner.classList.add("spinner-border",color,size),spinner.innerHTML='Loading...',spinner}}));
+
+//# sourceMappingURL=progress.min.js.map
\ No newline at end of file
diff --git a/amd/build/progress.min.js.map b/amd/build/progress.min.js.map
new file mode 100644
index 0000000..0508e2b
--- /dev/null
+++ b/amd/build/progress.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"progress.min.js","sources":["../src/progress.js"],"sourcesContent":["/**\n * Create a progress bar.\n *\n * @param {string} id\n * @param {string} classname\n * @param {int} assessmentmappingid\n * @param {int} progress\n * @param {boolean} showPercentage\n * @return {HTMLElement}\n */\nexport const createProgressBar = (id, classname, assessmentmappingid, progress, showPercentage = false) => {\n // Create the progress bar and set the attributes.\n let progressBar = document.createElement('div');\n progressBar.classList.add('progress', classname);\n progressBar.setAttribute('id', id);\n progressBar.setAttribute('data-assessmentmappingid', assessmentmappingid);\n\n // Update the progress.\n updateProgressBar(progressBar, progress, showPercentage);\n\n return progressBar;\n};\n\n/**\n * Update the progress bar.\n *\n * @param {HTMLElement} progressBar\n * @param {int} progress\n * @param {boolean} showPercentage\n * @return {void}\n */\nexport const updateProgressBar = (progressBar, progress, showPercentage = false) => {\n // Show percentage if showPercentage is true.\n let progressLabel = '';\n if (showPercentage) {\n progressLabel = progress + '%';\n }\n\n // Update the progress bar.\n progressBar.innerHTML = '' + progressLabel + '
';\n};\n\n/**\n * Create a spinner.\n *\n * @param {string} color\n * @param {string} size\n * @return {HTMLElement}\n */\nexport const createSpinner = (color, size) => {\n // Create the spinner and set the attributes.\n let spinner = document.createElement('div');\n spinner.setAttribute('role', 'status');\n spinner.classList.add('spinner-border', color, size);\n spinner.innerHTML = 'Loading...';\n\n return spinner;\n};\n"],"names":["id","classname","assessmentmappingid","progress","showPercentage","progressBar","document","createElement","classList","add","setAttribute","updateProgressBar","progressLabel","innerHTML","color","size","spinner"],"mappings":"6OAUiC,SAACA,GAAIC,UAAWC,oBAAqBC,cAAUC,uEAExEC,YAAcC,SAASC,cAAc,cACzCF,YAAYG,UAAUC,IAAI,WAAYR,WACtCI,YAAYK,aAAa,KAAMV,IAC/BK,YAAYK,aAAa,2BAA4BR,qBAGrDS,kBAAkBN,YAAaF,SAAUC,gBAElCC,mBAWEM,kBAAoB,SAACN,YAAaF,cAAUC,uEAEjDQ,cAAgB,GAChBR,iBACAQ,cAAgBT,SAAW,KAI/BE,YAAYQ,UAAY,+DACpBV,SAAW,wDAA0DA,SAAW,MAAQS,cAAgB,8EAUnF,CAACE,MAAOC,YAE7BC,QAAUV,SAASC,cAAc,cACrCS,QAAQN,aAAa,OAAQ,UAC7BM,QAAQR,UAAUC,IAAI,iBAAkBK,MAAOC,MAC/CC,QAAQH,UAAY,0CAEbG"}
\ No newline at end of file
diff --git a/amd/build/sitsgradepush.min.js b/amd/build/sitsgradepush.min.js
index 8d3163a..200f75a 100644
--- a/amd/build/sitsgradepush.min.js
+++ b/amd/build/sitsgradepush.min.js
@@ -1,3 +1,3 @@
-define("local_sitsgradepush/sitsgradepush",["exports","./sitsgradepush_helper","core/notification"],(function(_exports,_sitsgradepush_helper,_notification){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};_exports.init=(coursemoduleid,mappingids)=>{let pushbuton=document.getElementById("local_sitsgradepush_pushbutton_async");if(null===pushbuton)return;let promises=[];pushbuton.addEventListener("click",(async e=>{e.preventDefault();let total=mappingids.length,count=0;mappingids.forEach((function(mappingid){let promise=(0,_sitsgradepush_helper.schedulePushTask)(mappingid).then((function(result){let taskstatus=document.getElementById("taskstatus-"+mappingid);if(result.success)count+=1,taskstatus.innerHTML=' '+result.status;else{let errormessagerow=document.createElement("tr");errormessagerow.setAttribute("class","error-message-row"),errormessagerow.innerHTML=''+result.message+" | ";let currentrow=taskstatus.closest("tr");null!==currentrow.nextElementSibling&¤trow.nextElementSibling.classList.contains("error-message-row")&¤trow.nextElementSibling.remove(),currentrow.insertAdjacentElement("afterend",errormessagerow)}return result.success})).catch((function(error){window.console.error(error)}));promises.push(promise)})),await Promise.all(promises),_notification.default.addNotification({message:count+" of "+total+" push tasks have been scheduled.",type:count===total?"success":"warning"})}))}}));
+define("local_sitsgradepush/sitsgradepush",["exports","./sitsgradepush_helper","./progress","core/notification"],(function(_exports,_sitsgradepush_helper,_progress,_notification){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};let updatePageIntervalId=null;async function updateTasksInfo(courseid){let update=await(0,_sitsgradepush_helper.getAssessmentsUpdate)(courseid);if(update.success){let assessments=JSON.parse(update.assessments);assessments.length>0?(function(assessments){let taskStatusContainers=document.querySelectorAll(".task-status"),assessmentsHasTasks=assessments.filter((assessment=>null!==assessment.task)),assessmentIds=new Set(assessmentsHasTasks.map((item=>item.assessmentmappingid)));taskStatusContainers.forEach((taskStatusContainer=>{assessmentIds.has(taskStatusContainer.getAttribute("data-assessmentmappingid"))||(taskStatusContainer.innerHTML="")})),assessmentsHasTasks.forEach((assessment=>{let task=assessment.task,progressBarId="progress-bar-"+task.assessmentmappingid,progressBar=document.getElementById(progressBarId);if(progressBar)(0,_progress.updateProgressBar)(progressBar,task.progress);else{progressBar=(0,_progress.createProgressBar)(progressBarId,"async",task.assessmentmappingid,0);let taskStatusContainer=document.getElementById("task-status-container-"+task.assessmentmappingid);if(taskStatusContainer){let spinner=(0,_progress.createSpinner)("text-primary","spinner-border-sm");taskStatusContainer.appendChild(spinner),taskStatusContainer.appendChild(progressBar)}}}))}(assessments),function(assessments){document.querySelectorAll(".last-transfer-task-date").forEach((container=>{let assessment=assessments.find((assessment=>assessment.assessmentmappingid===container.getAttribute("data-assessmentmappingid")));assessment&&null!==assessment.lasttransfertime&&(container.innerHTML=assessment.lasttransfertime)}))}(assessments)):clearInterval(updatePageIntervalId)}else clearInterval(updatePageIntervalId),window.console.error(update.message)}_exports.init=(courseid,coursemoduleid,mappingids)=>{!function(courseid,mappingids){let pushbuton=document.getElementById("local_sitsgradepush_pushbutton_async");if(null===pushbuton)return;let promises=[];pushbuton.addEventListener("click",(async e=>{e.preventDefault();let total=mappingids.length,count=0;mappingids.forEach((function(mappingid){let promise=(0,_sitsgradepush_helper.schedulePushTask)(mappingid).then((function(result){if(result.success)count+=1;else{let errormessagerow=document.createElement("tr");errormessagerow.setAttribute("class","error-message-row"),errormessagerow.innerHTML=''+result.message+" | ";let currentrow=document.getElementById("task-status-container-"+mappingid).closest("tr");null!==currentrow.nextElementSibling&¤trow.nextElementSibling.classList.contains("error-message-row")&¤trow.nextElementSibling.remove(),currentrow.insertAdjacentElement("afterend",errormessagerow)}return result.success})).catch((function(error){window.console.error(error)}));promises.push(promise)})),await Promise.all(promises),await updateTasksInfo(courseid),await _notification.default.addNotification({message:count+" of "+total+" push tasks have been scheduled.",type:count===total?"success":"warning"})}))}(courseid,mappingids),updateTasksInfo(courseid),updatePageIntervalId=setInterval((()=>{updateTasksInfo(courseid)}),15e3)}}));
//# sourceMappingURL=sitsgradepush.min.js.map
\ No newline at end of file
diff --git a/amd/build/sitsgradepush.min.js.map b/amd/build/sitsgradepush.min.js.map
index cb1f2b9..206c31b 100644
--- a/amd/build/sitsgradepush.min.js.map
+++ b/amd/build/sitsgradepush.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"sitsgradepush.min.js","sources":["../src/sitsgradepush.js"],"sourcesContent":["import {schedulePushTask} from \"./sitsgradepush_helper\";\nimport notification from 'core/notification';\n\nexport const init = (coursemoduleid, mappingids) => {\n // Get the push button.\n let pushbuton = document.getElementById('local_sitsgradepush_pushbutton_async');\n\n // Exit if the push button is not found.\n if (pushbuton === null) {\n return;\n }\n\n let promises = [];\n\n // Schedule a push task for each assessment mapping.\n pushbuton.addEventListener('click', async(e) => {\n e.preventDefault();\n\n // Number of assessment mappings.\n let total = mappingids.length;\n let count = 0;\n\n // Schedule a task to push grades to SITS for each assessment mapping.\n mappingids.forEach(function(mappingid) {\n let promise = schedulePushTask(mappingid)\n .then(function(result) {\n let taskstatus = document.getElementById('taskstatus-' + mappingid);\n if (result.success) {\n count = count + 1;\n taskstatus.innerHTML = ' ' + result.status;\n } else {\n // Create an error message row.\n let errormessagerow = document.createElement(\"tr\");\n errormessagerow.setAttribute(\"class\", \"error-message-row\");\n errormessagerow.innerHTML =\n '\">' +\n ' ' + result.message + ' ' +\n ' | ';\n\n // Find the closest row to the assessment mapping.\n let currentrow = taskstatus.closest(\"tr\");\n\n // Remove the existing error message row if it exists.\n if (currentrow.nextElementSibling !== null &&\n currentrow.nextElementSibling.classList.contains(\"error-message-row\")) {\n currentrow.nextElementSibling.remove();\n }\n\n // Insert the error message row for the assessment mapping.\n currentrow.insertAdjacentElement(\"afterend\", errormessagerow);\n }\n return result.success;\n })\n .catch(function(error) {\n window.console.error(error);\n });\n\n promises.push(promise);\n });\n\n // Wait for all the push tasks to be scheduled.\n await Promise.all(promises);\n\n // Display a notification.\n notification.addNotification({\n message: count + ' of ' + total + ' push tasks have been scheduled.',\n type: (count === total) ? 'success' : 'warning'\n });\n });\n};\n"],"names":["coursemoduleid","mappingids","pushbuton","document","getElementById","promises","addEventListener","async","e","preventDefault","total","length","count","forEach","mappingid","promise","then","result","taskstatus","success","innerHTML","status","errormessagerow","createElement","setAttribute","message","currentrow","closest","nextElementSibling","classList","contains","remove","insertAdjacentElement","catch","error","window","console","push","Promise","all","addNotification","type"],"mappings":"mUAGoB,CAACA,eAAgBC,kBAE7BC,UAAYC,SAASC,eAAe,2CAGtB,OAAdF,qBAIAG,SAAW,GAGfH,UAAUI,iBAAiB,SAASC,MAAAA,IAChCC,EAAEC,qBAGEC,MAAQT,WAAWU,OACnBC,MAAQ,EAGZX,WAAWY,SAAQ,SAASC,eACpBC,SAAU,0CAAiBD,WAC1BE,MAAK,SAASC,YACPC,WAAaf,SAASC,eAAe,cAAgBU,cACrDG,OAAOE,QACPP,OAAgB,EAChBM,WAAWE,UAAY,+CAAiDH,OAAOI,WAC5E,KAECC,gBAAkBnB,SAASoB,cAAc,MAC7CD,gBAAgBE,aAAa,QAAS,qBACtCF,gBAAgBF,UACZ,iEACkDH,OAAOQ,QADzD,kBAKAC,WAAaR,WAAWS,QAAQ,MAGE,OAAlCD,WAAWE,oBACXF,WAAWE,mBAAmBC,UAAUC,SAAS,sBACjDJ,WAAWE,mBAAmBG,SAIlCL,WAAWM,sBAAsB,WAAYV,wBAE1CL,OAAOE,WAEjBc,OAAM,SAASC,OACZC,OAAOC,QAAQF,MAAMA,UAG7B7B,SAASgC,KAAKtB,kBAIZuB,QAAQC,IAAIlC,gCAGLmC,gBAAgB,CACzBf,QAASb,MAAQ,OAASF,MAAQ,mCAClC+B,KAAO7B,QAAUF,MAAS,UAAY"}
\ No newline at end of file
+{"version":3,"file":"sitsgradepush.min.js","sources":["../src/sitsgradepush.js"],"sourcesContent":["import {schedulePushTask, getAssessmentsUpdate} from \"./sitsgradepush_helper\";\nimport {createProgressBar, updateProgressBar, createSpinner} from \"./progress\";\nimport notification from 'core/notification';\n\nlet updatePageIntervalId = null; // The interval ID for updating the progress.\nlet updatePageDelay = 15000; // The delay for updating the page.\n\n/**\n * Initialize the course module marks transfer page (index.php).\n *\n * @param {int} courseid\n * @param {int} coursemoduleid\n * @param {int[]} mappingids\n */\nexport const init = (courseid, coursemoduleid, mappingids) => {\n // Initialize the transfer marks button.\n initPushButton(courseid, mappingids);\n\n // Update the tasks progresses.\n updateTasksInfo(courseid);\n\n // Update the tasks progresses every 15 seconds.\n updatePageIntervalId = setInterval(() => {\n updateTasksInfo(courseid);\n }, updatePageDelay);\n};\n\n/**\n * Initialize the transfer marks button.\n *\n * @param {int} courseid\n * @param {int[]} mappingids\n */\nfunction initPushButton(courseid, mappingids) {\n // Get the push button.\n let pushbuton = document.getElementById('local_sitsgradepush_pushbutton_async');\n\n // Exit if the push button is not found.\n if (pushbuton === null) {\n return;\n }\n\n let promises = [];\n\n // Schedule a push task for each assessment mapping.\n pushbuton.addEventListener('click', async(e) => {\n e.preventDefault();\n\n // Number of assessment mappings.\n let total = mappingids.length;\n let count = 0;\n\n // Schedule a task to push grades to SITS for each assessment mapping.\n mappingids.forEach(function(mappingid) {\n let promise = schedulePushTask(mappingid)\n .then(function(result) {\n if (result.success) {\n count = count + 1;\n } else {\n // Create an error message row.\n let errormessagerow = document.createElement(\"tr\");\n errormessagerow.setAttribute(\"class\", \"error-message-row\");\n errormessagerow.innerHTML =\n '\">' +\n ' ' + result.message + ' ' +\n ' | ';\n\n // Find the task status container.\n let taskstatus = document.getElementById('task-status-container-' + mappingid);\n\n // Find the closest row to the assessment mapping.\n let currentrow = taskstatus.closest(\"tr\");\n\n // Remove the existing error message row if it exists.\n if (currentrow.nextElementSibling !== null &&\n currentrow.nextElementSibling.classList.contains(\"error-message-row\")) {\n currentrow.nextElementSibling.remove();\n }\n\n // Insert the error message row for the assessment mapping.\n currentrow.insertAdjacentElement(\"afterend\", errormessagerow);\n }\n return result.success;\n })\n .catch(function(error) {\n window.console.error(error);\n });\n\n promises.push(promise);\n });\n\n // Wait for all the push tasks to be scheduled.\n await Promise.all(promises);\n\n await updateTasksInfo(courseid);\n\n // Display a notification.\n await notification.addNotification({\n message: count + ' of ' + total + ' push tasks have been scheduled.',\n type: (count === total) ? 'success' : 'warning'\n });\n });\n}\n\n/**\n * Update all marks transfer tasks information.\n * e.g. progress bars, spinners and last transferred task date.\n *\n * @param {int} courseid\n * @return {void}\n */\nasync function updateTasksInfo(courseid) {\n // Get all latest tasks statuses.\n let update = await getAssessmentsUpdate(courseid);\n if (update.success) {\n // Parse the JSON string.\n let assessments = JSON.parse(update.assessments);\n\n if (assessments.length > 0) {\n // Update the progress bars and spinners.\n updateProgress(assessments);\n\n // Update the last transferred task date.\n updateLastTransferredTaskDate(assessments);\n } else {\n clearInterval(updatePageIntervalId);\n }\n } else {\n // Stop updating the tasks information if there is an error getting the updated tasks information.\n clearInterval(updatePageIntervalId);\n window.console.error(update.message);\n }\n}\n\n/**\n * Update the progress bars and spinners.\n *\n * @param {object[]} assessments\n */\nfunction updateProgress(assessments) {\n // Get the task status containers.\n let taskStatusContainers = document.querySelectorAll('.task-status');\n\n // Filter assessments that are having task in progress.\n let assessmentsHasTasks = assessments.filter(assessment => assessment.task !== null);\n\n // The assessment mapping IDs having task in progress.\n let assessmentIds = new Set(assessmentsHasTasks.map(item => item.assessmentmappingid));\n\n // Remove the progress bars and spinners for the assessment mappings that are not having task in progress.\n taskStatusContainers.forEach(taskStatusContainer => {\n if (!assessmentIds.has(taskStatusContainer.getAttribute('data-assessmentmappingid'))) {\n taskStatusContainer.innerHTML = '';\n }\n });\n\n // Update the task status containers with progress bars and spinner.\n assessmentsHasTasks.forEach(assessment => {\n let task = assessment.task;\n let progressBarId = 'progress-bar-' + task.assessmentmappingid;\n let progressBar = document.getElementById(progressBarId);\n\n // If the progress bar not exists, create a new one, otherwise update the progress.\n if (!progressBar) {\n progressBar = createProgressBar(progressBarId, 'async', task.assessmentmappingid, 0);\n let taskStatusContainer = document.getElementById('task-status-container-' + task.assessmentmappingid);\n if (taskStatusContainer) {\n let spinner = createSpinner('text-primary', 'spinner-border-sm');\n taskStatusContainer.appendChild(spinner);\n taskStatusContainer.appendChild(progressBar);\n }\n } else {\n updateProgressBar(progressBar, task.progress);\n }\n });\n}\n\n/**\n * Update the last transferred task date.\n *\n * @param {object[]} assessments\n */\nfunction updateLastTransferredTaskDate(assessments) {\n let containers = document.querySelectorAll('.last-transfer-task-date');\n\n containers.forEach(container => {\n let assessment = assessments.find(\n assessment => assessment.assessmentmappingid === container.getAttribute('data-assessmentmappingid')\n );\n\n if (assessment && assessment.lasttransfertime !== null) {\n container.innerHTML = assessment.lasttransfertime;\n }\n });\n}\n"],"names":["updatePageIntervalId","updateTasksInfo","courseid","update","success","assessments","JSON","parse","length","taskStatusContainers","document","querySelectorAll","assessmentsHasTasks","filter","assessment","task","assessmentIds","Set","map","item","assessmentmappingid","forEach","taskStatusContainer","has","getAttribute","innerHTML","progressBarId","progressBar","getElementById","progress","spinner","appendChild","updateProgress","container","find","lasttransfertime","updateLastTransferredTaskDate","clearInterval","window","console","error","message","coursemoduleid","mappingids","pushbuton","promises","addEventListener","async","e","preventDefault","total","count","mappingid","promise","then","result","errormessagerow","createElement","setAttribute","currentrow","closest","nextElementSibling","classList","contains","remove","insertAdjacentElement","catch","push","Promise","all","notification","addNotification","type","initPushButton","setInterval"],"mappings":"gVAIIA,qBAAuB,oBA2GZC,gBAAgBC,cAEvBC,aAAe,8CAAqBD,aACpCC,OAAOC,QAAS,KAEZC,YAAcC,KAAKC,MAAMJ,OAAOE,aAEhCA,YAAYG,OAAS,YAqBTH,iBAEhBI,qBAAuBC,SAASC,iBAAiB,gBAGjDC,oBAAsBP,YAAYQ,QAAOC,YAAkC,OAApBA,WAAWC,OAGlEC,cAAgB,IAAIC,IAAIL,oBAAoBM,KAAIC,MAAQA,KAAKC,uBAGjEX,qBAAqBY,SAAQC,sBACpBN,cAAcO,IAAID,oBAAoBE,aAAa,+BACpDF,oBAAoBG,UAAY,OAKxCb,oBAAoBS,SAAQP,iBACpBC,KAAOD,WAAWC,KAClBW,cAAgB,gBAAkBX,KAAKK,oBACvCO,YAAcjB,SAASkB,eAAeF,kBAGrCC,4CASiBA,YAAaZ,KAAKc,cATtB,CACdF,aAAc,+BAAkBD,cAAe,QAASX,KAAKK,oBAAqB,OAC9EE,oBAAsBZ,SAASkB,eAAe,yBAA2Bb,KAAKK,wBAC9EE,oBAAqB,KACjBQ,SAAU,2BAAc,eAAgB,qBAC5CR,oBAAoBS,YAAYD,SAChCR,oBAAoBS,YAAYJ,kBAjDpCK,CAAe3B,sBA8DYA,aAClBK,SAASC,iBAAiB,4BAEhCU,SAAQY,gBACXnB,WAAaT,YAAY6B,MACzBpB,YAAcA,WAAWM,sBAAwBa,UAAUT,aAAa,8BAGxEV,YAA8C,OAAhCA,WAAWqB,mBACzBF,UAAUR,UAAYX,WAAWqB,qBApEjCC,CAA8B/B,cAE9BgC,cAAcrC,2BAIlBqC,cAAcrC,sBACdsC,OAAOC,QAAQC,MAAMrC,OAAOsC,uBApHhB,CAACvC,SAAUwC,eAAgBC,wBAmBvBzC,SAAUyC,gBAE1BC,UAAYlC,SAASkB,eAAe,2CAGtB,OAAdgB,qBAIAC,SAAW,GAGfD,UAAUE,iBAAiB,SAASC,MAAAA,IAChCC,EAAEC,qBAGEC,MAAQP,WAAWnC,OACnB2C,MAAQ,EAGZR,WAAWtB,SAAQ,SAAS+B,eACpBC,SAAU,0CAAiBD,WAC1BE,MAAK,SAASC,WACPA,OAAOnD,QACP+C,OAAgB,MACb,KAECK,gBAAkB9C,SAAS+C,cAAc,MAC7CD,gBAAgBE,aAAa,QAAS,qBACtCF,gBAAgB/B,UACZ,iEACkD8B,OAAOd,QADzD,kBAQAkB,WAHajD,SAASkB,eAAe,yBAA2BwB,WAGxCQ,QAAQ,MAGE,OAAlCD,WAAWE,oBACXF,WAAWE,mBAAmBC,UAAUC,SAAS,sBACjDJ,WAAWE,mBAAmBG,SAIlCL,WAAWM,sBAAsB,WAAYT,wBAE1CD,OAAOnD,WAEjB8D,OAAM,SAAS1B,OACZF,OAAOC,QAAQC,MAAMA,UAG7BK,SAASsB,KAAKd,kBAIZe,QAAQC,IAAIxB,gBAEZ5C,gBAAgBC,gBAGhBoE,sBAAaC,gBAAgB,CAC/B9B,QAASU,MAAQ,OAASD,MAAQ,mCAClCsB,KAAOrB,QAAUD,MAAS,UAAY,eAnF9CuB,CAAevE,SAAUyC,YAGzB1C,gBAAgBC,UAGhBF,qBAAuB0E,aAAY,KAC/BzE,gBAAgBC,YAlBF"}
\ No newline at end of file
diff --git a/amd/build/sitsgradepush_helper.min.js b/amd/build/sitsgradepush_helper.min.js
index 9f9ee9a..c046cfa 100644
--- a/amd/build/sitsgradepush_helper.min.js
+++ b/amd/build/sitsgradepush_helper.min.js
@@ -1,3 +1,3 @@
-define("local_sitsgradepush/sitsgradepush_helper",["exports","core/ajax"],(function(_exports,_ajax){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.schedulePushTask=_exports.mapAssessment=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.schedulePushTask=assessmentmappingid=>new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_schedule_push_task",args:{assessmentmappingid:assessmentmappingid}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}));_exports.mapAssessment=function(courseid,coursemoduleid,mabid){let partid=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;return new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_map_assessment",args:{courseid:courseid,coursemoduleid:coursemoduleid,mabid:mabid,partid:partid}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}))}}));
+define("local_sitsgradepush/sitsgradepush_helper",["exports","core/ajax"],(function(_exports,_ajax){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.transferMarkForStudent=_exports.schedulePushTask=_exports.mapAssessment=_exports.getTransferStudents=_exports.getAssessmentsUpdate=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.schedulePushTask=async assessmentmappingid=>new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_schedule_push_task",args:{assessmentmappingid:assessmentmappingid}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}));_exports.mapAssessment=async function(courseid,coursemoduleid,mabid){let partid=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;return new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_map_assessment",args:{courseid:courseid,coursemoduleid:coursemoduleid,mabid:mabid,partid:partid}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}))};_exports.getAssessmentsUpdate=async courseid=>new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_get_assessments_update",args:{courseid:courseid}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}));_exports.getTransferStudents=assessmentmappingid=>new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_get_transfer_students",args:{assessmentmappingid:assessmentmappingid}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}));_exports.transferMarkForStudent=(assessmentmappingid,userid)=>new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_transfer_mark_for_student",args:{assessmentmappingid:assessmentmappingid,userid:userid}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}))}));
//# sourceMappingURL=sitsgradepush_helper.min.js.map
\ No newline at end of file
diff --git a/amd/build/sitsgradepush_helper.min.js.map b/amd/build/sitsgradepush_helper.min.js.map
index eddb2e8..a20b79e 100644
--- a/amd/build/sitsgradepush_helper.min.js.map
+++ b/amd/build/sitsgradepush_helper.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"sitsgradepush_helper.min.js","sources":["../src/sitsgradepush_helper.js"],"sourcesContent":["import Ajax from 'core/ajax';\n\n/**\n * Schedule a task to push grades to SITS.\n *\n * @param {string} assessmentmappingid The assessment mapping ID.\n * @return {Promise} Promise.\n */\nexport const schedulePushTask = (assessmentmappingid) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_schedule_push_task',\n args: {\n 'assessmentmappingid': assessmentmappingid,\n },\n }])[0].done(function(response) {\n resolve(response);\n }).fail(function(err) {\n window.console.log(err);\n reject(err);\n });\n });\n};\n\n/**\n * Map an assessment to a component grade.\n *\n * @param {string} courseid\n * @param {string} coursemoduleid\n * @param {string} mabid\n * @param {string} partid\n * @return {Promise}\n */\nexport const mapAssessment = (courseid, coursemoduleid, mabid, partid = null) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_map_assessment',\n args: {\n 'courseid': courseid,\n 'coursemoduleid': coursemoduleid,\n 'mabid': mabid,\n 'partid': partid,\n },\n }])[0].done(function(response) {\n resolve(response);\n }).fail(function(err) {\n window.console.log(err);\n reject(err);\n });\n });\n};\n"],"names":["assessmentmappingid","Promise","resolve","reject","call","methodname","args","done","response","fail","err","window","console","log","courseid","coursemoduleid","mabid","partid"],"mappings":"0SAQiCA,qBACtB,IAAIC,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,yCACZC,KAAM,qBACqBN,wBAE3B,GAAGO,MAAK,SAASC,UACjBN,QAAQM,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBP,OAAOO,kCAcU,SAACI,SAAUC,eAAgBC,WAAOC,8DAAS,YAC7D,IAAIhB,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,qCACZC,KAAM,UACUQ,wBACMC,qBACTC,aACCC,WAEd,GAAGV,MAAK,SAASC,UACjBN,QAAQM,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBP,OAAOO"}
\ No newline at end of file
+{"version":3,"file":"sitsgradepush_helper.min.js","sources":["../src/sitsgradepush_helper.js"],"sourcesContent":["import Ajax from 'core/ajax';\n\n/**\n * Schedule a task to push grades to SITS.\n *\n * @param {int} assessmentmappingid The assessment mapping ID.\n * @return {Promise} Promise.\n */\nexport const schedulePushTask = async (assessmentmappingid) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_schedule_push_task',\n args: {\n 'assessmentmappingid': assessmentmappingid,\n },\n }])[0].done(function(response) {\n resolve(response);\n }).fail(function(err) {\n window.console.log(err);\n reject(err);\n });\n });\n};\n\n/**\n * Map an assessment to a component grade.\n *\n * @param {int} courseid\n * @param {int} coursemoduleid\n * @param {int} mabid\n * @param {int|null} partid\n * @return {Promise}\n */\nexport const mapAssessment = async (courseid, coursemoduleid, mabid, partid = null) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_map_assessment',\n args: {\n 'courseid': courseid,\n 'coursemoduleid': coursemoduleid,\n 'mabid': mabid,\n 'partid': partid,\n },\n }])[0].done(function(response) {\n resolve(response);\n }).fail(function(err) {\n window.console.log(err);\n reject(err);\n });\n });\n};\n\n/**\n * Get the latest information about the assessment mappings of a course.\n * For updating the dashboard page and activity marks transfer page.\n *\n * @param {int} courseid\n * @return {Promise}\n */\nexport const getAssessmentsUpdate = async(courseid) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_get_assessments_update',\n args: {\n 'courseid': courseid,\n },\n }])[0].done(function(response) {\n resolve(response);\n }).fail(function(err) {\n window.console.log(err);\n reject(err);\n });\n });\n};\n\n/**\n * Get the students information for a given assessment mapping.\n * For synchronous marks transfer.\n *\n * @param {int} assessmentmappingid\n * @return {Promise}\n */\nexport const getTransferStudents = (assessmentmappingid) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_get_transfer_students',\n args: {'assessmentmappingid': assessmentmappingid},\n }])[0].done(function(response) {\n resolve(response);\n }).fail(function(err) {\n window.console.log(err);\n reject(err);\n });\n });\n};\n\n/**\n * Transfer mark for a student.\n *\n * @param {int} assessmentmappingid\n * @param {int} userid\n * @return {Promise}\n */\nexport const transferMarkForStudent = (assessmentmappingid, userid) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_transfer_mark_for_student',\n args: {\n 'assessmentmappingid': assessmentmappingid,\n 'userid': userid\n },\n }])[0].done(function(response) {\n resolve(response);\n }).fail(function(err) {\n window.console.log(err);\n reject(err);\n });\n });\n};\n"],"names":["async","Promise","resolve","reject","call","methodname","args","assessmentmappingid","done","response","fail","err","window","console","log","courseid","coursemoduleid","mabid","partid","userid"],"mappings":"qYAQgCA,MAAAA,qBACrB,IAAIC,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,yCACZC,KAAM,qBACqBC,wBAE3B,GAAGC,MAAK,SAASC,UACjBP,QAAQO,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBR,OAAOQ,kCAcUX,eAAOe,SAAUC,eAAgBC,WAAOC,8DAAS,YACnE,IAAIjB,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,qCACZC,KAAM,UACUS,wBACMC,qBACTC,aACCC,WAEd,GAAGV,MAAK,SAASC,UACjBP,QAAQO,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBR,OAAOQ,0CAYiBX,MAAAA,UACzB,IAAIC,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,6CACZC,KAAM,UACUS,aAEhB,GAAGP,MAAK,SAASC,UACjBP,QAAQO,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBR,OAAOQ,wCAYiBJ,qBACzB,IAAIN,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,4CACZC,KAAM,qBAAwBC,wBAC9B,GAAGC,MAAK,SAASC,UACjBP,QAAQO,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBR,OAAOQ,2CAYmB,CAACJ,oBAAqBY,SACjD,IAAIlB,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,gDACZC,KAAM,qBACqBC,2BACbY,WAEd,GAAGX,MAAK,SAASC,UACjBP,QAAQO,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBR,OAAOQ"}
\ No newline at end of file
diff --git a/amd/src/dashboard.js b/amd/src/dashboard.js
index 8acf58f..789d8a0 100644
--- a/amd/src/dashboard.js
+++ b/amd/src/dashboard.js
@@ -1,13 +1,75 @@
-import {schedulePushTask} from "./sitsgradepush_helper";
+import {
+ schedulePushTask,
+ getTransferStudents,
+ transferMarkForStudent,
+ getAssessmentsUpdate
+} from "./sitsgradepush_helper";
+import {createProgressBar, updateProgressBar, createSpinner} from "./progress";
import notification from "core/notification";
+import ModalFactory from 'core/modal_factory';
+import ModalEvents from 'core/modal_events';
-export const init = () => {
+let updatePageIntervalId = null; // The interval ID for updating the progress.
+let syncThreshold = 30; // The threshold which determines whether it is a sync or async marks transfer.
+let globalCourseid = null; // The global variable for course ID.
+let updatePageDelay = 15000; // The delay for updating the page.
+let async = null; // The async config.
+
+/**
+ * Initialize the dashboard page.
+ *
+ * @param {int} courseid
+ * @param {int} syncThresholdConfig
+ * @param {int} asyncConfig
+ */
+export const init = (courseid, syncThresholdConfig, asyncConfig) => {
// If there is a saved message by successfully mapped an assessment in localStorage, display it.
displayNotification();
+ // Set the sync threshold from the plugin config.
+ syncThreshold = syncThresholdConfig;
+
+ // Set the global variable course ID.
+ globalCourseid = courseid;
+
+ // Set the async config.
+ async = asyncConfig;
+
// Find the page element.
let page = document.getElementById("page");
+ // Initialize the module delivery dropdown list.
+ let tableSelector = initModuleDeliverySelector(page);
+
+ // Initialize the back to top button.
+ initBackToTopButton(page, tableSelector);
+
+ // Initialize the change source buttons.
+ initChangeSourceButtons();
+
+ // Initialize the push buttons.
+ initPushMarkButtons(page, courseid);
+
+ // Initialize the push all button.
+ initPushAllButton(page, courseid);
+
+ // Update the dashboard page with the latest information.
+ // E.g. progress bars, push buttons, records icons.
+ updateAssessments(courseid);
+
+ // Update the page every 15 seconds.
+ updatePageIntervalId = setInterval(() => {
+ updateAssessments(globalCourseid);
+ }, updatePageDelay);
+
+};
+
+/**
+ * Initialize the module delivery dropdown list.
+ *
+ * @param {HTMLElement} page
+ */
+function initModuleDeliverySelector(page) {
// Find the module delivery table selector.
let tableSelector = document.getElementById("module-delivery-selector");
@@ -30,6 +92,16 @@ export const init = () => {
}
});
+ return tableSelector;
+}
+
+/**
+ * Initialize the back to top button.
+ *
+ * @param {HTMLElement} page
+ * @param {HTMLElement} tableSelector
+ */
+function initBackToTopButton(page, tableSelector) {
// Find the back to top button.
let backToTopButton = document.getElementById("backToTopButton");
@@ -47,7 +119,13 @@ export const init = () => {
page.scrollTo({top: 0, behavior: "smooth"});
tableSelector.selectedIndex = 0;
});
+}
+/**
+ * Initialize the change source buttons.
+ *
+ */
+function initChangeSourceButtons() {
// Get all change source buttons.
let changesourcebuttons = document.querySelectorAll(".change-source-button:not([disabled])");
@@ -61,26 +139,57 @@ export const init = () => {
});
});
}
+}
+/**
+ * Initialize the push mark buttons.
+ *
+ * @param {HTMLElement} page
+ * @param {int} courseid
+ */
+function initPushMarkButtons(page, courseid) {
// Get all the push buttons that are not disabled.
let mabpushbuttons = document.querySelectorAll(".push-mark-button:not([disabled])");
if (mabpushbuttons.length > 0) {
// Push grades when the user clicks on each enabled push button.
mabpushbuttons.forEach(function(button) {
- button.addEventListener("click", function() {
- pushMarks(this);
+ button.addEventListener("click", async function() {
+ // Find the number of students to push grades.
+ let studentcount = button.getAttribute("data-numberofstudents");
+ let assessmentmappingid = button.getAttribute("data-assessmentmappingid");
+
+ // Do synchronous marks transfer if the number of marks to be transferred is less than the sync threshold.
+ // Or if the async config is disabled.
+ if ((studentcount && studentcount < syncThreshold) || async === '0') {
+ await syncMarksTransfer(assessmentmappingid);
+ } else {
+ // Schedule an asynchronous marks transfer task.
+ let result = await pushMarks(this);
+ if (result.success) {
+ // Update the page after scheduling a marks transfer task.
+ updateAssessments(courseid);
+ }
+ }
});
});
}
+}
+/**
+ * Initialize the push all button.
+ *
+ * @param {HTMLElement} page
+ * @param {int} courseid
+ */
+function initPushAllButton(page, courseid) {
// Get the push all button.
let pushallbutton = document.getElementById("push-all-button");
// Push grades for all the not disabled push buttons when the user clicks on the push all button.
pushallbutton.addEventListener("click", async function() {
- // Get the updated not disabled push buttons.
- let mabpushbuttons = document.querySelectorAll(".push-mark-button:not([disabled])");
+ // Get the updated not disabled push buttons and has assessment ID.
+ let mabpushbuttons = document.querySelectorAll(".push-mark-button:not([disabled])[data-assessmentmappingid]");
// Number of not disabled push buttons.
let total = mabpushbuttons.length;
@@ -94,7 +203,7 @@ export const init = () => {
// Create a Promise for each button and push it into the array.
let promise = pushMarks(button)
.then(function(result) {
- if (result) {
+ if (result.success) {
count = count + 1;
}
return result;
@@ -112,12 +221,15 @@ export const init = () => {
page.scrollTo({top: 0, behavior: "instant"});
// Show the notification.
- notification.addNotification({
+ await notification.addNotification({
message: count + ' of ' + total + ' push tasks have been scheduled.',
type: (count === total) ? 'success' : 'warning'
});
+
+ // Update the page information.
+ updateAssessments(courseid);
});
-};
+}
/**
* Schedule a push task when the user clicks on a push button.
@@ -135,14 +247,6 @@ async function pushMarks(button) {
// Check if the push task is successfully scheduled.
if (result.success) {
- // Update the icon.
- let icon = button.parentNode.parentNode.querySelector("span:last-child");
- icon.innerHTML = '';
-
- // Disable the push button after scheduled push task successfully.
- button.disabled = true;
-
// Remove the tooltip (for Firefox and Safari).
let tooltipid = button.getAttribute("aria-describedby");
if (tooltipid !== null && document.getElementById(tooltipid) !== null) {
@@ -172,13 +276,218 @@ async function pushMarks(button) {
currentrow.insertAdjacentElement("afterend", errormessagerow);
}
- return result.success;
+ return result;
} catch (error) {
window.console.error(error);
return false;
}
}
+/**
+ * Update the dashboard page with the latest information.
+ * e.g. progress bars, push buttons, records icons.
+ *
+ * @param {int} courseid
+ * @return void
+ */
+async function updateAssessments(courseid) {
+ // Get latest assessments information for the dashboard page.
+ let update = await getAssessmentsUpdate(courseid);
+
+ if (update.success) {
+ // Parse the JSON string.
+ let assessments = JSON.parse(update.assessments);
+
+ if (assessments.length > 0) {
+ // Update the all the progress bars and push buttons.
+ updateTasksProgresses(assessments);
+
+ // Update the records icons.
+ updateIcon(assessments);
+ }
+ } else {
+ // Stop update the page if error occurred.
+ clearInterval(updatePageIntervalId);
+ window.console.error(update.message);
+ }
+}
+
+/**
+ * Update all the progress bars and push buttons in the dashboard page.
+ *
+ * @param {object[]} assessments
+ */
+function updateTasksProgresses(assessments) {
+ // Filter assessments that are having task in progress.
+ let assessmentsHasTasks = assessments.filter(assessment => assessment.task !== null);
+
+ // The assessment mapping IDs having task in progress.
+ let assessmentIds = new Set(assessmentsHasTasks.map(item => item.assessmentmappingid));
+
+ // Update the progress bars.
+ updateProgressBars(assessmentsHasTasks, assessmentIds);
+
+ // Update the push buttons.
+ updatePushButtons(assessmentIds);
+}
+
+/**
+ * Update all the progress bars in the dashboard page.
+ *
+ * @param {object[]} assessmentsHasTasks
+ * @param {Set} assessmentIds
+ */
+function updateProgressBars(assessmentsHasTasks, assessmentIds) {
+ let progressBars = document.querySelectorAll('.progress.async');
+
+ // Remove the progress bars that are not in the assessmentIds.
+ progressBars.forEach(progressBar => {
+ if (!assessmentIds.has(progressBar.getAttribute('data-assessmentmappingid'))) {
+ progressBar.remove();
+ }
+ });
+
+ assessmentsHasTasks.forEach(assessment => {
+ let progressBarId = 'progress-bar-' + assessment.task.assessmentmappingid;
+ let progressBar = document.getElementById(progressBarId);
+ let task = assessment.task;
+
+ // If the progress bar not exists, create a new one, otherwise update the progress.
+ if (!progressBar) {
+ progressBar = createProgressBar(progressBarId, 'async', task.assessmentmappingid, task.progress);
+ let button = document.querySelector('.push-mark-button[data-assessmentmappingid="' + task.assessmentmappingid + '"]');
+ button.parentNode.parentNode.insertAdjacentElement('afterend', progressBar);
+ } else {
+ updateProgressBar(progressBar, task.progress);
+ }
+ });
+}
+
+/**
+ * Update all the push buttons in the dashboard page.
+ *
+ * @param {Set} assessmentIds
+ */
+function updatePushButtons(assessmentIds) {
+ // Find all push buttons.
+ let pushButtons = document.querySelectorAll('.push-mark-button');
+
+ pushButtons.forEach(function(pushButton) {
+ let assessmentmappingid = pushButton.getAttribute('data-assessmentmappingid');
+
+ if (assessmentIds.has(assessmentmappingid)) {
+ // If the task ID is found, show spinner and disable button.
+ let spinner = createSpinner('text-light', 'spinner-border-sm');
+ pushButton.innerHTML = spinner.outerHTML;
+ pushButton.disabled = true;
+ } else {
+ // Reset to original state if not in taskIds.
+ pushButton.innerHTML = '';
+ pushButton.disabled = false;
+ }
+ });
+}
+
+/**
+ * Update the icons to show that there are transfer records.
+ *
+ * @param {object[]} assessments
+ */
+function updateIcon(assessments) {
+ let icons = document.querySelectorAll('.records-icon');
+
+ // Get assessment mappings that have transfer records.
+ let assessmentsHasTransferRecords = assessments.filter(update => update.transferrecords === 1);
+
+ // Extract the assessment mapping IDs.
+ let assessmentIds = new Set(assessmentsHasTransferRecords.map(assessment => assessment.assessmentmappingid));
+
+ // Update the icons to show that there are transfer records.
+ icons.forEach(function(icon) {
+ let assessmentmappingid = icon.getAttribute('data-assessmentmappingid');
+ if (assessmentIds.has(assessmentmappingid)) {
+ if (icon.classList.contains('fa-circle-info')) {
+ icon.classList.replace('fa-solid', 'fa-regular');
+ icon.classList.replace('fa-circle-info', 'fa-file-lines');
+ }
+ }
+ });
+}
+
+/**
+ * Transfer marks for all the students in the assessment mapping synchronously.
+ *
+ * @param {int} assessmentmappingid
+ * @return {Promise}
+ */
+async function syncMarksTransfer(assessmentmappingid) {
+ // Stop the page update while transferring marks.
+ clearInterval(updatePageIntervalId);
+
+ // Get the students to transfer marks.
+ let result = await getTransferStudents(assessmentmappingid);
+
+ if (result.success) {
+ if (result.students.length > 0) {
+ let progressbar =
+ createProgressBar('dashboard-progress-bar-sync', 'sync', assessmentmappingid, 0, true);
+
+ // Create a modal to show the progress.
+ let modal = await ModalFactory.create({
+ type: ModalFactory.types.ALERT,
+ title: 'Transferring Marks',
+ body: '' + progressbar.outerHTML,
+ buttons: {'cancel': 'Cancel'}
+ });
+
+ await modal.show();
+ let isModalVisible = true;
+ let modalProgressbar = document.getElementById('dashboard-progress-bar-sync');
+
+ // Destroy the modal when it is hidden.
+ modal.getRoot().on(ModalEvents.hidden, () => {
+ modal.destroy();
+ isModalVisible = false;
+ });
+
+ let students = JSON.parse(result.students);
+
+ let studentcount = students.length;
+ let count = 0;
+ let promises = [];
+ for (const student of students) {
+ // Stop the progress if the modal is closed.
+ if (!isModalVisible) {
+ break;
+ }
+
+ // Transfer mark for each student.
+ let promise = await transferMarkForStudent(assessmentmappingid, student.userid);
+ if (promise.success) {
+ count = count + 1;
+ } else {
+ let errormessage = document.getElementById('error-message-modal-sync');
+ errormessage.innerHTML = '' + promise.message + '
';
+ window.console.error(promise.message);
+ }
+
+ promises.push(promise);
+
+ // Calculate the progress.
+ let progress = Math.round((count / studentcount) * 100);
+ updateProgressBar(modalProgressbar, progress, true);
+ }
+ await Promise.all(promises);
+ await modal.setButtonText('cancel', 'Close');
+ }
+ }
+
+ // Resume the page update.
+ updatePageIntervalId = setInterval(() => {
+ updateAssessments(globalCourseid);
+ }, updatePageDelay);
+}
+
/**
* Display a notification if a success message is available in localStorage.
*/
diff --git a/amd/src/progress.js b/amd/src/progress.js
new file mode 100644
index 0000000..aa1b139
--- /dev/null
+++ b/amd/src/progress.js
@@ -0,0 +1,59 @@
+/**
+ * Create a progress bar.
+ *
+ * @param {string} id
+ * @param {string} classname
+ * @param {int} assessmentmappingid
+ * @param {int} progress
+ * @param {boolean} showPercentage
+ * @return {HTMLElement}
+ */
+export const createProgressBar = (id, classname, assessmentmappingid, progress, showPercentage = false) => {
+ // Create the progress bar and set the attributes.
+ let progressBar = document.createElement('div');
+ progressBar.classList.add('progress', classname);
+ progressBar.setAttribute('id', id);
+ progressBar.setAttribute('data-assessmentmappingid', assessmentmappingid);
+
+ // Update the progress.
+ updateProgressBar(progressBar, progress, showPercentage);
+
+ return progressBar;
+};
+
+/**
+ * Update the progress bar.
+ *
+ * @param {HTMLElement} progressBar
+ * @param {int} progress
+ * @param {boolean} showPercentage
+ * @return {void}
+ */
+export const updateProgressBar = (progressBar, progress, showPercentage = false) => {
+ // Show percentage if showPercentage is true.
+ let progressLabel = '';
+ if (showPercentage) {
+ progressLabel = progress + '%';
+ }
+
+ // Update the progress bar.
+ progressBar.innerHTML = '' + progressLabel + '
';
+};
+
+/**
+ * Create a spinner.
+ *
+ * @param {string} color
+ * @param {string} size
+ * @return {HTMLElement}
+ */
+export const createSpinner = (color, size) => {
+ // Create the spinner and set the attributes.
+ let spinner = document.createElement('div');
+ spinner.setAttribute('role', 'status');
+ spinner.classList.add('spinner-border', color, size);
+ spinner.innerHTML = 'Loading...';
+
+ return spinner;
+};
diff --git a/amd/src/sitsgradepush.js b/amd/src/sitsgradepush.js
index 6918cde..80c8f66 100644
--- a/amd/src/sitsgradepush.js
+++ b/amd/src/sitsgradepush.js
@@ -1,7 +1,37 @@
-import {schedulePushTask} from "./sitsgradepush_helper";
+import {schedulePushTask, getAssessmentsUpdate} from "./sitsgradepush_helper";
+import {createProgressBar, updateProgressBar, createSpinner} from "./progress";
import notification from 'core/notification';
-export const init = (coursemoduleid, mappingids) => {
+let updatePageIntervalId = null; // The interval ID for updating the progress.
+let updatePageDelay = 15000; // The delay for updating the page.
+
+/**
+ * Initialize the course module marks transfer page (index.php).
+ *
+ * @param {int} courseid
+ * @param {int} coursemoduleid
+ * @param {int[]} mappingids
+ */
+export const init = (courseid, coursemoduleid, mappingids) => {
+ // Initialize the transfer marks button.
+ initPushButton(courseid, mappingids);
+
+ // Update the tasks progresses.
+ updateTasksInfo(courseid);
+
+ // Update the tasks progresses every 15 seconds.
+ updatePageIntervalId = setInterval(() => {
+ updateTasksInfo(courseid);
+ }, updatePageDelay);
+};
+
+/**
+ * Initialize the transfer marks button.
+ *
+ * @param {int} courseid
+ * @param {int[]} mappingids
+ */
+function initPushButton(courseid, mappingids) {
// Get the push button.
let pushbuton = document.getElementById('local_sitsgradepush_pushbutton_async');
@@ -24,10 +54,8 @@ export const init = (coursemoduleid, mappingids) => {
mappingids.forEach(function(mappingid) {
let promise = schedulePushTask(mappingid)
.then(function(result) {
- let taskstatus = document.getElementById('taskstatus-' + mappingid);
if (result.success) {
count = count + 1;
- taskstatus.innerHTML = ' ' + result.status;
} else {
// Create an error message row.
let errormessagerow = document.createElement("tr");
@@ -37,6 +65,9 @@ export const init = (coursemoduleid, mappingids) => {
'' + result.message + '
' +
'';
+ // Find the task status container.
+ let taskstatus = document.getElementById('task-status-container-' + mappingid);
+
// Find the closest row to the assessment mapping.
let currentrow = taskstatus.closest("tr");
@@ -61,10 +92,104 @@ export const init = (coursemoduleid, mappingids) => {
// Wait for all the push tasks to be scheduled.
await Promise.all(promises);
+ await updateTasksInfo(courseid);
+
// Display a notification.
- notification.addNotification({
+ await notification.addNotification({
message: count + ' of ' + total + ' push tasks have been scheduled.',
type: (count === total) ? 'success' : 'warning'
});
});
-};
+}
+
+/**
+ * Update all marks transfer tasks information.
+ * e.g. progress bars, spinners and last transferred task date.
+ *
+ * @param {int} courseid
+ * @return {void}
+ */
+async function updateTasksInfo(courseid) {
+ // Get all latest tasks statuses.
+ let update = await getAssessmentsUpdate(courseid);
+ if (update.success) {
+ // Parse the JSON string.
+ let assessments = JSON.parse(update.assessments);
+
+ if (assessments.length > 0) {
+ // Update the progress bars and spinners.
+ updateProgress(assessments);
+
+ // Update the last transferred task date.
+ updateLastTransferredTaskDate(assessments);
+ } else {
+ clearInterval(updatePageIntervalId);
+ }
+ } else {
+ // Stop updating the tasks information if there is an error getting the updated tasks information.
+ clearInterval(updatePageIntervalId);
+ window.console.error(update.message);
+ }
+}
+
+/**
+ * Update the progress bars and spinners.
+ *
+ * @param {object[]} assessments
+ */
+function updateProgress(assessments) {
+ // Get the task status containers.
+ let taskStatusContainers = document.querySelectorAll('.task-status');
+
+ // Filter assessments that are having task in progress.
+ let assessmentsHasTasks = assessments.filter(assessment => assessment.task !== null);
+
+ // The assessment mapping IDs having task in progress.
+ let assessmentIds = new Set(assessmentsHasTasks.map(item => item.assessmentmappingid));
+
+ // Remove the progress bars and spinners for the assessment mappings that are not having task in progress.
+ taskStatusContainers.forEach(taskStatusContainer => {
+ if (!assessmentIds.has(taskStatusContainer.getAttribute('data-assessmentmappingid'))) {
+ taskStatusContainer.innerHTML = '';
+ }
+ });
+
+ // Update the task status containers with progress bars and spinner.
+ assessmentsHasTasks.forEach(assessment => {
+ let task = assessment.task;
+ let progressBarId = 'progress-bar-' + task.assessmentmappingid;
+ let progressBar = document.getElementById(progressBarId);
+
+ // If the progress bar not exists, create a new one, otherwise update the progress.
+ if (!progressBar) {
+ progressBar = createProgressBar(progressBarId, 'async', task.assessmentmappingid, 0);
+ let taskStatusContainer = document.getElementById('task-status-container-' + task.assessmentmappingid);
+ if (taskStatusContainer) {
+ let spinner = createSpinner('text-primary', 'spinner-border-sm');
+ taskStatusContainer.appendChild(spinner);
+ taskStatusContainer.appendChild(progressBar);
+ }
+ } else {
+ updateProgressBar(progressBar, task.progress);
+ }
+ });
+}
+
+/**
+ * Update the last transferred task date.
+ *
+ * @param {object[]} assessments
+ */
+function updateLastTransferredTaskDate(assessments) {
+ let containers = document.querySelectorAll('.last-transfer-task-date');
+
+ containers.forEach(container => {
+ let assessment = assessments.find(
+ assessment => assessment.assessmentmappingid === container.getAttribute('data-assessmentmappingid')
+ );
+
+ if (assessment && assessment.lasttransfertime !== null) {
+ container.innerHTML = assessment.lasttransfertime;
+ }
+ });
+}
diff --git a/amd/src/sitsgradepush_helper.js b/amd/src/sitsgradepush_helper.js
index 347ae53..819f91c 100644
--- a/amd/src/sitsgradepush_helper.js
+++ b/amd/src/sitsgradepush_helper.js
@@ -3,10 +3,10 @@ import Ajax from 'core/ajax';
/**
* Schedule a task to push grades to SITS.
*
- * @param {string} assessmentmappingid The assessment mapping ID.
+ * @param {int} assessmentmappingid The assessment mapping ID.
* @return {Promise} Promise.
*/
-export const schedulePushTask = (assessmentmappingid) => {
+export const schedulePushTask = async (assessmentmappingid) => {
return new Promise((resolve, reject) => {
Ajax.call([{
methodname: 'local_sitsgradepush_schedule_push_task',
@@ -25,13 +25,13 @@ export const schedulePushTask = (assessmentmappingid) => {
/**
* Map an assessment to a component grade.
*
- * @param {string} courseid
- * @param {string} coursemoduleid
- * @param {string} mabid
- * @param {string} partid
+ * @param {int} courseid
+ * @param {int} coursemoduleid
+ * @param {int} mabid
+ * @param {int|null} partid
* @return {Promise}
*/
-export const mapAssessment = (courseid, coursemoduleid, mabid, partid = null) => {
+export const mapAssessment = async (courseid, coursemoduleid, mabid, partid = null) => {
return new Promise((resolve, reject) => {
Ajax.call([{
methodname: 'local_sitsgradepush_map_assessment',
@@ -49,3 +49,71 @@ export const mapAssessment = (courseid, coursemoduleid, mabid, partid = null) =>
});
});
};
+
+/**
+ * Get the latest information about the assessment mappings of a course.
+ * For updating the dashboard page and activity marks transfer page.
+ *
+ * @param {int} courseid
+ * @return {Promise}
+ */
+export const getAssessmentsUpdate = async(courseid) => {
+ return new Promise((resolve, reject) => {
+ Ajax.call([{
+ methodname: 'local_sitsgradepush_get_assessments_update',
+ args: {
+ 'courseid': courseid,
+ },
+ }])[0].done(function(response) {
+ resolve(response);
+ }).fail(function(err) {
+ window.console.log(err);
+ reject(err);
+ });
+ });
+};
+
+/**
+ * Get the students information for a given assessment mapping.
+ * For synchronous marks transfer.
+ *
+ * @param {int} assessmentmappingid
+ * @return {Promise}
+ */
+export const getTransferStudents = (assessmentmappingid) => {
+ return new Promise((resolve, reject) => {
+ Ajax.call([{
+ methodname: 'local_sitsgradepush_get_transfer_students',
+ args: {'assessmentmappingid': assessmentmappingid},
+ }])[0].done(function(response) {
+ resolve(response);
+ }).fail(function(err) {
+ window.console.log(err);
+ reject(err);
+ });
+ });
+};
+
+/**
+ * Transfer mark for a student.
+ *
+ * @param {int} assessmentmappingid
+ * @param {int} userid
+ * @return {Promise}
+ */
+export const transferMarkForStudent = (assessmentmappingid, userid) => {
+ return new Promise((resolve, reject) => {
+ Ajax.call([{
+ methodname: 'local_sitsgradepush_transfer_mark_for_student',
+ args: {
+ 'assessmentmappingid': assessmentmappingid,
+ 'userid': userid
+ },
+ }])[0].done(function(response) {
+ resolve(response);
+ }).fail(function(err) {
+ window.console.log(err);
+ reject(err);
+ });
+ });
+};
diff --git a/apiclients/easikit/classes/webclient.php b/apiclients/easikit/classes/webclient.php
index 1051258..d34b160 100644
--- a/apiclients/easikit/classes/webclient.php
+++ b/apiclients/easikit/classes/webclient.php
@@ -16,6 +16,9 @@
namespace sitsapiclient_easikit;
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/filelib.php');
+
use cache;
use curl;
use local_sitsgradepush\api\irequest;
diff --git a/classes/assessment/assessmentfactory.php b/classes/assessment/assessmentfactory.php
index ac99cd6..418c467 100644
--- a/classes/assessment/assessmentfactory.php
+++ b/classes/assessment/assessmentfactory.php
@@ -28,11 +28,18 @@ class assessmentfactory {
/**
* Return assessment object by a given mod name.
*
- * @param \stdClass $coursemodule
+ * @param int|\stdClass $coursemodule
* @return assessment
* @throws \moodle_exception
*/
- public static function get_assessment(\stdClass $coursemodule) {
+ public static function get_assessment(int|\stdClass $coursemodule) {
+ // Get course module object if coursemodule is an id.
+ if (is_int($coursemodule)) {
+ if (!$coursemodule = \get_coursemodule_from_id('', $coursemodule)) {
+ throw new \moodle_exception('error:coursemodulenotfound', 'local_sitsgradepush', $coursemodule);
+ }
+ }
+
switch ($coursemodule->modname) {
case 'quiz':
return new quiz($coursemodule);
diff --git a/classes/external/get_assessments_update.php b/classes/external/get_assessments_update.php
new file mode 100644
index 0000000..24106f0
--- /dev/null
+++ b/classes/external/get_assessments_update.php
@@ -0,0 +1,94 @@
+.
+namespace local_sitsgradepush\external;
+
+use core_external\external_api;
+use core_external\external_function_parameters;
+use core_external\external_single_structure;
+use core_external\external_value;
+use local_sitsgradepush\manager;
+
+/**
+ * External API for getting assessments update for page updates.
+ *
+ * @package local_sitsgradepush
+ * @copyright 2023 onwards University College London {@link https://www.ucl.ac.uk/}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author Alex Yeung
+ */
+class get_assessments_update extends external_api {
+ /**
+ * Returns description of method parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters() {
+ return new external_function_parameters([
+ 'courseid' => new external_value(PARAM_INT, 'Course ID', VALUE_REQUIRED),
+ 'coursemoduleid' => new external_value(PARAM_INT, 'Course module ID', VALUE_DEFAULT, 0),
+ ]);
+ }
+
+ /**
+ * Returns description of method result value.
+ *
+ * @return \external_single_structure
+ */
+ public static function execute_returns() {
+ return new external_single_structure([
+ 'success' => new external_value(PARAM_BOOL, 'Result of request', VALUE_REQUIRED),
+ 'assessments' => new external_value(PARAM_RAW, 'Assessments Latest Status', VALUE_REQUIRED),
+ 'message' => new external_value(PARAM_TEXT, 'Error message', VALUE_OPTIONAL),
+ ]);
+ }
+
+ /**
+ * Get the tasks progresses for a given course.
+ *
+ * @param int $courseid
+ * @param int $coursemoduleid
+ *
+ * @return array
+ */
+ public static function execute(int $courseid, int $coursemoduleid = 0) {
+ try {
+ // Validate parameters.
+ $params = self::validate_parameters(
+ self::execute_parameters(),
+ [
+ 'courseid' => $courseid,
+ 'coursemoduleid' => $coursemoduleid,
+ ]
+ );
+
+ // Get updates.
+ if (empty($assessments = manager::get_manager()
+ ->get_data_for_page_update($params['courseid'], $params['coursemoduleid']))) {
+ $assessments = [];
+ }
+
+ return [
+ 'success' => true,
+ 'assessments' => json_encode($assessments),
+ ];
+ } catch (\Exception $e) {
+ return [
+ 'success' => false,
+ 'message' => $e->getMessage(),
+ ];
+ }
+ }
+}
diff --git a/classes/external/get_transfer_students.php b/classes/external/get_transfer_students.php
new file mode 100644
index 0000000..1be6c08
--- /dev/null
+++ b/classes/external/get_transfer_students.php
@@ -0,0 +1,100 @@
+.
+namespace local_sitsgradepush\external;
+
+use context_course;
+use core_external\external_api;
+use core_external\external_function_parameters;
+use core_external\external_single_structure;
+use core_external\external_value;
+use local_sitsgradepush\logger;
+use local_sitsgradepush\manager;
+use local_sitsgradepush\taskmanager;
+
+/**
+ * External API for getting students information for an assessment mapping.
+ *
+ * @package local_sitsgradepush
+ * @copyright 2023 onwards University College London {@link https://www.ucl.ac.uk/}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author Alex Yeung
+ */
+class get_transfer_students extends external_api {
+ /**
+ * Returns description of method parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters() {
+ return new external_function_parameters([
+ 'assessmentmappingid' => new external_value(PARAM_INT, 'Assessment mapping ID', VALUE_REQUIRED),
+ ]);
+ }
+
+ /**
+ * Returns description of method result value.
+ *
+ * @return \external_single_structure
+ */
+ public static function execute_returns() {
+ return new external_single_structure([
+ 'success' => new external_value(PARAM_BOOL, 'Result of request', VALUE_REQUIRED),
+ 'students' => new external_value(PARAM_RAW, 'Students', VALUE_OPTIONAL),
+ 'message' => new external_value(PARAM_TEXT, 'Error message', VALUE_OPTIONAL),
+ ]);
+ }
+
+ /**
+ * Get students information for an assessment mapping
+ *
+ * @param int $assessmentmappingid
+ * @return array
+ */
+ public static function execute(int $assessmentmappingid) {
+ try {
+ $params = self::validate_parameters(
+ self::execute_parameters(),
+ [
+ 'assessmentmappingid' => $assessmentmappingid,
+ ]
+ );
+
+ $students = manager::get_manager()->get_students_in_assessment_mapping($params['assessmentmappingid']);
+ if (!empty($students)) {
+ $students = array_map(function ($student) {
+ return [
+ 'userid' => $student->userid,
+ 'lastgradepushresult' => $student->lastgradepushresult,
+ 'lastsublogpushresult' => $student->lastsublogpushresult,
+ ];
+ }, $students);
+ } else {
+ $students = [];
+ }
+
+ return [
+ 'success' => true,
+ 'students' => json_encode($students),
+ 'message' => 'Mark Transfer task completed successfully.',
+ ];
+ } catch (\Exception $e) {
+ return [
+ 'success' => false,
+ 'message' => $e->getMessage(),
+ ];
+ }
+ }
+}
diff --git a/classes/external/schedule_push_task.php b/classes/external/schedule_push_task.php
index 3e9e40e..5fc4924 100644
--- a/classes/external/schedule_push_task.php
+++ b/classes/external/schedule_push_task.php
@@ -15,12 +15,11 @@
// along with Moodle. If not, see .
namespace local_sitsgradepush\external;
-use context_course;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
-use local_sitsgradepush\manager;
+use local_sitsgradepush\taskmanager;
/**
* External API for scheduling a push task.
@@ -62,14 +61,13 @@ public static function execute_returns() {
* @return array
*/
public static function execute(int $assessmentmappingid) {
- global $USER;
try {
$params = self::validate_parameters(
self::execute_parameters(),
['assessmentmappingid' => $assessmentmappingid]
);
- $manager = manager::get_manager();
- $manager->schedule_push_task($params['assessmentmappingid']);
+
+ taskmanager::schedule_push_task($params['assessmentmappingid']);
return [
'success' => true,
diff --git a/classes/external/transfer_mark_for_student.php b/classes/external/transfer_mark_for_student.php
new file mode 100644
index 0000000..3bde340
--- /dev/null
+++ b/classes/external/transfer_mark_for_student.php
@@ -0,0 +1,93 @@
+.
+namespace local_sitsgradepush\external;
+
+use context_course;
+use core_external\external_api;
+use core_external\external_function_parameters;
+use core_external\external_single_structure;
+use core_external\external_value;
+use local_sitsgradepush\logger;
+use local_sitsgradepush\manager;
+use local_sitsgradepush\taskmanager;
+
+/**
+ * External API for transfer mark for a student.
+ *
+ * @package local_sitsgradepush
+ * @copyright 2023 onwards University College London {@link https://www.ucl.ac.uk/}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author Alex Yeung
+ */
+class transfer_mark_for_student extends external_api {
+ /**
+ * Returns description of method parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters() {
+ return new external_function_parameters([
+ 'assessmentmappingid' => new external_value(PARAM_INT, 'Assessment mapping ID', VALUE_REQUIRED),
+ 'userid' => new external_value(PARAM_INT, 'User ID', VALUE_REQUIRED),
+ ]);
+ }
+
+ /**
+ * Returns description of method result value.
+ *
+ * @return \external_single_structure
+ */
+ public static function execute_returns() {
+ return new external_single_structure([
+ 'success' => new external_value(PARAM_BOOL, 'Result of request', VALUE_REQUIRED),
+ 'message' => new external_value(PARAM_TEXT, 'Error message', VALUE_OPTIONAL),
+ ]);
+ }
+
+ /**
+ * Transfer marks and submission logs for a student.
+ *
+ * @param int $assessmentmappingid
+ * @param int $userid
+ *
+ * @return array
+ */
+ public static function execute(int $assessmentmappingid, int $userid) {
+ try {
+ $params = self::validate_parameters(
+ self::execute_parameters(),
+ [
+ 'assessmentmappingid' => $assessmentmappingid,
+ 'userid' => $userid,
+ ]
+ );
+
+ $manager = manager::get_manager();
+ $manager->push_grade_to_sits($params['assessmentmappingid'], $params['userid']);
+ $manager->push_submission_log_to_sits($params['assessmentmappingid'], $params['userid']);
+
+ return [
+ 'success' => true,
+ 'message' => 'Mark Transfer Successful',
+ ];
+ } catch (\Exception $e) {
+ return [
+ 'success' => false,
+ 'message' => $e->getMessage(),
+ ];
+ }
+ }
+}
diff --git a/classes/manager.php b/classes/manager.php
index 87b64ea..1b524c5 100644
--- a/classes/manager.php
+++ b/classes/manager.php
@@ -69,6 +69,9 @@ class manager {
/** @var string DB table for storing error log */
const TABLE_ERROR_LOG = 'local_sitsgradepush_err_log';
+ /** @var string DB table for storing push tasks */
+ const TABLE_TASKS = 'local_sitsgradepush_tasks';
+
/** @var string[] Fields mapping - Local DB component grades table fields to returning SITS fields */
const MAPPING_COMPONENT_GRADE = [
'modcode' => 'MOD_CODE',
@@ -86,21 +89,6 @@ class manager {
/** @var string[] Allowed activity types */
const ALLOWED_ACTIVITIES = ['assign', 'quiz', 'turnitintooltwo'];
- /** @var int Push task status - requested*/
- const PUSH_TASK_STATUS_REQUESTED = 0;
-
- /** @var int Push task status - queued*/
- const PUSH_TASK_STATUS_QUEUED = 1;
-
- /** @var int Push task status - processing*/
- const PUSH_TASK_STATUS_PROCESSING = 2;
-
- /** @var int Push task status - completed*/
- const PUSH_TASK_STATUS_COMPLETED = 3;
-
- /** @var int Push task status - failed*/
- const PUSH_TASK_STATUS_FAILED = -1;
-
/** @var null Manager instance */
private static $instance = null;
@@ -854,13 +842,17 @@ public function get_transfer_logs (int $assessmentmappingid, int $userid, string
/**
* Get the assessment data.
*
- * @param assessment $assessment
+ * @param int $coursemoduleid
+ * @param int|null $assessmentmappingid
* @return array
* @throws \coding_exception
* @throws \dml_exception
* @throws \moodle_exception
*/
- public function get_assessment_data(assessment $assessment): array {
+ public function get_assessment_data(int $coursemoduleid, int $assessmentmappingid = null): array {
+ // Get the assessment.
+ $assessment = assessmentfactory::get_assessment($coursemoduleid);
+
$assessmentdata = [];
$assessmentdata['studentsnotrecognized'] = [];
$students = $assessment->get_all_participants();
@@ -872,6 +864,13 @@ public function get_assessment_data(assessment $assessment): array {
return [];
}
+ // Only return the mapping that matches the assessment mapping ID if assessmentmappingid is given.
+ if ($assessmentmappingid) {
+ $mappings = array_filter($mappings, function ($mapping) use ($assessmentmappingid) {
+ return $mapping->id == $assessmentmappingid;
+ });
+ }
+
// Fetch students from SITS.
foreach ($mappings as $mapping) {
$mabkey = $mapping->mapcode . '-' . $mapping->mabseq;
@@ -1014,169 +1013,6 @@ public function get_moodle_ast_codes_work_with_exam_room_code(): bool|array {
return false;
}
- /**
- * Schedule push task.
- *
- * @param int $assessmentmappingid Assessment mapping id
- * @return bool|int
- * @throws \dml_exception
- */
- public function schedule_push_task(int $assessmentmappingid): bool|int {
- global $DB, $USER;
-
- // Check if the assessment mapping exists.
- $mapping = $DB->get_record(self::TABLE_ASSESSMENT_MAPPING, ['id' => $assessmentmappingid]);
- if (!$mapping) {
- throw new \moodle_exception('error:assessmentmapping', 'local_sitsgradepush', '', $assessmentmappingid);
- }
-
- // Check if there is already in one of the following status: added, queued, processing.
- if (self::get_pending_task_in_queue($mapping->id)) {
- throw new \moodle_exception('error:duplicatedtask', 'local_sitsgradepush');
- }
-
- // Check course module exists.
- if (!$DB->record_exists('course_modules', ['id' => $mapping->coursemoduleid])) {
- throw new \moodle_exception('error:coursemodulenotfound', 'local_sitsgradepush', '', $mapping->coursemoduleid);
- }
-
- // Check if the assessment component exists.
- if (!$DB->record_exists('local_sitsgradepush_mab', ['id' => $mapping->componentgradeid])) {
- throw new \moodle_exception('error:mab_not_found', 'local_sitsgradepush', '', $mapping->componentgradeid);
- }
-
- // Create and insert the task.
- $task = new \stdClass();
- $task->userid = $USER->id;
- $task->timescheduled = time();
- $task->assessmentmappingid = $assessmentmappingid;
-
- return $DB->insert_record('local_sitsgradepush_tasks', $task);
- }
-
- /**
- * Get push task in status requested, queued or processing for a course module.
- *
- * @param int $assessmentmappingid Assessment mapping id
- * @return \stdClass|bool false if no task found, otherwise return the task object with button label.
- * @throws \coding_exception|\dml_exception
- */
- public function get_pending_task_in_queue(int $assessmentmappingid): bool|\stdClass {
- global $DB;
-
- $sql = 'SELECT *
- FROM {local_sitsgradepush_tasks}
- WHERE assessmentmappingid = :assessmentmappingid AND status IN (:status1, :status2, :status3)
- ORDER BY id DESC';
- $params = [
- 'assessmentmappingid' => $assessmentmappingid,
- 'status1' => self::PUSH_TASK_STATUS_REQUESTED,
- 'status2' => self::PUSH_TASK_STATUS_QUEUED,
- 'status3' => self::PUSH_TASK_STATUS_PROCESSING,
- ];
-
- // Add button label to the task object.
- if ($result = $DB->get_record_sql($sql, $params)) {
- switch ($result->status) {
- case self::PUSH_TASK_STATUS_REQUESTED:
- $result->buttonlabel = get_string('task:status:requested', 'local_sitsgradepush');
- break;
- case self::PUSH_TASK_STATUS_QUEUED:
- $result->buttonlabel = get_string('task:status:queued', 'local_sitsgradepush');
- break;
- case self::PUSH_TASK_STATUS_PROCESSING:
- $result->buttonlabel = get_string('task:status:processing', 'local_sitsgradepush');
- break;
- }
-
- return $result;
- } else {
- return false;
- }
- }
-
- /**
- * Get last finished push task for a course module.
- *
- * @param int $assessmentmappingid Assessment mapping id
- * @return false|mixed Returns false if no task found, otherwise return the task object with status text.
- * @throws \dml_exception|\coding_exception
- */
- public function get_last_finished_push_task(int $assessmentmappingid): mixed {
- global $DB;
-
- // Get the last task for the course module.
- $sql = 'SELECT *
- FROM {local_sitsgradepush_tasks}
- WHERE assessmentmappingid = :assessmentmappingid AND status IN (:status1, :status2)
- ORDER BY id DESC
- LIMIT 1';
-
- $params = [
- 'assessmentmappingid' => $assessmentmappingid,
- 'status1' => self::PUSH_TASK_STATUS_COMPLETED,
- 'status2' => self::PUSH_TASK_STATUS_FAILED,
- ];
-
- // Add status text to the task object.
- if ($task = $DB->get_record_sql($sql, $params)) {
- switch ($task->status) {
- case self::PUSH_TASK_STATUS_COMPLETED:
- $task->statustext = get_string('task:status:completed', 'local_sitsgradepush');
- break;
- case self::PUSH_TASK_STATUS_FAILED:
- $task->statustext = get_string('task:status:failed', 'local_sitsgradepush');
- break;
- }
-
- return $task;
- } else {
- return false;
- }
- }
-
- /**
- * Returns number of running tasks.
- *
- * @return int
- * @throws \dml_exception
- */
- public function get_number_of_running_tasks(): int {
- global $DB;
- return $DB->count_records('local_sitsgradepush_tasks', ['status' => self::PUSH_TASK_STATUS_PROCESSING]);
- }
-
- /**
- * Returns number of pending tasks.
- *
- * @param int $status
- * @param int $limit
- * @return array
- * @throws \dml_exception
- */
- public function get_push_tasks(int $status, int $limit): array {
- global $DB;
- return $DB->get_records('local_sitsgradepush_tasks', ['status' => $status], 'timescheduled ASC', '*', 0, $limit);
- }
-
- /**
- * Update push task status.
- *
- * @param int $id
- * @param int $status
- * @param int|null $errlogid
- * @return void
- * @throws \dml_exception
- */
- public function update_push_task_status(int $id, int $status, int $errlogid = null): void {
- global $DB;
- $task = $DB->get_record('local_sitsgradepush_tasks', ['id' => $id]);
- $task->status = $status;
- $task->timeupdated = time();
- $task->errlogid = $errlogid;
- $DB->update_record('local_sitsgradepush_tasks', $task);
- }
-
/**
* Get user profile fields.
*
@@ -1352,6 +1188,11 @@ public function validate_component_grade(int $componentgradeid, int $coursemodul
throw new \moodle_exception('error:coursemodulenotfound', 'local_sitsgradepush', '', $coursemoduleid);
}
+ // Do not allow mapping activity which is not from current academic year.
+ if (!$this->is_current_academic_year_activity($coursemodule->course)) {
+ throw new \moodle_exception('error:pastactivity', 'local_sitsgradepush');
+ }
+
// A component grade can only be mapped to one activity, so there is only one mapping record for each component grade.
if (!empty($mapping = $this->is_component_grade_mapped($componentgradeid))) {
// Check if this mapping has grades pushed.
@@ -1400,7 +1241,7 @@ public function get_student_grade(int $coursemoduleid, int $userid, int $partid
}
// Get the assessment object.
- $assessment = assessmentfactory::get_assessment($coursemodule);
+ $assessment = assessmentfactory::get_assessment($coursemoduleid);
if (empty($assessment)) {
throw new \moodle_exception('error:assessmentnotfound', 'local_sitsgradepush', '', $coursemoduleid);
}
@@ -1430,6 +1271,71 @@ public function get_all_course_activities(int $courseid): array {
return $activities;
}
+ /**
+ * Get students in an assessment mapping eligible for marks transfer.
+ *
+ * @param $assessmentmappingid
+ * @return null
+ * @throws \coding_exception
+ * @throws \dml_exception
+ * @throws \moodle_exception
+ */
+ public function get_students_in_assessment_mapping($assessmentmappingid) {
+ global $DB;
+
+ if (!$assessmentmapping = $DB->get_record(self::TABLE_ASSESSMENT_MAPPING, ['id' => $assessmentmappingid])) {
+ throw new \moodle_exception('error:assessmentmapping', 'local_sitsgradepush', '', $assessmentmappingid);
+ }
+
+ $assessment = $this->get_assessment_data($assessmentmapping->coursemoduleid, $assessmentmappingid);
+ $mapping = reset($assessment['mappings']);
+
+ return !empty($mapping->students) ? $mapping->students : null;
+ }
+
+ /**
+ * Get data required for page update, e.g. progress bars, last transfer time.
+ *
+ * @param $courseid
+ * @param $couresmoduleid
+ * @return array
+ * @throws \coding_exception
+ * @throws \dml_exception
+ */
+ public function get_data_for_page_update($courseid, $couresmoduleid = 0) {
+ global $DB;
+ $results = [];
+
+ $conditions = [
+ 'courseid' => $courseid,
+ ];
+
+ if ($couresmoduleid) {
+ $conditions['coursemoduleid'] = $couresmoduleid;
+ }
+
+ // Get assessment mappings.
+ $mappings = $DB->get_records(self::TABLE_ASSESSMENT_MAPPING, $conditions);
+ if (empty($mappings)) {
+ return [];
+ }
+
+ foreach ($mappings as $mapping) {
+ // Check if there is a pending / running task for this mapping.
+ $task = taskmanager::get_pending_task_in_queue($mapping->id);
+ $result = new \stdClass();
+ $result->assessmentmappingid = $mapping->id;
+ $result->courseid = $courseid;
+ $result->coursemoduleid = $mapping->coursemoduleid;
+ $result->task = !empty($task) ? $task : null;
+ $result->transferrecords = $this->has_grades_pushed($mapping->id) ? 1 : 0;
+ $result->lasttransfertime = taskmanager::get_last_push_task_time($mapping->id);
+ $results[] = $result;
+ }
+
+ return $results;
+ }
+
/**
* Save transfer log.
*
diff --git a/classes/output/renderer.php b/classes/output/renderer.php
index d48ae50..e800ba5 100644
--- a/classes/output/renderer.php
+++ b/classes/output/renderer.php
@@ -16,9 +16,9 @@
namespace local_sitsgradepush\output;
-use local_sitsgradepush\assessment\assessment;
use local_sitsgradepush\errormanager;
use local_sitsgradepush\manager;
+use local_sitsgradepush\taskmanager;
use moodle_page;
use plugin_renderer_base;
@@ -196,8 +196,14 @@ public function render_dashboard(array $moduledeliveries, int $courseid) : strin
// Get the assessment mapping status.
if ($coursemodule = get_coursemodule_from_id('', $componentgrade->coursemoduleid)) {
+ $students = $this->manager->get_students_in_assessment_mapping($componentgrade->assessmentmappingid);
+ $numberofstudents = 0;
+ if (!empty($students)) {
+ $numberofstudents = count($students);
+ }
+
$assessmentmapping = new \stdClass();
- $assessmentmapping->info =
+ $assessmentmapping->numberofstudents = $numberofstudents;
$assessmentmapping->id = $componentgrade->assessmentmappingid;
$assessmentmapping->type = get_module_types_names()[$coursemodule->modname];
$assessmentmapping->name = $coursemodule->name;
@@ -366,7 +372,7 @@ private function get_label_html(int $errortype = null) : string {
private function get_last_push_task_time(int $assessmentmappingid) {
// Add last task details to the mapping if any.
$time = null;
- if ($lasttask = $this->manager->get_last_finished_push_task($assessmentmappingid)) {
+ if ($lasttask = taskmanager::get_last_finished_push_task($assessmentmappingid)) {
$time = get_string(
'label:lastpushtext',
'local_sitsgradepush', [
@@ -386,47 +392,22 @@ private function get_last_push_task_time(int $assessmentmappingid) {
* @throws \coding_exception
* @throws \dml_exception
*/
- private function get_assessment_mapping_status_icon(int $assessmentmappingid) {
- $manager = manager::get_manager();
-
- // Work out the status of this assessment mapping.
- if ($task = $manager->get_pending_task_in_queue($assessmentmappingid)) {
- $status = match (intval($task->status)) {
- manager::PUSH_TASK_STATUS_REQUESTED => self::PUSH_STATUS_ICON_REQUESTED,
- manager::PUSH_TASK_STATUS_QUEUED => self::PUSH_STATUS_ICON_QUEUED,
- manager::PUSH_TASK_STATUS_PROCESSING => self::PUSH_STATUS_ICON_PROCESSING,
- };
- } else {
- $status = $manager->has_grades_pushed($assessmentmappingid) ?
- self::PUSH_STATUS_ICON_HAS_PUSH_RECORDS : self::PUSH_STATUS_ICON_NO_PUSH_RECORDS;
- }
+ public function get_assessment_mapping_status_icon(int $assessmentmappingid) {
+ $status = manager::get_manager()->has_grades_pushed($assessmentmappingid) ?
+ self::PUSH_STATUS_ICON_HAS_PUSH_RECORDS : self::PUSH_STATUS_ICON_NO_PUSH_RECORDS;
$result = new \stdClass();
$result->status = $status;
switch ($status) {
- case self::PUSH_STATUS_ICON_REQUESTED:
- $result->statusicon = '';
- $result->statustext = get_string('task:status:requested', 'local_sitsgradepush');
- break;
- case self::PUSH_STATUS_ICON_QUEUED:
- $result->statusicon = '';
- $result->statustext = get_string('task:status:queued', 'local_sitsgradepush');
- break;
- case self::PUSH_STATUS_ICON_PROCESSING:
- $result->statusicon = '';
- $result->statustext = get_string('task:status:processing', 'local_sitsgradepush');
- break;
case self::PUSH_STATUS_ICON_HAS_PUSH_RECORDS:
- $result->statusicon = '';
+ $result->statusicon = '';
$result->statustext = get_string('pushrecordsexist', 'local_sitsgradepush');
break;
default:
- $result->statusicon = '';
+ $result->statusicon = '';
$result->statustext = get_string('pushrecordsnotexist', 'local_sitsgradepush');
break;
}
diff --git a/classes/task/adhoctask.php b/classes/task/adhoctask.php
index 7e7a77a..62b9857 100644
--- a/classes/task/adhoctask.php
+++ b/classes/task/adhoctask.php
@@ -17,9 +17,9 @@
namespace local_sitsgradepush\task;
use core\task\adhoc_task;
-use local_sitsgradepush\assessment\assessmentfactory;
use local_sitsgradepush\logger;
use local_sitsgradepush\manager;
+use local_sitsgradepush\taskmanager;
/**
* Ad-hoc task to push grades to SITS.
@@ -45,74 +45,22 @@ public function get_name() {
* Execute the task.
*/
public function execute() {
- global $DB;
try {
- // Get the manager.
- $manager = manager::get_manager();
-
// Get task data.
$data = $this->get_custom_data();
- // Get the task.
- if (!$task = $DB->get_record('local_sitsgradepush_tasks', ['id' => $data->taskid])) {
- throw new \moodle_exception('error:tasknotfound', 'local_sitsgradepush');
- }
-
- // Check assessment mapping exists.
- if (!$assessmentmapping = $DB->get_record('local_sitsgradepush_mapping', ['id' => $task->assessmentmappingid])) {
- throw new \moodle_exception('error:mappingnotfound', 'local_sitsgradepush');
- }
-
- // Get the course module.
- if (!$coursemodule = get_coursemodule_from_id(null, $assessmentmapping->coursemoduleid)) {
- throw new \moodle_exception(
- 'error:coursemodulenotfound', 'local_sitsgradepush', '', $assessmentmapping->coursemoduleid);
- }
-
- // Check the MAB exists.
- if (!$DB->get_record('local_sitsgradepush_mab', ['id' => $assessmentmapping->componentgradeid])) {
- throw new \moodle_exception('error:mab_not_found', 'local_sitsgradepush', '', $assessmentmapping->componentgradeid);
- }
-
- // Log start.
- mtrace(date('Y-m-d H:i:s', time()) . ' : ' . 'Processing push task [#' . $data->taskid . ']');
-
- // Update task status to processing.
- $manager->update_push_task_status($task->id, manager::PUSH_TASK_STATUS_PROCESSING);
-
- // Get assessment.
- $assessment = assessmentfactory::get_assessment($coursemodule);
- if ($assessmentdata = $manager->get_assessment_data($assessment)) {
- foreach ($assessmentdata['mappings'] as $mapping) {
- // If the mapping is not the assessment mapping stated in the task, skip it.
- if ($mapping->id != $assessmentmapping->id) {
- continue;
- }
-
- // Skip if there is no student in the mapping.
- if (empty($mapping->students)) {
- continue;
- }
-
- // Push grades for each student in the mapping.
- foreach ($mapping->students as $student) {
- $manager->push_grade_to_sits($mapping, $student->userid);
- $manager->push_submission_log_to_sits($mapping, $student->userid);
- }
- }
- }
-
- // Log complete.
- mtrace(date('Y-m-d H:i:s', time()) . ' : ' . 'Completed push task [#' . $data->taskid . ']');
-
- // Update task status.
- $manager->update_push_task_status($task->id, manager::PUSH_TASK_STATUS_COMPLETED);
+ // Run task.
+ taskmanager::run_task($data->taskid);
+ taskmanager::send_email_notification($data->taskid, true);
} catch (\Exception $e) {
// Log error.
$errlogid = logger::log('Push task failed: ' . $e->getMessage());
// Update task status.
- $manager->update_push_task_status($task->id, manager::PUSH_TASK_STATUS_FAILED, $errlogid);
+ taskmanager::update_task_status($data->taskid, taskmanager::PUSH_TASK_STATUS_FAILED, $errlogid);
+
+ // Email the user.
+ taskmanager::send_email_notification($data->taskid, false);
}
}
}
diff --git a/classes/task/pushtask.php b/classes/task/pushtask.php
index b4c6cc9..cd3da0d 100644
--- a/classes/task/pushtask.php
+++ b/classes/task/pushtask.php
@@ -17,8 +17,8 @@
namespace local_sitsgradepush\task;
use core\task\scheduled_task;
-use local_sitsgradepush\manager;
-use core\task\manager as task_manager;
+use core\task\manager as coretaskmanager;
+use local_sitsgradepush\taskmanager;
/**
* Scheduled task to process grade push requests and queue adhoc tasks.
@@ -48,16 +48,13 @@ public function get_name() : string {
* @return void
*/
public function execute() {
- // Get the manager.
- $manager = manager::get_manager();
-
// Get the number of concurrent tasks allowed.
if (!$concurrenttasksallowed = get_config('local_sitsgradepush', 'concurrent_running_tasks')) {
$concurrenttasksallowed = self::MAX_CONCURRENT_TASKS;
}
// Get the number of tasks currently running.
- $runningtasks = $manager->get_number_of_running_tasks();
+ $runningtasks = taskmanager::get_number_of_running_tasks();
if ($runningtasks >= $concurrenttasksallowed) {
// Too many tasks running, exit.
@@ -67,7 +64,8 @@ public function execute() {
}
// Get queued tasks.
- $tasks = $manager->get_push_tasks(manager::PUSH_TASK_STATUS_REQUESTED, $concurrenttasksallowed - $runningtasks);
+ $tasks = taskmanager::get_push_tasks(taskmanager::PUSH_TASK_STATUS_REQUESTED, $concurrenttasksallowed - $runningtasks);
+
if (empty($tasks)) {
// No tasks to run, exit.
mtrace(date('Y-m-d H:i:s', time()) . ' : ' .
@@ -75,6 +73,7 @@ public function execute() {
return;
}
+ // Get the number of tasks to run.
$count = $runningtasks + 1;
// Run the tasks.
@@ -87,10 +86,10 @@ public function execute() {
$adhoctask->set_custom_data([
'taskid' => $task->id,
]);
- task_manager::queue_adhoc_task($adhoctask);
+ coretaskmanager::queue_adhoc_task($adhoctask);
// Mark the task as queued.
- $manager->update_push_task_status($task->id, manager::PUSH_TASK_STATUS_QUEUED);
+ taskmanager::update_task_status($task->id, taskmanager::PUSH_TASK_STATUS_QUEUED);
$count++;
}
diff --git a/classes/taskmanager.php b/classes/taskmanager.php
new file mode 100644
index 0000000..ad4adce
--- /dev/null
+++ b/classes/taskmanager.php
@@ -0,0 +1,401 @@
+.
+
+namespace local_sitsgradepush;
+
+use context_user;
+
+/**
+ * Manager class which handles push task.
+ *
+ * @package local_sitsgradepush
+ * @copyright 2023 onwards University College London {@link https://www.ucl.ac.uk/}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author Alex Yeung
+ */
+class taskmanager {
+
+ /** @var int Push task status - requested */
+ const PUSH_TASK_STATUS_REQUESTED = 0;
+
+ /** @var int Push task status - queued */
+ const PUSH_TASK_STATUS_QUEUED = 1;
+
+ /** @var int Push task status - processing */
+ const PUSH_TASK_STATUS_PROCESSING = 2;
+
+ /** @var int Push task status - completed */
+ const PUSH_TASK_STATUS_COMPLETED = 3;
+
+ /** @var int Push task status - failed */
+ const PUSH_TASK_STATUS_FAILED = -1;
+
+ /**
+ * Run push task.
+ *
+ * @param int $taskid
+ * @throws \dml_exception|\moodle_exception
+ */
+ public static function run_task(int $taskid) {
+ global $DB;
+
+ $manager = manager::get_manager();
+
+ // Get the task.
+ if (!$task = $DB->get_record('local_sitsgradepush_tasks', ['id' => $taskid])) {
+ throw new \moodle_exception('error:tasknotfound', 'local_sitsgradepush');
+ }
+
+ // Check if the assessment mapping exists.
+ $assessmentmapping = $DB->get_record(manager::TABLE_ASSESSMENT_MAPPING, ['id' => $task->assessmentmappingid]);
+ if (!$assessmentmapping) {
+ throw new \moodle_exception('error:assessmentmapping', 'local_sitsgradepush', '', $task->assessmentmappingid);
+ }
+
+ // Asynchronous task.
+ if ($task->sync == 0) {
+ // Log start.
+ mtrace(date('Y-m-d H:i:s', time()) . ' : ' . 'Processing push task [#' . $taskid . ']');
+ }
+
+ // Update task status to processing.
+ self::update_task_status($task->id, self::PUSH_TASK_STATUS_PROCESSING);
+
+ // Get the assessment data.
+ if ($assessmentdata = $manager->get_assessment_data($assessmentmapping->coursemoduleid, $assessmentmapping->id)) {
+ // The first mapping is our mapping.
+ $mapping = reset($assessmentdata['mappings']);
+
+ if (!empty($mapping->students)) {
+ // Number of students in the mapping.
+ $numberofstudents = count($mapping->students);
+
+ // Update when progress is a multiple of 10%.
+ $progressincrement = 10;
+ $lastupdatedprogress = 0;
+
+ $i = 0;
+ // Push mark and submission log for each student in the mapping.
+ foreach ($mapping->students as $student) {
+ $manager->push_grade_to_sits($mapping, $student->userid);
+ $manager->push_submission_log_to_sits($mapping, $student->userid);
+ $i++;
+
+ // Calculate progress.
+ $progress = ($i) / $numberofstudents * 100;
+
+ // Check if progress has passed the next multiple of 10%.
+ if (floor($progress / $progressincrement) > floor($lastupdatedprogress / $progressincrement)) {
+ // Update database.
+ self::update_task_progress($task->id, floor($progress));
+ $lastupdatedprogress = $progress;
+ }
+ }
+ }
+ }
+
+ if ($task->sync == 0) {
+ // Log complete.
+ mtrace(date('Y-m-d H:i:s', time()) . ' : ' . 'Completed push task [#' . $taskid . ']');
+ }
+
+ // Update progress to 100%.
+ self::update_task_progress($task->id, 100);
+
+ // Update task status.
+ self::update_task_status($task->id, self::PUSH_TASK_STATUS_COMPLETED);
+ }
+
+ /**
+ * Update task progress.
+ *
+ * @param int $taskid
+ * @param int $progress
+ * @throws \dml_exception
+ */
+ public static function update_task_progress(int $taskid, int $progress) {
+ global $DB;
+ $DB->set_field('local_sitsgradepush_tasks', 'progress', $progress, ['id' => $taskid]);
+ }
+
+ /**
+ * Get number of running tasks.
+ *
+ * @param int $sync
+ * @return int
+ * @throws \dml_exception
+ */
+ public static function get_number_of_running_tasks($sync = 0): int {
+ global $DB;
+ return $DB->count_records('local_sitsgradepush_tasks', ['status' => self::PUSH_TASK_STATUS_PROCESSING, 'sync' => $sync]);
+ }
+
+ /**
+ * Get last finished push task for a course module.
+ *
+ * @param int $assessmentmappingid Assessment mapping id
+ * @return false|mixed Returns false if no task found, otherwise return the task object with status text.
+ * @throws \dml_exception|\coding_exception
+ */
+ public static function get_last_finished_push_task(int $assessmentmappingid): mixed {
+ global $DB;
+
+ // Get the last task for the course module.
+ $sql = 'SELECT *
+ FROM {' . manager::TABLE_TASKS . '}
+ WHERE assessmentmappingid = :assessmentmappingid AND status IN (:status1, :status2)
+ ORDER BY id DESC
+ LIMIT 1';
+
+ $params = [
+ 'assessmentmappingid' => $assessmentmappingid,
+ 'status1' => self::PUSH_TASK_STATUS_COMPLETED,
+ 'status2' => self::PUSH_TASK_STATUS_FAILED,
+ ];
+
+ // Add status text to the task object.
+ if ($task = $DB->get_record_sql($sql, $params)) {
+ switch ($task->status) {
+ case self::PUSH_TASK_STATUS_COMPLETED:
+ $task->statustext = get_string('task:status:completed', 'local_sitsgradepush');
+ break;
+ case self::PUSH_TASK_STATUS_FAILED:
+ $task->statustext = get_string('task:status:failed', 'local_sitsgradepush');
+ break;
+ }
+
+ return $task;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the last push task time.
+ *
+ * @param int $assessmentmappingid Assessment mapping ID
+ * @return string|null Last push task time
+ * @throws \coding_exception
+ * @throws \dml_exception
+ */
+ public static function get_last_push_task_time(int $assessmentmappingid) {
+ // Add last task details to the mapping if any.
+ $time = null;
+ if ($lasttask = self::get_last_finished_push_task($assessmentmappingid)) {
+ $time = get_string(
+ 'label:lastpushtext',
+ 'local_sitsgradepush', [
+ 'statustext' => $lasttask->statustext,
+ 'date' => date('d/m/Y', $lasttask->timeupdated),
+ 'time' => date('g:i:s a', $lasttask->timeupdated), ]);
+ }
+
+ return $time;
+ }
+
+ /**
+ * Returns push tasks for a given status.
+ *
+ * @param int $status
+ * @param int $limit
+ * @return array
+ * @throws \dml_exception
+ */
+ public static function get_push_tasks(int $status, int $limit = 0): array {
+ global $DB;
+ return $DB->get_records('local_sitsgradepush_tasks', ['status' => $status], 'timescheduled ASC', '*', 0, $limit);
+ }
+
+ /**
+ * Update task status.
+ *
+ * @param int $taskid
+ * @param int $status
+ * @param int|null $errlogid
+ * @throws \dml_exception
+ */
+ public static function update_task_status(int $taskid, int $status, int $errlogid = null) {
+ global $DB;
+
+ $task = $DB->get_record('local_sitsgradepush_tasks', ['id' => $taskid]);
+ $task->status = $status;
+ $task->timeupdated = time();
+ $task->errlogid = $errlogid;
+ $DB->update_record('local_sitsgradepush_tasks', $task);
+ }
+
+ /**
+ * Schedule push task.
+ *
+ * @param int $assessmentmappingid Assessment mapping id
+ * @return bool
+ * @throws \dml_exception
+ * @throws \moodle_exception
+ */
+ public static function schedule_push_task(int $assessmentmappingid) {
+ global $DB, $USER;
+
+ // Check if the assessment mapping exists.
+ $mapping = $DB->get_record(manager::TABLE_ASSESSMENT_MAPPING, ['id' => $assessmentmappingid]);
+ if (!$mapping) {
+ throw new \moodle_exception('error:assessmentmapping', 'local_sitsgradepush', '', $assessmentmappingid);
+ }
+
+ // Check if there is already in one of the following status: added, queued, processing.
+ if (self::get_pending_task_in_queue($mapping->id)) {
+ throw new \moodle_exception('error:duplicatedtask', 'local_sitsgradepush');
+ }
+
+ // Check course module exists.
+ if (!$DB->record_exists('course_modules', ['id' => $mapping->coursemoduleid])) {
+ throw new \moodle_exception('error:coursemodulenotfound', 'local_sitsgradepush', '', $mapping->coursemoduleid);
+ }
+
+ // Check if the assessment component exists.
+ if (!$DB->record_exists('local_sitsgradepush_mab', ['id' => $mapping->componentgradeid])) {
+ throw new \moodle_exception('error:mab_not_found', 'local_sitsgradepush', '', $mapping->componentgradeid);
+ }
+
+ // Create and insert the task.
+ $task = new \stdClass();
+ $task->userid = $USER->id;
+ $task->timescheduled = time();
+ $task->assessmentmappingid = $assessmentmappingid;
+ $task->status = self::PUSH_TASK_STATUS_REQUESTED;
+
+ // Check the number of students in the mapping.
+ $assessmentdata = manager::get_manager()->get_assessment_data($mapping->coursemoduleid, $mapping->id);
+ $mapping = reset($assessmentdata['mappings']);
+
+ // Check if the mapping has valid students for mark transfer.
+ if (empty($mapping->students)) {
+ throw new \moodle_exception('error:nostudentfoundformapping', 'local_sitsgradepush');
+ }
+
+ // Failed to insert the task.
+ if (!$DB->insert_record('local_sitsgradepush_tasks', $task)) {
+ throw new \moodle_exception('error:inserttask', 'local_sitsgradepush');
+ }
+
+ return true;
+ }
+
+ /**
+ * Get push task in status requested, queued or processing for a course module.
+ *
+ * @param int $assessmentmappingid Assessment mapping id
+ * @return \stdClass|bool false if no task found, otherwise return the task object with button label.
+ * @throws \coding_exception|\dml_exception
+ */
+ public static function get_pending_task_in_queue(int $assessmentmappingid): bool|\stdClass {
+ global $DB;
+
+ $sql = 'SELECT *
+ FROM {' . manager::TABLE_TASKS . '}
+ WHERE assessmentmappingid = :assessmentmappingid AND status IN (:status1, :status2, :status3)
+ ORDER BY id DESC';
+ $params = [
+ 'assessmentmappingid' => $assessmentmappingid,
+ 'status1' => self::PUSH_TASK_STATUS_REQUESTED,
+ 'status2' => self::PUSH_TASK_STATUS_QUEUED,
+ 'status3' => self::PUSH_TASK_STATUS_PROCESSING,
+ ];
+
+ // Add button label to the task object.
+ if ($result = $DB->get_record_sql($sql, $params)) {
+ switch ($result->status) {
+ case self::PUSH_TASK_STATUS_REQUESTED:
+ $result->buttonlabel = get_string('task:status:requested', 'local_sitsgradepush');
+ break;
+ case self::PUSH_TASK_STATUS_QUEUED:
+ $result->buttonlabel = get_string('task:status:queued', 'local_sitsgradepush');
+ break;
+ case self::PUSH_TASK_STATUS_PROCESSING:
+ $result->buttonlabel = get_string('task:status:processing', 'local_sitsgradepush');
+ break;
+ }
+
+ return $result;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Email user the result of the task.
+ *
+ * @param $taskid
+ * @param bool $success
+ * @return void
+ * @throws \coding_exception
+ * @throws \dml_exception
+ * @throws \moodle_exception
+ */
+ public static function send_email_notification($taskid, bool $success) : void {
+ global $DB, $PAGE;
+
+ // Define email object for the subject and content.
+ $email = new \stdClass();
+ $email->status = $success ? get_string('task:status:completed', 'local_sitsgradepush') :
+ get_string('task:status:failed', 'local_sitsgradepush');
+ $email->activityname = get_string('email:unknown', 'local_sitsgradepush');
+ $email->mab = get_string('email:unknown', 'local_sitsgradepush');
+
+ // Get the task first.
+ if (!$task = $DB->get_record(manager::TABLE_TASKS, ['id' => $taskid])) {
+ throw new \moodle_exception('error:tasknotfound', 'local_sitsgradepush');
+ }
+
+ // Get the content of the task.
+ $sql = 'SELECT am.id, am.coursemoduleid, CONCAT(cg.mapcode, "-", cg.mabseq) AS mab
+ FROM {' . manager::TABLE_ASSESSMENT_MAPPING . '} am
+ JOIN {' . manager::TABLE_COMPONENT_GRADE . '} cg ON am.componentgradeid = cg.id
+ WHERE am.id = :assessmentmappingid';
+
+ $params = [
+ 'assessmentmappingid' => $task->assessmentmappingid,
+ ];
+
+ // Task content found.
+ if ($result = $DB->get_record_sql($sql, $params)) {
+ $coursemodule = get_coursemodule_from_id(null, $result->coursemoduleid);
+ $email->mab = $result->mab;
+ $email->activityname = $coursemodule->name;
+ $url = new \moodle_url(
+ '/local/sitsgradepush/index.php',
+ ['id' => $coursemodule->id, 'modname' => $coursemodule->modname]
+ );
+ $email->link = $url->out(false);
+ }
+
+ // Email user the result of the task.
+ if ($success) {
+ $content = get_string('email:content:success', 'local_sitsgradepush', $email);
+ } else {
+ $content = get_string('email:content:fail', 'local_sitsgradepush', $email);
+ }
+
+ // Make status' first letter uppercase for subject.
+ $email->status = ucfirst($email->status);
+ $subject = get_string('email:subject', 'local_sitsgradepush', $email);
+
+ // Get the user who scheduled the task.
+ $user = $DB->get_record('user', ['id' => $task->userid]);
+
+ $PAGE->set_context(context_user::instance($user->id));
+ email_to_user($user, $user, $subject, $content);
+ }
+}
diff --git a/dashboard.php b/dashboard.php
index 40f9679..ef282e1 100644
--- a/dashboard.php
+++ b/dashboard.php
@@ -90,7 +90,15 @@
}
// Initialise the javascript.
-$PAGE->requires->js_call_amd('local_sitsgradepush/dashboard', 'init', []);
+$PAGE->requires->js_call_amd(
+ 'local_sitsgradepush/dashboard',
+ 'init',
+ [
+ $courseid,
+ get_config('local_sitsgradepush', 'sync_threshold'),
+ get_config('local_sitsgradepush', 'async'),
+ ]
+);
// Page footer.
echo $OUTPUT->footer();
diff --git a/db/install.xml b/db/install.xml
index c1d1c06..e3269d6 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -1,5 +1,5 @@
-
@@ -90,6 +90,7 @@
+
diff --git a/db/services.php b/db/services.php
index dcfae10..7717d83 100644
--- a/db/services.php
+++ b/db/services.php
@@ -41,6 +41,25 @@
'type' => 'write',
'loginrequired' => true,
],
+ 'local_sitsgradepush_get_transfer_students' => [
+ 'classname' => 'local_sitsgradepush\external\get_transfer_students',
+ 'description' => 'Get transfer students for a given assessment mapping',
+ 'ajax' => true,
+ 'type' => 'read',
+ 'loginrequired' => true,
+ ],
+ 'local_sitsgradepush_transfer_mark_for_student' => [
+ 'classname' => 'local_sitsgradepush\external\transfer_mark_for_student',
+ 'description' => 'Transfer mark for a given student',
+ 'ajax' => true,
+ 'type' => 'write',
+ 'loginrequired' => true,
+ ],
+ 'local_sitsgradepush_get_assessments_update' => [
+ 'classname' => 'local_sitsgradepush\external\get_assessments_update',
+ 'description' => 'Get assessment updates for a given course / course module',
+ 'ajax' => true,
+ 'type' => 'read',
+ 'loginrequired' => true,
+ ],
];
-
-
diff --git a/db/upgrade.php b/db/upgrade.php
index dee807f..f4707a1 100644
--- a/db/upgrade.php
+++ b/db/upgrade.php
@@ -367,5 +367,19 @@ function xmldb_local_sitsgradepush_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2023112400, 'local', 'sitsgradepush');
}
+ if ($oldversion < 2023121800) {
+ // Define field progress to be added to local_sitsgradepush_tasks.
+ $table = new xmldb_table('local_sitsgradepush_tasks');
+ $field = new xmldb_field('progress', XMLDB_TYPE_INTEGER, '3', null, null, null, null, 'assessmentmappingid');
+
+ // Conditionally launch add field progress.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Sitsgradepush savepoint reached.
+ upgrade_plugin_savepoint(true, 2023121800, 'local', 'sitsgradepush');
+ }
+
return true;
}
diff --git a/index.php b/index.php
index d4f036f..be542c2 100644
--- a/index.php
+++ b/index.php
@@ -78,11 +78,9 @@
echo '' . get_string('index:header', 'local_sitsgradepush') . '
';
$manager = manager::get_manager();
-// Get assessment.
-$assessment = assessmentfactory::get_assessment($coursemodule);
// Get page content.
-$content = $manager->get_assessment_data($assessment);
+$content = $manager->get_assessment_data($coursemoduleid);
$mappingids = [];
@@ -108,7 +106,7 @@
}
// Refresh data after completed all pushes.
- $content = $manager->get_assessment_data($assessment);
+ $content = $manager->get_assessment_data($coursemoduleid);
$buttonlabel = get_string('label:ok', 'local_sitsgradepush');
} else {
$url->param('pushgrade', 1);
@@ -144,7 +142,7 @@
echo '';
// Initialize javascript.
-$PAGE->requires->js_call_amd('local_sitsgradepush/sitsgradepush', 'init', [$coursemoduleid, $mappingids]);
+$PAGE->requires->js_call_amd('local_sitsgradepush/sitsgradepush', 'init', [$coursemodule->course, $coursemoduleid, $mappingids]);
// And the page footer.
echo $OUTPUT->footer();
diff --git a/lang/en/local_sitsgradepush.php b/lang/en/local_sitsgradepush.php
index 17cf301..7dafb56 100644
--- a/lang/en/local_sitsgradepush.php
+++ b/lang/en/local_sitsgradepush.php
@@ -48,6 +48,8 @@
$string['settings:concurrenttasks:desc'] = 'Number of concurrent ad-hoc tasks allowed';
$string['settings:userprofilefield'] = 'User Profile Field';
$string['settings:userprofilefield:desc'] = 'User profile field for export staff';
+$string['settings:sync_threshold'] = 'Sync Threshold';
+$string['settings:sync_threshold:desc'] = 'The threshold to allow for running synchronous mark transfer task';
$string['label:gradepushassessmentselect'] = 'Select SITS assessment to link to';
$string['label:jumpto'] = 'Jump to: ';
$string['label:pushall'] = 'Transfer All';
@@ -114,7 +116,7 @@
$string['error:mapassessment'] = 'You do not have permission to map assessment.';
$string['error:pushgradespermission'] = 'You do not have permission to transfer marks.';
$string['error:nostudentgrades'] = 'No student marks found.';
-$string['error:nostudentfoundformapping'] = 'No student found for this assessment component.';
+$string['error:nostudentfoundformapping'] = 'No students found for this assessment component.';
$string['error:emptyresponse'] = 'Empty response received when calling {$a}.';
$string['error:turnitin_numparts'] = 'Turnitin assignment with multiple parts is not supported by Marks Transfer.';
$string['error:duplicatedtask'] = 'There is already a transfer task in queue / processing for this assessment mapping.';
@@ -131,6 +133,7 @@
$string['error:no_update_for_same_mapping'] = 'Nothing to update as the assessment component is already mapped to this activity.';
$string['error:same_map_code_for_same_activity'] = 'An activity cannot be mapped to more than one assessment component with same map code';
$string['error:missingparams'] = 'Missing parameters.';
+$string['error:inserttask'] = 'Failed to insert task.';
$string['form:alert_no_mab_found'] = 'No assessment components found';
$string['form:info_turnitin_numparts'] = 'Please note Turnitin assignment with multiple parts is not supported by Marks Transfer.';
@@ -168,3 +171,9 @@
$string['privacy:metadata:local_sitsgradepush_tasks:userid'] = 'The user who requested the transfer task.';
$string['privacy:metadata:local_sitsgradepush_tasks:status'] = 'The status of the transfer task.';
$string['privacy:metadata:local_sitsgradepush_tasks:info'] = 'Additional information about the transfer task.';
+
+// Email strings.
+$string['email:subject'] = 'Marks Transfer Task {$a->status}: {$a->activityname} - {$a->mab}';
+$string['email:content:success'] = 'Marks Transfer Task from {$a->activityname} to {$a->mab} has been {$a->status}.
You can check the marks transfer history for this task here: {$a->activityname} - {$a->mab}';
+$string['email:content:fail'] = 'Marks Transfer Task from {$a->activityname} to {$a->mab} has been {$a->status}.
Please try again later.';
+$string['email:unknown'] = 'unknown';
diff --git a/settings.php b/settings.php
index a6ecb40..3b8dfb7 100644
--- a/settings.php
+++ b/settings.php
@@ -57,6 +57,14 @@
0
));
+ // Threshold to allow for synchronous mark transfer.
+ $settings->add(new admin_setting_configtext('local_sitsgradepush/sync_threshold',
+ get_string('settings:sync_threshold', 'local_sitsgradepush'),
+ get_string('settings:sync_threshold:desc', 'local_sitsgradepush'),
+ 30,
+ PARAM_INT
+ ));
+
// Setting to enable/disable submission log push.
$settings->add(new admin_setting_configcheckbox(
'local_sitsgradepush/sublogpush',
diff --git a/styles.css b/styles.css
index 9edce51..d0e208c 100644
--- a/styles.css
+++ b/styles.css
@@ -2,6 +2,19 @@
text-align: center;
}
+#dashboard-progress-bar-sync {
+ height: 20px;
+}
+
+.sitsgradepush-history-table .progress {
+ height: 15px;
+ width: 150px;
+}
+
+.sitsgradepush-history-table .spinner-border {
+ margin-right: 10px;
+}
+
.sitsgradepush-dasboard .selector-container {
margin-bottom: 20px;
}
@@ -52,6 +65,10 @@
background-color: #0056b3; /* Hover background color */
}
+.sitsgradepush-dasboard .progress {
+ margin: 10px 5px 0px 5px;
+}
+
/* Styles for select source page */
/* Styles scoped to cards within a specific .sitsgradepush-select-source container */
.sitsgradepush-select-source .card-custom {
diff --git a/templates/assessmentgrades.mustache b/templates/assessmentgrades.mustache
index 49b3177..0a6c4d0 100644
--- a/templates/assessmentgrades.mustache
+++ b/templates/assessmentgrades.mustache
@@ -46,15 +46,17 @@
}]
}
}}
-
+
{{tabletitle}}
{{#additionalinfo}}
- {{{taskstatus}}}
- {{lasttask}}
+ {{lasttask}}
+
|
diff --git a/templates/module_delivery_table.mustache b/templates/module_delivery_table.mustache
index af95797..b743411 100644
--- a/templates/module_delivery_table.mustache
+++ b/templates/module_delivery_table.mustache
@@ -135,6 +135,7 @@
title="{{#str}} dashboard:transfermark, local_sitsgradepush {{/str}}"
{{#assessmentmapping}}
data-assessmentmappingid="{{id}}"
+ data-numberofstudents="{{numberofstudents}}"
{{/assessmentmapping}}
{{disablepushgradebutton}}>
@@ -145,7 +146,7 @@
{{{statusicon}}}
{{/assessmentmapping}}
{{^assessmentmapping}}
-
{{/assessmentmapping}}
diff --git a/version.php b/version.php
index 8cc5bba..ce84cf2 100644
--- a/version.php
+++ b/version.php
@@ -27,7 +27,7 @@
$plugin->component = 'local_sitsgradepush';
$plugin->release = '0.1.0';
-$plugin->version = 2023112400;
+$plugin->version = 2023121800;
$plugin->requires = 2021051708;
$plugin->maturity = MATURITY_ALPHA;
$plugin->dependencies = [