From 982fed112f5a45aef78c5d237447c25df97fbfa6 Mon Sep 17 00:00:00 2001 From: Alex Yeung Date: Mon, 18 Dec 2023 15:11:54 +0000 Subject: [PATCH] CTP-2811 Improve UX when push is taking place --- amd/build/dashboard.min.js | 2 +- amd/build/dashboard.min.js.map | 2 +- amd/build/progress.min.js | 3 + amd/build/progress.min.js.map | 1 + amd/build/sitsgradepush.min.js | 2 +- amd/build/sitsgradepush.min.js.map | 2 +- amd/build/sitsgradepush_helper.min.js | 2 +- amd/build/sitsgradepush_helper.min.js.map | 2 +- amd/src/dashboard.js | 465 ++++++++++++++++-- amd/src/progress.js | 59 +++ amd/src/sitsgradepush.js | 137 +++++- amd/src/sitsgradepush_helper.js | 82 ++- apiclients/easikit/classes/webclient.php | 3 + classes/assessment/assessmentfactory.php | 11 +- classes/cachemanager.php | 3 + classes/external/get_assessments_update.php | 94 ++++ classes/external/get_transfer_students.php | 97 ++++ classes/external/schedule_push_task.php | 8 +- .../external/transfer_mark_for_student.php | 107 ++++ classes/manager.php | 302 +++++------- classes/output/renderer.php | 88 +--- classes/task/adhoctask.php | 68 +-- classes/task/pushtask.php | 17 +- classes/taskmanager.php | 401 +++++++++++++++ dashboard.php | 11 +- db/caches.php | 5 + db/install.xml | 3 +- db/services.php | 23 +- db/upgrade.php | 14 + index.php | 8 +- lang/en/local_sitsgradepush.php | 12 +- settings.php | 8 + styles.css | 17 + templates/assessmentgrades.mustache | 8 +- templates/module_delivery_table.mustache | 16 +- version.php | 2 +- 36 files changed, 1670 insertions(+), 415 deletions(-) create mode 100644 amd/build/progress.min.js create mode 100644 amd/build/progress.min.js.map create mode 100644 amd/src/progress.js create mode 100644 classes/external/get_assessments_update.php create mode 100644 classes/external/get_transfer_students.php create mode 100644 classes/external/transfer_mark_for_student.php create mode 100644 classes/taskmanager.php diff --git a/amd/build/dashboard.min.js b/amd/build/dashboard.min.js index 9b91061..a31e79f 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='";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","core/str"],(function(_exports,_sitsgradepush_helper,_progress,_notification,_modal_factory,_modal_events,_str){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 showErrorMessageForButton(button,result.message);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 null!==assessmentmappingid?(pushButton.innerHTML='',pushButton.disabled=!1):pushButton.disabled=!0}))}(assessmentIds)}(assessments),function(assessments){let pushButtons=document.querySelectorAll(".push-mark-button"),assessmentsHasTransferRecords=assessments.filter((update=>1===update.transferrecords)),assessmentIds=new Set(assessmentsHasTransferRecords.map((assessment=>assessment.assessmentmappingid)));pushButtons.forEach((function(button){let assessmentmappingid=button.getAttribute("data-assessmentmappingid"),icon=button.parentNode.parentNode.querySelector(".records-icon");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")):icon.classList.contains("fa-file-lines")&&(icon.classList.replace("fa-regular","fa-solid"),icon.classList.replace("fa-file-lines","fa-circle-info"))}))}(assessments))}else clearInterval(updatePageIntervalId),window.console.error(update.message)}function showErrorMessageForButton(button,message){let errormessagerow=document.createElement("tr");errormessagerow.setAttribute("class","error-message-row"),errormessagerow.innerHTML='";let currentrow=button.closest("tr");null!==currentrow.nextElementSibling&¤trow.nextElementSibling.classList.contains("error-message-row")&¤trow.nextElementSibling.remove(),currentrow.insertAdjacentElement("afterend",errormessagerow)}function getPagePosition(page){return page instanceof Window?page.scrollY:page.scrollTop}_exports.init=(courseid,syncThresholdConfig,asyncConfig,moodleVersion)=>{let page;!function(){let successMessage=localStorage.getItem("successMessage");successMessage&&(_notification.default.addNotification({message:successMessage,type:"success"}),localStorage.removeItem("successMessage"))}(),syncThreshold=syncThresholdConfig,globalCourseid=courseid,async=asyncConfig,page=moodleVersion>"2023100900"?window:document.getElementById("page");let tableSelector=function(page){let tableSelector=document.getElementById("module-delivery-selector");return tableSelector.addEventListener("change",(function(){let selectedTable=document.getElementById(tableSelector.value),pagePosition=getPagePosition(page);if(selectedTable){let offset=-100,scrollPosition=pagePosition+selectedTable.getBoundingClientRect().top+offset;page.scrollTo({top:scrollPosition,behavior:"smooth"})}})),tableSelector}(page);!function(page,tableSelector){let backToTopButton=document.getElementById("backToTopButton");page.addEventListener("scroll",(function(){getPagePosition(page)>=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");mabpushbuttons.length>0&&mabpushbuttons.forEach((function(button){button.addEventListener("click",(async function(){let studentcount=button.getAttribute("data-numberofstudents"),assessmentmappingid=button.getAttribute("data-assessmentmappingid");if(null!==assessmentmappingid)if("0"!==studentcount)if(window.console.log("studentCount: "+studentcount),window.console.log("syncThreshold: "+syncThreshold),studentcount>0&&studentcount0&&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){let generalErrorMessage=await(0,_str.get_string)("error:sync_partially_failed","local_sitsgradepush");document.getElementById("error-message-modal-sync").innerHTML='",window.console.error(promise.message)}promises.push(promise),count+=1;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)}else showErrorMessageForButton(button,"There are no marks to transfer.");else button.disabled=!0}))}))}(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..e33b373 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';\nimport {get_string as getString} from 'core/str';\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 * @param {string} moodleVersion\n */\nexport const init = (courseid, syncThresholdConfig, asyncConfig, moodleVersion) => {\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 let page;\n\n // Get the scrollable page element depending on the Moodle version.\n if (moodleVersion > '2023100900') {\n // Moodle 4.3 and above.\n page = window;\n } else {\n // Moodle 4.2 and below.\n page = document.getElementById(\"page\");\n }\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 * @return {HTMLElement}\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 // Get the scroll position of the page.\n let pagePosition = getPagePosition(page);\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 = pagePosition + 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 // Get the scroll position of the page.\n if (getPagePosition(page) >= 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\");\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 // Disable the button if there is no assessment mapping ID.\n if (assessmentmappingid === null) {\n button.disabled = true;\n return;\n }\n\n if (studentcount === '0') {\n // Show an error message if there is no student to push grades.\n showErrorMessageForButton(button, 'There are no marks to transfer.');\n return;\n }\nwindow.console.log('studentCount: ' + studentcount);\nwindow.console.log('syncThreshold: ' + syncThreshold);\n if (studentcount > 0 && studentcount < syncThreshold) {\n window.console.log('sync');\n } else {\n window.console.log('async');\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 > 0 && 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|boolean} 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 // Show an error message if the transfer task is not successfully scheduled.\n showErrorMessageForButton(button, result.message);\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 {Promise}\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 The assessment mapping IDs having task in progress.\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 if (assessmentmappingid !== null) {\n // Reset the button to the original state.\n pushButton.innerHTML = '';\n pushButton.disabled = false;\n } else {\n // No assessment mapping, disable the button.\n pushButton.disabled = true;\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 pushButtons = document.querySelectorAll('.push-mark-button');\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 pushButtons.forEach(function(button) {\n let assessmentmappingid = button.getAttribute('data-assessmentmappingid');\n let icon = button.parentNode.parentNode.querySelector('.records-icon');\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 } else {\n if (icon.classList.contains('fa-file-lines')) {\n icon.classList.replace('fa-regular', 'fa-solid');\n icon.classList.replace('fa-file-lines', 'fa-circle-info');\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 // Get general error message.\n let generalErrorMessage = await getString('error:sync_partially_failed', 'local_sitsgradepush');\n let errormessage = document.getElementById('error-message-modal-sync');\n errormessage.innerHTML = '
' + generalErrorMessage + '
';\n window.console.error(promise.message);\n }\n\n promises.push(promise);\n\n // Increment the count by 1 for each student.\n count = count + 1;\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 * Show an error message at the table row under the button.\n *\n * @param {HTMLElement} button\n * @param {string} message\n */\nfunction showErrorMessageForButton(button, message) {\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 '
' + 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/**\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\n/**\n * Get the scroll position of the page.\n *\n * @param {HTMLElement} page\n * @return {*|number}\n */\nfunction getPagePosition(page) {\n if (page instanceof Window) {\n // Get the scroll position of the page.\n return page.scrollY;\n } else {\n // Get the scroll position of the page.\n return page.scrollTop;\n }\n}\n"],"names":["updatePageIntervalId","syncThreshold","globalCourseid","async","pushMarks","button","assessmentmappingid","getAttribute","result","success","tooltipid","document","getElementById","remove","showErrorMessageForButton","message","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","insertAdjacentElement","updateProgressBars","pushButton","spinner","innerHTML","outerHTML","disabled","updatePushButtons","updateTasksProgresses","pushButtons","assessmentsHasTransferRecords","transferrecords","icon","classList","contains","replace","updateIcon","clearInterval","errormessagerow","createElement","setAttribute","currentrow","closest","nextElementSibling","getPagePosition","page","Window","scrollY","scrollTop","syncThresholdConfig","asyncConfig","moodleVersion","successMessage","localStorage","getItem","addNotification","type","removeItem","displayNotification","tableSelector","addEventListener","selectedTable","value","pagePosition","offset","scrollPosition","getBoundingClientRect","top","scrollTo","behavior","initModuleDeliverySelector","backToTopButton","style","display","selectedIndex","initBackToTopButton","changesourcebuttons","location","href","initChangeSourceButtons","mabpushbuttons","studentcount","log","students","progressbar","modal","ModalFactory","create","types","ALERT","title","body","buttons","show","isModalVisible","modalProgressbar","getRoot","on","ModalEvents","hidden","destroy","count","promises","student","promise","userid","generalErrorMessage","push","Math","round","Promise","all","setButtonText","setInterval","syncMarksTransfer","this","initPushMarkButtons","total","then","catch","notification","initPushAllButton"],"mappings":"skBAYIA,qBAAuB,KACvBC,cAAgB,GAChBC,eAAiB,KAEjBC,MAAQ,oBAgQGC,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,cAIvCC,0BAA0BT,OAAQG,OAAOO,gBAGtCP,OACT,MAAOQ,cACLC,OAAOC,QAAQF,MAAMA,QACd,kBAWAG,kBAAkBC,cAEzBC,aAAe,8CAAqBD,aAEpCC,OAAOZ,QAAS,KAEZa,YAAcC,KAAKC,MAAMH,OAAOC,aAEhCA,YAAYG,OAAS,aAmBFH,iBAEvBI,oBAAsBJ,YAAYK,QAAOC,YAAkC,OAApBA,WAAWC,OAGlEC,cAAgB,IAAIC,IAAIL,oBAAoBM,KAAIC,MAAQA,KAAK3B,iCAezCoB,oBAAqBI,eAC1BnB,SAASuB,iBAAiB,mBAGhCC,SAAQC,cACZN,cAAcO,IAAID,YAAY7B,aAAa,8BAC5C6B,YAAYvB,YAIpBa,oBAAoBS,SAAQP,iBACpBU,cAAgB,gBAAkBV,WAAWC,KAAKvB,oBAClD8B,YAAczB,SAASC,eAAe0B,eACtCT,KAAOD,WAAWC,QAGjBO,4CAKiBA,YAAaP,KAAKU,cALtB,CACdH,aAAc,+BAAkBE,cAAe,QAAST,KAAKvB,oBAAqBuB,KAAKU,UAC1E5B,SAAS6B,cAAc,+CAAiDX,KAAKvB,oBAAsB,MACzGmC,WAAWA,WAAWC,sBAAsB,WAAYN,kBA/BvEO,CAAmBjB,oBAAqBI,wBA2CjBA,eAELnB,SAASuB,iBAAiB,qBAEhCC,SAAQ,SAASS,gBACrBtC,oBAAsBsC,WAAWrC,aAAa,+BAE9CuB,cAAcO,IAAI/B,qBAAsB,KAEpCuC,SAAU,2BAAc,aAAc,qBAC1CD,WAAWE,UAAYD,QAAQE,UAC/BH,WAAWI,UAAW,OACS,OAAxB1C,qBAEPsC,WAAWE,UAAY,qCACvBF,WAAWI,UAAW,GAGtBJ,WAAWI,UAAW,KA1D9BC,CAAkBnB,eA5BVoB,CAAsB5B,sBAgGdA,iBACZ6B,YAAcxC,SAASuB,iBAAiB,qBAGxCkB,8BAAgC9B,YAAYK,QAAON,QAAqC,IAA3BA,OAAOgC,kBAGpEvB,cAAgB,IAAIC,IAAIqB,8BAA8BpB,KAAIJ,YAAcA,WAAWtB,uBAGvF6C,YAAYhB,SAAQ,SAAS9B,YACrBC,oBAAsBD,OAAOE,aAAa,4BAC1C+C,KAAOjD,OAAOoC,WAAWA,WAAWD,cAAc,iBAClDV,cAAcO,IAAI/B,qBACdgD,KAAKC,UAAUC,SAAS,oBACxBF,KAAKC,UAAUE,QAAQ,WAAY,cACnCH,KAAKC,UAAUE,QAAQ,iBAAkB,kBAGzCH,KAAKC,UAAUC,SAAS,mBACxBF,KAAKC,UAAUE,QAAQ,aAAc,YACrCH,KAAKC,UAAUE,QAAQ,gBAAiB,sBAlH5CC,CAAWpC,mBAIfqC,cAAc3D,sBACdiB,OAAOC,QAAQF,MAAMK,OAAON,kBAsM3BD,0BAA0BT,OAAQU,aAEnC6C,gBAAkBjD,SAASkD,cAAc,MAG7CD,gBAAgBE,aAAa,QAAS,qBACtCF,gBAAgBd,UACZ,iEACkD/B,QADlD,kBAKAgD,WAAa1D,OAAO2D,QAAQ,MAGM,OAAlCD,WAAWE,oBACXF,WAAWE,mBAAmBV,UAAUC,SAAS,sBACjDO,WAAWE,mBAAmBpD,SAIlCkD,WAAWrB,sBAAsB,WAAYkB,0BA6BxCM,gBAAgBC,aACjBA,gBAAgBC,OAETD,KAAKE,QAGLF,KAAKG,wBAxiBA,CAAClD,SAAUmD,oBAAqBC,YAAaC,qBAazDN,qBAggBAO,eAAiBC,aAAaC,QAAQ,kBAGtCF,uCAEaG,gBAAgB,CACzB9D,QAAS2D,eACTI,KAAM,YAIVH,aAAaI,WAAW,mBAthB5BC,GAGA/E,cAAgBsE,oBAGhBrE,eAAiBkB,SAGjBjB,MAAQqE,YAOJL,KAFAM,cAAgB,aAETxD,OAGAN,SAASC,eAAe,YAI/BqE,uBA+B4Bd,UAE5Bc,cAAgBtE,SAASC,eAAe,mCAG5CqE,cAAcC,iBAAiB,UAAU,eAEjCC,cAAgBxE,SAASC,eAAeqE,cAAcG,OAGtDC,aAAenB,gBAAgBC,SAG/BgB,cAAe,KACXG,QAAU,IAEVC,eAAiBF,aADDF,cAAcK,wBAAwBC,IACNH,OAGpDnB,KAAKuB,SAAS,CACVD,IAAKF,eACLI,SAAU,eAKfV,cAzDaW,CAA2BzB,gBAkEtBA,KAAMc,mBAE3BY,gBAAkBlF,SAASC,eAAe,mBAG9CuD,KAAKe,iBAAiB,UAAU,WAExBhB,gBAAgBC,OAAS,IACzB0B,gBAAgBC,MAAMC,QAAU,QAEhCF,gBAAgBC,MAAMC,QAAU,UAKxCF,gBAAgBX,iBAAiB,SAAS,WACtCf,KAAKuB,SAAS,CAACD,IAAK,EAAGE,SAAU,WACjCV,cAAce,cAAgB,KAhFlCC,CAAoB9B,KAAMc,8BA0FtBiB,oBAAsBvF,SAASuB,iBAAiB,yCAIhDgE,oBAAoBzE,OAAS,GAC7ByE,oBAAoB/D,SAAQ,SAAS9B,QACjCA,OAAO6E,iBAAiB,SAAS,WAE7BjE,OAAOkF,SAASC,KAAO/F,OAAOE,aAAa,kBA/FvD8F,YA2GyBlC,KAAM/C,cAE3BkF,eAAiB3F,SAASuB,iBAAiB,qBAE3CoE,eAAe7E,OAAS,GAExB6E,eAAenE,SAAQ,SAAS9B,QAC5BA,OAAO6E,iBAAiB,SAAS/E,qBAEzBoG,aAAelG,OAAOE,aAAa,yBACnCD,oBAAsBD,OAAOE,aAAa,+BAGlB,OAAxBD,uBAKiB,MAAjBiG,gBAKpBtF,OAAOC,QAAQsF,IAAI,iBAAmBD,cACtCtF,OAAOC,QAAQsF,IAAI,kBAAoBvG,eACnBsG,aAAe,GAAKA,aAAetG,cACnCgB,OAAOC,QAAQsF,IAAI,QAEnBvF,OAAOC,QAAQsF,IAAI,SAIlBD,aAAe,GAAKA,aAAetG,eAA4B,MAAVE,2BAyPzCG,qBAE7BqD,cAAc3D,0BAGVQ,aAAe,6CAAoBF,wBAEnCE,OAAOC,SACHD,OAAOiG,SAAShF,OAAS,EAAG,KACxBiF,aACA,+BAAkB,8BAA+B,OAAQpG,oBAAqB,GAAG,GAGjFqG,YAAcC,uBAAaC,OAAO,CAClC/B,KAAM8B,uBAAaE,MAAMC,MACzBC,MAAO,qBACPC,KAAM,4CAA8CP,YAAY3D,UAChEmE,QAAS,QAAW,kBAGlBP,MAAMQ,WACRC,gBAAiB,EACjBC,iBAAmB1G,SAASC,eAAe,+BAG/C+F,MAAMW,UAAUC,GAAGC,sBAAYC,QAAQ,KACnCd,MAAMe,UACNN,gBAAiB,SAGjBX,SAAWlF,KAAKC,MAAMhB,OAAOiG,UAE7BF,aAAeE,SAAShF,OACxBkG,MAAQ,EACRC,SAAW,OACV,MAAMC,WAAWpB,SAAU,KAEvBW,yBAKDU,cAAgB,gDAAuBxH,oBAAqBuH,QAAQE,YACnED,QAAQrH,QAAS,KAEduH,0BAA4B,mBAAU,8BAA+B,uBACtDrH,SAASC,eAAe,4BAC9BkC,UAAY,iDAAmDkF,oBAAsB,SAClG/G,OAAOC,QAAQF,MAAM8G,QAAQ/G,SAGjC6G,SAASK,KAAKH,SAGdH,OAAgB,MAGZpF,SAAW2F,KAAKC,MAAOR,MAAQpB,aAAgB,qCACjCc,iBAAkB9E,UAAU,SAE5C6F,QAAQC,IAAIT,gBACZjB,MAAM2B,cAAc,SAAU,SAK5CtI,qBAAuBuI,aAAY,KAC/BpH,kBAAkBjB,kBAjfJ,MAsLIsI,CAAkBlI,yBACrB,QAEgBF,UAAUqI,OAClBhI,SAEPU,kBAAkBC,eAnBtBN,0BAA0BT,OAAQ,wCANlCA,OAAO2C,UAAW,QAtHlC0F,CAAoBvE,EAAM/C,mBA6JH+C,KAAM/C,UAETT,SAASC,eAAe,mBAG9BsE,iBAAiB,SAAS/E,qBAEhCmG,eAAiB3F,SAASuB,iBAAiB,+DAG3CyG,MAAQrC,eAAe7E,OACvBkG,MAAQ,EAGRC,SAAW,GAGftB,eAAenE,SAAQ,SAAS9B,YAExByH,QAAU1H,UAAUC,QACnBuI,MAAK,SAASpI,eACPA,OAAOC,UACPkH,OAAgB,GAEbnH,UACRqI,OAAM,SAAS7H,OACdC,OAAOC,QAAQF,MAAMA,UAG7B4G,SAASK,KAAKH,kBAIZM,QAAQC,IAAIT,UAGlBzD,KAAKuB,SAAS,CAACD,IAAK,EAAGE,SAAU,kBAG3BmD,sBAAajE,gBAAgB,CAC/B9D,QAAS4G,MAAQ,OAASgB,MAAQ,mCAClC7D,KAAO6C,QAAUgB,MAAS,UAAY,YAI1CxH,kBAAkBC,aAvMtB2H,CAAkB5E,KAAM/C,UAIxBD,kBAAkBC,UAGlBpB,qBAAuBuI,aAAY,KAC/BpH,kBAAkBjB,kBAxDJ"} \ 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='";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='";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..a3be0a5 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,eAAMe,SAAUC,eAAgBC,WAAOC,8DAAS,YAClE,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..6213503 100644 --- a/amd/src/dashboard.js +++ b/amd/src/dashboard.js @@ -1,13 +1,86 @@ -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'; +import {get_string as getString} from 'core/str'; -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 + * @param {string} moodleVersion + */ +export const init = (courseid, syncThresholdConfig, asyncConfig, moodleVersion) => { // If there is a saved message by successfully mapped an assessment in localStorage, display it. displayNotification(); - // Find the page element. - let page = document.getElementById("page"); + // Set the sync threshold from the plugin config. + syncThreshold = syncThresholdConfig; + + // Set the global variable course ID. + globalCourseid = courseid; + + // Set the async config. + async = asyncConfig; + + let page; + + // Get the scrollable page element depending on the Moodle version. + if (moodleVersion > '2023100900') { + // Moodle 4.3 and above. + page = window; + } else { + // Moodle 4.2 and below. + 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 + * @return {HTMLElement} + */ +function initModuleDeliverySelector(page) { // Find the module delivery table selector. let tableSelector = document.getElementById("module-delivery-selector"); @@ -16,11 +89,14 @@ export const init = () => { // Find the selected table by ID. let selectedTable = document.getElementById(tableSelector.value); + // Get the scroll position of the page. + let pagePosition = getPagePosition(page); + // Calculate the scroll position to be 100 pixels above the table. if (selectedTable) { let offset = -100; let tablePosition = selectedTable.getBoundingClientRect().top; - let scrollPosition = page.scrollTop + tablePosition + offset; + let scrollPosition = pagePosition + tablePosition + offset; // Scroll to the calculated position. page.scrollTo({ @@ -30,12 +106,23 @@ 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"); // Show the button when the user scrolls down 100 pixels from the top of the page. page.addEventListener("scroll", function() { - if (page.scrollTop >= 100) { + // Get the scroll position of the page. + if (getPagePosition(page) >= 100) { backToTopButton.style.display = "block"; } else { backToTopButton.style.display = "none"; @@ -47,7 +134,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 +154,69 @@ 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])"); + let mabpushbuttons = document.querySelectorAll(".push-mark-button"); 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"); + + // Disable the button if there is no assessment mapping ID. + if (assessmentmappingid === null) { + button.disabled = true; + return; + } + + if (studentcount === '0') { + // Show an error message if there is no student to push grades. + showErrorMessageForButton(button, 'There are no marks to transfer.'); + return; + } + + // 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 > 0 && 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 +230,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,18 +248,21 @@ 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. * * @param {HTMLElement} button The button element. - * @return {Promise} Promise. + * @return {Promise|boolean} Promise. */ async function pushMarks(button) { try { @@ -135,50 +274,270 @@ 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) { document.getElementById(tooltipid).remove(); } } else { - // Create an error message row. - let errormessagerow = document.createElement("tr"); - - // Set the class and content of the error message row. - errormessagerow.setAttribute("class", "error-message-row"); - errormessagerow.innerHTML = - '' + - '' + - ''; - - // Find the closest row to the button. - let currentrow = button.closest("tr"); - - // Remove the existing error message row if it exists. - if (currentrow.nextElementSibling !== null && - currentrow.nextElementSibling.classList.contains("error-message-row")) { - currentrow.nextElementSibling.remove(); - } - - // Insert the error message row after the current row. - currentrow.insertAdjacentElement("afterend", errormessagerow); + // Show an error message if the transfer task is not successfully scheduled. + showErrorMessageForButton(button, result.message); } - 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 {Promise} + */ +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 The assessment mapping IDs having task in progress. + */ +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 if (assessmentmappingid !== null) { + // Reset the button to the original state. + pushButton.innerHTML = ''; + pushButton.disabled = false; + } else { + // No assessment mapping, disable the button. + pushButton.disabled = true; + } + }); +} + +/** + * Update the icons to show that there are transfer records. + * + * @param {object[]} assessments + */ +function updateIcon(assessments) { + let pushButtons = document.querySelectorAll('.push-mark-button'); + + // 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. + pushButtons.forEach(function(button) { + let assessmentmappingid = button.getAttribute('data-assessmentmappingid'); + let icon = button.parentNode.parentNode.querySelector('.records-icon'); + 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'); + } + } else { + if (icon.classList.contains('fa-file-lines')) { + icon.classList.replace('fa-regular', 'fa-solid'); + icon.classList.replace('fa-file-lines', 'fa-circle-info'); + } + } + }); +} + +/** + * 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) { + // Get general error message. + let generalErrorMessage = await getString('error:sync_partially_failed', 'local_sitsgradepush'); + let errormessage = document.getElementById('error-message-modal-sync'); + errormessage.innerHTML = ''; + window.console.error(promise.message); + } + + promises.push(promise); + + // Increment the count by 1 for each student. + count = count + 1; + + // 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); +} + +/** + * Show an error message at the table row under the button. + * + * @param {HTMLElement} button + * @param {string} message + */ +function showErrorMessageForButton(button, message) { + // Create an error message row. + let errormessagerow = document.createElement("tr"); + + // Set the class and content of the error message row. + errormessagerow.setAttribute("class", "error-message-row"); + errormessagerow.innerHTML = + '' + + '' + + ''; + + // Find the closest row to the button. + let currentrow = button.closest("tr"); + + // Remove the existing error message row if it exists. + if (currentrow.nextElementSibling !== null && + currentrow.nextElementSibling.classList.contains("error-message-row")) { + currentrow.nextElementSibling.remove(); + } + + // Insert the error message row after the current row. + currentrow.insertAdjacentElement("afterend", errormessagerow); +} + /** * Display a notification if a success message is available in localStorage. */ @@ -198,3 +557,19 @@ function displayNotification() { localStorage.removeItem('successMessage'); } } + +/** + * Get the scroll position of the page. + * + * @param {HTMLElement} page + * @return {*|number} + */ +function getPagePosition(page) { + if (page instanceof Window) { + // Get the scroll position of the page. + return page.scrollY; + } else { + // Get the scroll position of the page. + return page.scrollTop; + } +} 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) => { '' + ''; + // 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..e00586e 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/cachemanager.php b/classes/cachemanager.php index 5193cde..c0f2beb 100644 --- a/classes/cachemanager.php +++ b/classes/cachemanager.php @@ -33,6 +33,9 @@ class cachemanager { /** @var string Cache area for storing students in an assessment component.*/ const CACHE_AREA_STUDENTSPR = 'studentspr'; + /** @var string Cache area for storing students in an assessment component.*/ + const CACHE_AREA_COMPONENTGRADES = 'componentgrades'; + /** * Get cache. * 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..2c393e3 --- /dev/null +++ b/classes/external/get_transfer_students.php @@ -0,0 +1,97 @@ +. +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 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..d916afa --- /dev/null +++ b/classes/external/transfer_mark_for_student.php @@ -0,0 +1,107 @@ +. +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 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) { + global $DB; + try { + $params = self::validate_parameters( + self::execute_parameters(), + [ + 'assessmentmappingid' => $assessmentmappingid, + 'userid' => $userid, + ] + ); + + $manager = manager::get_manager(); + + // Get assessment mapping. + $mapping = $DB->get_record(manager::TABLE_ASSESSMENT_MAPPING, ['id' => $params['assessmentmappingid']]); + + if (empty($mapping)) { + throw new \moodle_exception('error:assessmentmapping', 'local_sitsgradepush', '', $params['assessmentmappingid']); + } + + // Check if user has permission to transfer marks. + if (!has_capability('local/sitsgradepush:pushgrade', \context_course::instance($mapping->courseid))) { + throw new \moodle_exception('error:pushgradespermission', 'local_sitsgradepush'); + } + + // Get assessment mapping. + $assessmentmapping = $manager->get_assessment_mappings($mapping->coursemoduleid, $mapping->componentgradeid); + + $manager->push_grade_to_sits($assessmentmapping, $params['userid']); + $manager->push_submission_log_to_sits($assessmentmapping, $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..5cc5eb9 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; @@ -155,6 +143,26 @@ public function fetch_component_grades_from_sits(array $modocc): bool { try { if (!empty($modocc)) { foreach ($modocc as $occ) { + // Get the cache. + $key = implode('_', + [ + cachemanager::CACHE_AREA_COMPONENTGRADES, + $occ->mod_code, + $occ->mod_occ_mav, + $occ->mod_occ_psl_code, + $occ->mod_occ_year_code, + ] + ); + + // This cache is not really used. + // It is only used to check if the component grades have been fetched for this module occurrence. + $cache = cachemanager::get_cache(cachemanager::CACHE_AREA_COMPONENTGRADES, $key); + + // Skip if cache exists. + if (!empty($cache)) { + continue; + } + // Get component grades from SITS. $request = $this->apiclient->build_request(self::GET_COMPONENT_GRADE, $occ); $response = $this->apiclient->send_request($request); @@ -165,6 +173,9 @@ public function fetch_component_grades_from_sits(array $modocc): bool { // Filter out unwanted component grades by marking scheme. $response = $this->filter_out_invalid_component_grades($response); + // Set cache expiry to 30 days. + cachemanager::set_cache(cachemanager::CACHE_AREA_COMPONENTGRADES, $key, $response, 30 * 24 * 60 * 60); + // Save component grades to DB. $this->save_component_grades($response); } @@ -231,13 +242,10 @@ public function filter_out_invalid_component_grades(array $componentgrades): arr * @throws \dml_exception */ public function get_component_grade_options(int $courseid, mixed $coursemoduleid): array { - $options = []; - // Get module occurrences from portico enrolments block. $modocc = \block_portico_enrolments\manager::get_modocc_mappings($courseid); // Fetch component grades from SITS. - // TODO: Could do some caching here. $this->fetch_component_grades_from_sits($modocc); $modocccomponentgrades = $this->get_local_component_grades($modocc); @@ -854,13 +862,17 @@ public function get_transfer_logs (int $assessmentmappingid, int $userid, string /** * Get the assessment data. * - * @param assessment $assessment - * @return array + * @param int $coursemoduleid + * @param int|null $assessmentmappingid + * @return array|\stdClass * @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|\stdClass { + // Get the assessment. + $assessment = assessmentfactory::get_assessment($coursemoduleid); + $assessmentdata = []; $assessmentdata['studentsnotrecognized'] = []; $students = $assessment->get_all_participants(); @@ -872,6 +884,13 @@ public function get_assessment_data(assessment $assessment): array { return []; } + // Only process 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; @@ -905,7 +924,12 @@ public function get_assessment_data(assessment $assessment): array { } } - return $assessmentdata; + // Only return the mapping that matches the assessment mapping ID if assessmentmappingid is given. + if ($assessmentmappingid) { + return reset($assessmentdata['mappings']); + } else { + return $assessmentdata; + } } /** @@ -1014,169 +1038,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 +1213,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 +1266,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 +1296,70 @@ public function get_all_course_activities(int $courseid): array { return $activities; } + /** + * Get students in an assessment mapping eligible for marks transfer. + * + * @param int $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); + } + + $mapping = $this->get_assessment_data($assessmentmapping->coursemoduleid, $assessmentmappingid); + + return !empty($mapping->students) ? $mapping->students : null; + } + + /** + * Get data required for page update, e.g. progress bars, last transfer time. + * + * @param int $courseid + * @param int $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..cdf0611 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; @@ -116,23 +116,18 @@ public function render_assessment_push_status_table(\stdClass $mapping) : string } $lasttasktext = null; - $taskstatustext = null; + $hastaskinprogress = null; $mappingid = null; // Add last task details and push task status to the mapping object if any. if (!empty($mapping->id)) { $mappingid = $mapping->id; $lasttasktext = $this->get_last_push_task_time($mapping->id); - if ($taskstatus = $this->get_assessment_mapping_status_icon($mapping->id)) { - if ($taskstatus->status !== self::PUSH_STATUS_ICON_HAS_PUSH_RECORDS && - $taskstatus->status !== self::PUSH_STATUS_ICON_NO_PUSH_RECORDS) { - $taskstatustext = $taskstatus->statusicon . ' ' . $taskstatus->statustext; - } - } + $hastaskinprogress = taskmanager::get_pending_task_in_queue($mapping->id); } // Check if there is any task info to display. - $additionalinfo = $lasttasktext || $taskstatustext; + $additionalinfo = $lasttasktext || $hastaskinprogress; // Render the table. return $this->output->render_from_template('local_sitsgradepush/assessmentgrades', [ @@ -140,7 +135,6 @@ public function render_assessment_push_status_table(\stdClass $mapping) : string 'tabletitle' => $mapping->formattedname, 'students' => $students, 'lasttask' => $lasttasktext, - 'taskstatus' => $taskstatustext, 'additionalinfo' => $additionalinfo, ]); } @@ -190,14 +184,19 @@ public function render_dashboard(array $moduledeliveries, int $courseid) : strin if (empty($componentgrade->coursemoduleid)) { // Disable the change source button and push grade button if the MAB is not mapped to any activity. $componentgrade->disablechangesourcebutton = ' disabled'; - $componentgrade->disablepushgradebutton = ' disabled'; continue; } // 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; @@ -212,8 +211,6 @@ public function render_dashboard(array $moduledeliveries, int $courseid) : strin $componentgrade->assessmentmapping = $assessmentmapping; $componentgrade->disablechangesourcebutton = $this->disable_change_source_button($componentgrade->assessmentmappingid) ? ' disabled' : ''; - $componentgrade->disablepushgradebutton = - $this->disable_push_grade_button($assessmentmapping->status->status, $courseid) ? ' disabled' : ''; } else { throw new \moodle_exception('error:invalidcoursemoduleid', 'local_sitsgradepush'); } @@ -366,7 +363,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 +383,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; } @@ -444,24 +416,4 @@ private function get_assessment_mapping_status_icon(int $assessmentmappingid) { private function disable_change_source_button(int $assessmentmappingid) : bool { return $this->manager->has_grades_pushed($assessmentmappingid); } - - /** - * Disable the push grade button if the push task is in progress. - * - * @param string $pushstatus Push task status - * @param int $courseid Course ID - * @return bool - * @throws \coding_exception - */ - private function disable_push_grade_button(string $pushstatus, int $courseid) : bool { - // Disable the push grade button if the user does not have the capability. - if (!has_capability('local/sitsgradepush:pushgrade', \context_course::instance($courseid))) { - return true; - } - - return match($pushstatus) { - self::PUSH_STATUS_ICON_REQUESTED, self::PUSH_STATUS_ICON_QUEUED, self::PUSH_STATUS_ICON_PROCESSING => true, - default => false, - }; - } } 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..08aefe6 --- /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 ($mapping = $manager->get_assessment_data($assessmentmapping->coursemoduleid, $assessmentmapping->id)) { + 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. + * + * @return int + * @throws \dml_exception + */ + public static function get_number_of_running_tasks(): int { + global $DB; + return $DB->count_records('local_sitsgradepush_tasks', ['status' => self::PUSH_TASK_STATUS_PROCESSING]); + } + + /** + * 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 user has permission to transfer marks. + if (!has_capability('local/sitsgradepush:pushgrade', \context_course::instance($mapping->courseid))) { + throw new \moodle_exception('error:pushgradespermission', 'local_sitsgradepush'); + } + + // 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. + $mapping = manager::get_manager()->get_assessment_data($mapping->coursemoduleid, $mapping->id); + + // 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 int $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..c93ab21 100644 --- a/dashboard.php +++ b/dashboard.php @@ -90,7 +90,16 @@ } // 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'), + $CFG->version, + ] +); // Page footer. echo $OUTPUT->footer(); diff --git a/db/caches.php b/db/caches.php index cef8195..803981f 100644 --- a/db/caches.php +++ b/db/caches.php @@ -31,4 +31,9 @@ 'simplekeys' => true, 'simpledata' => false, ], + 'componentgrades' => [ + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => false, + ], ]; diff --git a/db/install.xml b/db/install.xml index c1d1c06..0e9c431 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..0bb218a 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 < 2023121900) { + // 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, 2023121900, '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..952aa12 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,8 @@ $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['error:sync_partially_failed'] = 'Some marks failed to transfer.'; $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 +172,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 @@ }] } }} - +
{{#additionalinfo}} diff --git a/templates/module_delivery_table.mustache b/templates/module_delivery_table.mustache index af95797..b7f934f 100644 --- a/templates/module_delivery_table.mustache +++ b/templates/module_delivery_table.mustache @@ -41,8 +41,9 @@ * type - String, assessment mapping type. * url - String, assessment mapping url. * statusicon - String, assessment mapping status icon. + * selectsourceurl - String, select source url. + * numberofstudents - String, number of students. * disablechangesourcebutton - String, value to insert into the button, for 'disabled' or not. - * disablepushgradebutton - String, value to insert into the button, for 'disabled' or not. Example context (json): { @@ -63,10 +64,11 @@ "type": "Assignment", "name": "Test assignment 2", "url": "http://test.m4.local:4001/mod/assign/view.php?id=960", - "statusicon": "" + "statusicon": "", + "selectsourceurl": "http://test.m4.local:4001/local/sitsgradepush/select_source.php?courseid=2&mabid=22", + "numberofstudents": "10" }, - "disablechangesourcebutton": " disabled", - "disablepushgradebutton": "" + "disablechangesourcebutton": " disabled" } }, "mapcode": "PHAY0063A7PE", @@ -135,8 +137,8 @@ title="{{#str}} dashboard:transfermark, local_sitsgradepush {{/str}}" {{#assessmentmapping}} data-assessmentmappingid="{{id}}" - {{/assessmentmapping}} - {{disablepushgradebutton}}> + data-numberofstudents="{{numberofstudents}}" + {{/assessmentmapping}}> @@ -145,7 +147,7 @@ {{{statusicon}}} {{/assessmentmapping}} {{^assessmentmapping}} - {{/assessmentmapping}} diff --git a/version.php b/version.php index 8cc5bba..c6293c8 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 = 2023121900; $plugin->requires = 2021051708; $plugin->maturity = MATURITY_ALPHA; $plugin->dependencies = [
{{tabletitle}}
- {{{taskstatus}}} - {{lasttask}} +
{{lasttask}}
+
+
+