From 3c6f0d077009b8868b5050dd3e971e74d8799115 Mon Sep 17 00:00:00 2001 From: Alex Yeung Date: Tue, 20 Aug 2024 14:16:09 +0100 Subject: [PATCH] CTP-2816 record non-submission as part of the transfer --- amd/build/dashboard.min.js | 2 +- amd/build/dashboard.min.js.map | 2 +- amd/build/sitsgradepush_helper.min.js | 2 +- amd/build/sitsgradepush_helper.min.js.map | 2 +- amd/src/dashboard.js | 53 ++++++++--- amd/src/sitsgradepush_helper.js | 4 +- classes/assessment/assessment.php | 3 + classes/external/schedule_push_task.php | 18 +++- classes/manager.php | 50 ++++++++--- classes/output/pushrecord.php | 89 ++++++++++++------- classes/output/renderer.php | 2 + classes/taskmanager.php | 9 +- db/install.xml | 3 +- db/upgrade.php | 15 ++++ lang/en/local_sitsgradepush.php | 2 + templates/confirmation_modal.mustache | 34 ++++++- templates/dashboard.mustache | 30 ++++++- .../marks_transfer_history_page.mustache | 3 + .../marks_transfer_history_table.mustache | 17 ++-- tests/manager_test.php | 3 +- version.php | 2 +- 21 files changed, 266 insertions(+), 79 deletions(-) diff --git a/amd/build/dashboard.min.js b/amd/build/dashboard.min.js index 02aa3ab..e1e7e9a 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};let updatePageIntervalId=null,globalCourseid=null;async function pushMarks(assessmentmappingid){try{let result=await(0,_sitsgradepush_helper.schedulePushTask)(assessmentmappingid);result.success&&function(assessmentmappingid){let changeSourceButton=document.getElementById("change-source-button-"+assessmentmappingid);changeSourceButton&&(changeSourceButton.style.display="none");updateMarksColumn([{task:{progress:0},assessmentmappingid:assessmentmappingid,markscount:0}])}(assessmentmappingid);let message="";return!result.success&&result.message&&(message=result.message),function(assessmentmappingid,message){let currentrow=document.getElementById("marks-col-field-"+assessmentmappingid).closest("tr");null!==currentrow.nextElementSibling&¤trow.nextElementSibling.classList.contains("error-message-row")&¤trow.nextElementSibling.remove();if(""!==message){let errormessagerow=document.createElement("tr");errormessagerow.setAttribute("class","error-message-row"),errormessagerow.innerHTML='",currentrow.insertAdjacentElement("afterend",errormessagerow)}}(assessmentmappingid,message),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&&updateMarksColumn(assessments)}else clearInterval(updatePageIntervalId),window.console.error(update.message)}function updateMarksColumn(assessments){assessments.forEach((assessment=>{let marksColumnFieldId="marks-col-field-"+assessment.assessmentmappingid,marksColumnField=document.getElementById(marksColumnFieldId);if(marksColumnField){let marksContainer=marksColumnField.querySelector(".marks-container"),taskContainer=marksColumnField.querySelector(".task-status-container");marksColumnField.setAttribute("data-markscount",assessment.markscount),marksColumnField.querySelector(".marks-count").innerHTML=assessment.markscount;let transferButton=marksColumnField.querySelector(".js-btn-transfer-marks");assessment.markscount>0?transferButton.classList.remove("d-none"):transferButton.classList.add("d-none"),null===assessment.task?(marksColumnField.setAttribute("data-task-running",!1),taskContainer.classList.add("d-none"),marksContainer.classList.remove("d-none")):(marksColumnField.setAttribute("data-task-running",!0),marksContainer.classList.add("d-none"),taskContainer.classList.remove("d-none"),(0,_sitsgradepush_helper.updateProgressBar)(taskContainer,assessment.task.progress))}}))}_exports.init=courseid=>{var page;!function(){let successMessage=localStorage.getItem("successMessage");successMessage&&(_notification.default.addNotification({message:successMessage,type:"success"}),localStorage.removeItem("successMessage"))}(),globalCourseid=courseid,page=window,document.querySelectorAll(".jump-to-dropdown-item").forEach((function(item){item.addEventListener("click",(function(){let value=item.getAttribute("data-value");if(null!==value){let pagePosition=function(page){return page instanceof Window?page.scrollY:page.scrollTop}(page),selectedTable=document.getElementById(value);if(selectedTable){let offset=-100,scrollPosition=pagePosition+selectedTable.getBoundingClientRect().top+offset;page.scrollTo({top:scrollPosition,behavior:"smooth"})}}}))})),function(courseid){updateAssessments(courseid),updatePageIntervalId=setInterval((()=>{updateAssessments(courseid)}),15e3),document.addEventListener("visibilitychange",(function(){"hidden"===document.visibilityState?clearInterval(updatePageIntervalId):(updateAssessments(courseid),updatePageIntervalId=setInterval((()=>{updateAssessments(courseid)}),15e3))}))}(courseid),function(page){let confirmTransferButton=document.getElementById("js-transfer-modal-button");if(null===confirmTransferButton)return;confirmTransferButton.addEventListener("click",(async function(){let assessmentmappingid=confirmTransferButton.getAttribute("data-assessmentmappingid");null!==assessmentmappingid&&"all"!==assessmentmappingid?await pushMarks(assessmentmappingid):"all"===assessmentmappingid&&await async function(page){let assessmentmappings=Array.from(document.querySelectorAll(".marks-col-field")).filter((element=>parseInt(element.getAttribute("data-markscount"),10)>0&&"false"===element.getAttribute("data-task-running"))),total=assessmentmappings.length,count=0,promises=[];assessmentmappings.forEach((function(element){let promise=pushMarks(element.getAttribute("data-assessmentmappingid")).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(globalCourseid)}(page)}))}(window)}})); +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};let updatePageIntervalId=null,globalCourseid=null;async function pushMarks(assessmentmappingid,recordnonsubmission){try{let result=await(0,_sitsgradepush_helper.schedulePushTask)(assessmentmappingid,recordnonsubmission);result.success&&function(assessmentmappingid){let changeSourceButton=document.getElementById("change-source-button-"+assessmentmappingid);changeSourceButton&&(changeSourceButton.style.display="none");updateMarksColumn([{task:{progress:0},assessmentmappingid:assessmentmappingid,markscount:0,nonsubmittedcount:0}])}(assessmentmappingid);let message="";return!result.success&&result.message&&(message=result.message),function(assessmentmappingid,message){let currentrow=document.getElementById("marks-col-field-"+assessmentmappingid).closest("tr");null!==currentrow.nextElementSibling&¤trow.nextElementSibling.classList.contains("error-message-row")&¤trow.nextElementSibling.remove();if(""!==message){let errormessagerow=document.createElement("tr");errormessagerow.setAttribute("class","error-message-row"),errormessagerow.innerHTML='",currentrow.insertAdjacentElement("afterend",errormessagerow)}}(assessmentmappingid,message),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&&updateMarksColumn(assessments)}else clearInterval(updatePageIntervalId),window.console.error(update.message)}function updateMarksColumn(assessments){assessments.forEach((assessment=>{let marksColumnFieldId="marks-col-field-"+assessment.assessmentmappingid,marksColumnField=document.getElementById(marksColumnFieldId);if(marksColumnField){let marksContainer=marksColumnField.querySelector(".marks-container"),taskContainer=marksColumnField.querySelector(".task-status-container");marksColumnField.setAttribute("data-markscount",assessment.markscount),marksColumnField.setAttribute("data-nonsubmittedcount",assessment.nonsubmittedcount),marksColumnField.querySelector(".marks-count").innerHTML=assessment.markscount;let transferButton=marksColumnField.querySelector(".js-btn-transfer-marks");assessment.markscount>0||assessment.nonsubmittedcount>0?transferButton.classList.remove("d-none"):transferButton.classList.add("d-none"),null===assessment.task?(marksColumnField.setAttribute("data-task-running",!1),taskContainer.classList.add("d-none"),marksContainer.classList.remove("d-none")):(marksColumnField.setAttribute("data-task-running",!0),marksContainer.classList.add("d-none"),taskContainer.classList.remove("d-none"),(0,_sitsgradepush_helper.updateProgressBar)(taskContainer,assessment.task.progress))}}))}_exports.init=courseid=>{var page;!function(){let successMessage=localStorage.getItem("successMessage");successMessage&&(_notification.default.addNotification({message:successMessage,type:"success"}),localStorage.removeItem("successMessage"))}(),globalCourseid=courseid,page=window,document.querySelectorAll(".jump-to-dropdown-item").forEach((function(item){item.addEventListener("click",(function(){let value=item.getAttribute("data-value");if(null!==value){let pagePosition=function(page){return page instanceof Window?page.scrollY:page.scrollTop}(page),selectedTable=document.getElementById(value);if(selectedTable){let offset=-100,scrollPosition=pagePosition+selectedTable.getBoundingClientRect().top+offset;page.scrollTo({top:scrollPosition,behavior:"smooth"})}}}))})),function(courseid){updateAssessments(courseid),updatePageIntervalId=setInterval((()=>{updateAssessments(courseid)}),15e3),document.addEventListener("visibilitychange",(function(){"hidden"===document.visibilityState?clearInterval(updatePageIntervalId):(updateAssessments(courseid),updatePageIntervalId=setInterval((()=>{updateAssessments(courseid)}),15e3))}))}(courseid),function(page){let confirmTransferButton=document.getElementById("js-transfer-modal-button");if(null===confirmTransferButton)return;confirmTransferButton.addEventListener("click",(async function(){let assessmentmappingid=confirmTransferButton.getAttribute("data-assessmentmappingid"),recordnonsubmission=document.getElementById("recordnonsubmission").checked;null!==assessmentmappingid&&"all"!==assessmentmappingid?await pushMarks(assessmentmappingid,recordnonsubmission):"all"===assessmentmappingid&&await async function(page,recordnonsubmission){let assessmentmappings=Array.from(document.querySelectorAll(".marks-col-field")).filter((element=>{let marksCount=parseInt(element.getAttribute("data-markscount"),10),nonSubmittedCount=parseInt(element.getAttribute("data-nonsubmittedcount"),10),taskRunning="true"===element.getAttribute("data-task-running");return(0!==marksCount||0!==nonSubmittedCount)&&(recordnonsubmission?(marksCount>0||nonSubmittedCount>0)&&!taskRunning:marksCount>0&&!taskRunning)})),total=assessmentmappings.length,count=0,promises=[];assessmentmappings.forEach((function(element){let promise=pushMarks(element.getAttribute("data-assessmentmappingid"),recordnonsubmission).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(globalCourseid)}(page,recordnonsubmission)}))}(window)}})); //# 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 e383b56..412c3bd 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, getAssessmentsUpdate, updateProgressBar} from \"./sitsgradepush_helper\";\nimport notification from \"core/notification\";\n\nlet updatePageIntervalId = null; // The interval ID for updating the progress.\nlet globalCourseid = null; // The global variable for course ID.\nlet updatePageDelay = 15000; // The delay for updating the page.\n\n/**\n * Initialize the dashboard page.\n *\n * @param {int} courseid\n */\nexport const init = (courseid) => {\n // If there is a saved message by successfully mapped an assessment in localStorage, display it.\n displayNotification();\n\n // Set the global variable course ID.\n globalCourseid = courseid;\n\n // Initialize the module delivery dropdown list.\n initModuleDeliverySelector(window);\n\n // Initialize assessment updates.\n initAssessmentUpdate(courseid);\n\n // Initialize confirmation modal.\n initConfirmationModal(window);\n};\n\n/**\n * Initialize the module delivery dropdown list.\n *\n * @param {Window} page\n */\nfunction initModuleDeliverySelector(page) {\n // Find all the dropdown items.\n let dropdownitems = document.querySelectorAll('.jump-to-dropdown-item');\n\n // Add event listener to each dropdown item.\n dropdownitems.forEach(function(item) {\n item.addEventListener('click', function() {\n let value = item.getAttribute('data-value');\n if (value !== null) {\n // Get the scroll position of the page.\n let pagePosition = getPagePosition(page);\n\n // Find the selected table by ID.\n let selectedTable = document.getElementById(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 = pagePosition + tablePosition + offset;\n\n // Scroll to the calculated position.\n page.scrollTo({\n top: scrollPosition,\n behavior: \"smooth\"\n });\n }\n }\n });\n });\n}\n\n/**\n * Initialize the confirmation modal.\n *\n * @param {Window} page\n */\nfunction initConfirmationModal(page) {\n // Find the confirmation modal.\n let confirmTransferButton = document.getElementById(\"js-transfer-modal-button\");\n\n // Exit if the confirmation modal is not found.\n if (confirmTransferButton === null) {\n return;\n }\n\n // Add event listener to the confirmation modal.\n confirmTransferButton.addEventListener(\"click\", async function() {\n let assessmentmappingid = confirmTransferButton.getAttribute('data-assessmentmappingid');\n if (assessmentmappingid !== null && assessmentmappingid !== 'all') {\n // Single transfer.\n await pushMarks(assessmentmappingid);\n } else if (assessmentmappingid === 'all') {\n // Bulk transfer.\n await pushAllMarks(page);\n }\n });\n}\n\n/**\n * Initialize the assessment updates.\n *\n * @param {int} courseid\n */\nfunction initAssessmentUpdate(courseid) {\n updateAssessments(courseid);\n\n // Update the page every 15 seconds.\n updatePageIntervalId = setInterval(() => {\n updateAssessments(courseid);\n }, updatePageDelay);\n\n // Add event listener to stop update the page when the page is not visible. e.g. when the user switches to another tab.\n document.addEventListener(\"visibilitychange\", function() {\n if (document.visibilityState === \"hidden\") {\n clearInterval(updatePageIntervalId);\n } else {\n updateAssessments(courseid);\n updatePageIntervalId = setInterval(() => {\n updateAssessments(courseid);\n }, updatePageDelay);\n }\n });\n}\n\n/**\n * Schedule a push task when the user clicks on a push button.\n *\n * @param {int} assessmentmappingid The button element.\n * @return {Promise|boolean} Promise.\n */\nasync function pushMarks(assessmentmappingid) {\n try {\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 UI once a task is scheduled successfully.\n updateUIOnTaskScheduling(assessmentmappingid);\n }\n let message = '';\n if (!result.success && result.message) {\n message = result.message;\n }\n\n // Show error message if there is any.\n showTransferErrorMessage(assessmentmappingid, message);\n return result;\n } catch (error) {\n window.console.error(error);\n return false;\n }\n}\n\n/**\n *\n * @param {HTMLElement} page\n * @return {Promise}\n */\nasync function pushAllMarks(page) {\n let assessmentmappings = Array.from(document.querySelectorAll('.marks-col-field'))\n .filter(element =>\n parseInt(element.getAttribute('data-markscount'), 10) > 0 &&\n element.getAttribute('data-task-running') === 'false'\n );\n\n // Number of not disabled push buttons.\n let total = assessmentmappings.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 assessmentmappings.forEach(function(element) {\n // Get the assessment mapping ID.\n let assessmentmappingid = element.getAttribute('data-assessmentmappingid');\n // Create a Promise for each button and push it into the array.\n let promise = pushMarks(assessmentmappingid)\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(globalCourseid);\n}\n\n/**\n * Update the UI once a task is scheduled successfully.\n * e.g. hide change source button, show progress bar.\n *\n * @param {int} assessmentmappingid\n */\nfunction updateUIOnTaskScheduling(assessmentmappingid) {\n // Find the change source button.\n let changeSourceButton = document.getElementById('change-source-button-' + assessmentmappingid);\n if (changeSourceButton) {\n // Hide the change source button.\n changeSourceButton.style.display = 'none';\n }\n\n // Hide the transfer button and show the progress bar immediately.\n let assessments = [\n {task: {progress: 0}, assessmentmappingid: assessmentmappingid, markscount: 0},\n ];\n updateMarksColumn(assessments);\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 updateMarksColumn(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 the marks' column for all assessments mappings.\n *\n * @param {object[]} assessments\n */\nfunction updateMarksColumn(assessments) {\n // Update assessment components which has mappings.\n assessments.forEach(assessment => {\n let marksColumnFieldId = 'marks-col-field-' + assessment.assessmentmappingid;\n let marksColumnField = document.getElementById(marksColumnFieldId);\n if (marksColumnField) {\n let marksContainer = marksColumnField.querySelector('.marks-container');\n let taskContainer = marksColumnField.querySelector('.task-status-container');\n\n // Set the marks count attribute.\n marksColumnField.setAttribute('data-markscount', assessment.markscount);\n\n // Marks count element that displays the number of marks.\n let marksCountElement = marksColumnField.querySelector('.marks-count');\n\n // Update the marks count.\n marksCountElement.innerHTML = assessment.markscount;\n\n // Show the transfer button if there are marks to transfer.\n let transferButton = marksColumnField.querySelector('.js-btn-transfer-marks');\n if (assessment.markscount > 0) {\n transferButton.classList.remove('d-none');\n } else {\n transferButton.classList.add('d-none');\n }\n\n // Show marks information if no task running.\n if (assessment.task === null) {\n marksColumnField.setAttribute('data-task-running', false);\n taskContainer.classList.add('d-none');\n marksContainer.classList.remove('d-none');\n } else {\n // Show task information if task running.\n marksColumnField.setAttribute('data-task-running', true);\n marksContainer.classList.add('d-none');\n taskContainer.classList.remove('d-none');\n updateProgressBar(taskContainer, assessment.task.progress);\n }\n }\n });\n}\n\n/**\n * Show an error message at the table row under the button.\n *\n * @param {int} assessmentmappingid\n * @param {string} message\n */\nfunction showTransferErrorMessage(assessmentmappingid, message) {\n // Find the marks column field.\n let marksColumnField = document.getElementById('marks-col-field-' + assessmentmappingid);\n\n // Find the closest row to the button.\n let currentrow = marksColumnField.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 if (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 // Insert the error message row after the current row.\n currentrow.insertAdjacentElement(\"afterend\", errormessagerow);\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\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","globalCourseid","pushMarks","assessmentmappingid","result","success","changeSourceButton","document","getElementById","style","display","updateMarksColumn","task","progress","markscount","updateUIOnTaskScheduling","message","currentrow","closest","nextElementSibling","classList","contains","remove","errormessagerow","createElement","setAttribute","innerHTML","insertAdjacentElement","showTransferErrorMessage","error","window","console","updateAssessments","courseid","update","assessments","JSON","parse","length","clearInterval","forEach","assessment","marksColumnFieldId","marksColumnField","marksContainer","querySelector","taskContainer","transferButton","add","page","successMessage","localStorage","getItem","addNotification","type","removeItem","displayNotification","querySelectorAll","item","addEventListener","value","getAttribute","pagePosition","Window","scrollY","scrollTop","getPagePosition","selectedTable","offset","scrollPosition","getBoundingClientRect","top","scrollTo","behavior","setInterval","visibilityState","initAssessmentUpdate","confirmTransferButton","async","assessmentmappings","Array","from","filter","element","parseInt","total","count","promises","promise","then","catch","push","Promise","all","notification","pushAllMarks","initConfirmationModal"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"dashboard.min.js","sources":["../src/dashboard.js"],"sourcesContent":["import {schedulePushTask, getAssessmentsUpdate, updateProgressBar} from \"./sitsgradepush_helper\";\nimport notification from \"core/notification\";\n\nlet updatePageIntervalId = null; // The interval ID for updating the progress.\nlet globalCourseid = null; // The global variable for course ID.\nlet updatePageDelay = 15000; // The delay for updating the page.\n\n/**\n * Initialize the dashboard page.\n *\n * @param {int} courseid\n */\nexport const init = (courseid) => {\n // If there is a saved message by successfully mapped an assessment in localStorage, display it.\n displayNotification();\n\n // Set the global variable course ID.\n globalCourseid = courseid;\n\n // Initialize the module delivery dropdown list.\n initModuleDeliverySelector(window);\n\n // Initialize assessment updates.\n initAssessmentUpdate(courseid);\n\n // Initialize confirmation modal.\n initConfirmationModal(window);\n};\n\n/**\n * Initialize the module delivery dropdown list.\n *\n * @param {Window} page\n */\nfunction initModuleDeliverySelector(page) {\n // Find all the dropdown items.\n let dropdownitems = document.querySelectorAll('.jump-to-dropdown-item');\n\n // Add event listener to each dropdown item.\n dropdownitems.forEach(function(item) {\n item.addEventListener('click', function() {\n let value = item.getAttribute('data-value');\n if (value !== null) {\n // Get the scroll position of the page.\n let pagePosition = getPagePosition(page);\n\n // Find the selected table by ID.\n let selectedTable = document.getElementById(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 = pagePosition + tablePosition + offset;\n\n // Scroll to the calculated position.\n page.scrollTo({\n top: scrollPosition,\n behavior: \"smooth\"\n });\n }\n }\n });\n });\n}\n\n/**\n * Initialize the confirmation modal.\n *\n * @param {Window} page\n */\nfunction initConfirmationModal(page) {\n // Find the confirmation modal.\n let confirmTransferButton = document.getElementById(\"js-transfer-modal-button\");\n\n // Exit if the confirmation modal is not found.\n if (confirmTransferButton === null) {\n return;\n }\n\n // Add event listener to the confirmation modal.\n confirmTransferButton.addEventListener(\"click\", async function() {\n let assessmentmappingid = confirmTransferButton.getAttribute('data-assessmentmappingid');\n\n // Check should we record non-submission as 0 AB.\n let recordnonsubmission = document.getElementById('recordnonsubmission').checked;\n\n if (assessmentmappingid !== null && assessmentmappingid !== 'all') {\n // Single transfer.\n await pushMarks(assessmentmappingid, recordnonsubmission);\n } else if (assessmentmappingid === 'all') {\n // Bulk transfer.\n await pushAllMarks(page, recordnonsubmission);\n }\n });\n}\n\n/**\n * Initialize the assessment updates.\n *\n * @param {int} courseid\n */\nfunction initAssessmentUpdate(courseid) {\n updateAssessments(courseid);\n\n // Update the page every 15 seconds.\n updatePageIntervalId = setInterval(() => {\n updateAssessments(courseid);\n }, updatePageDelay);\n\n // Add event listener to stop update the page when the page is not visible. e.g. when the user switches to another tab.\n document.addEventListener(\"visibilitychange\", function() {\n if (document.visibilityState === \"hidden\") {\n clearInterval(updatePageIntervalId);\n } else {\n updateAssessments(courseid);\n updatePageIntervalId = setInterval(() => {\n updateAssessments(courseid);\n }, updatePageDelay);\n }\n });\n}\n\n/**\n * Schedule a push task when the user clicks on a push button.\n *\n * @param {int} assessmentmappingid The button element.\n * @param {boolean} recordnonsubmission Record non-submission as 0 AB.\n * @return {Promise|boolean} Promise.\n */\nasync function pushMarks(assessmentmappingid, recordnonsubmission) {\n try {\n // Schedule a push task.\n let result = await schedulePushTask(assessmentmappingid, recordnonsubmission);\n\n // Check if the push task is successfully scheduled.\n if (result.success) {\n // Update the UI once a task is scheduled successfully.\n updateUIOnTaskScheduling(assessmentmappingid);\n }\n let message = '';\n if (!result.success && result.message) {\n message = result.message;\n }\n\n // Show error message if there is any.\n showTransferErrorMessage(assessmentmappingid, message);\n return result;\n } catch (error) {\n window.console.error(error);\n return false;\n }\n}\n\n/**\n *\n * @param {HTMLElement} page\n * @param {boolean} recordnonsubmission Record non-submission as 0 AB.\n * @return {Promise}\n */\nasync function pushAllMarks(page, recordnonsubmission) {\n // Find all the mappings that have marks to push based on the recordnonsubmission setting.\n let assessmentmappings = Array.from(document.querySelectorAll('.marks-col-field'))\n .filter(element => {\n // Get current mapping statuses.\n let marksCount = parseInt(element.getAttribute('data-markscount'), 10);\n let nonSubmittedCount = parseInt(element.getAttribute('data-nonsubmittedcount'), 10);\n let taskRunning = element.getAttribute('data-task-running') === 'true';\n\n // Nothing to push if there are no marks and no non-submitted records.\n if (marksCount === 0 && nonSubmittedCount === 0) {\n return false;\n }\n\n if (recordnonsubmission) {\n // Record non-submission enabled, return true when mapping has marks or non-submitted records and no task running.\n return (marksCount > 0 || nonSubmittedCount > 0) && !taskRunning;\n } else {\n // Record non-submission disabled, return true when mapping has marks and no task running.\n return marksCount > 0 && !taskRunning;\n }\n });\n\n // Number of not disabled push buttons.\n let total = assessmentmappings.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 assessmentmappings.forEach(function(element) {\n // Get the assessment mapping ID.\n let assessmentmappingid = element.getAttribute('data-assessmentmappingid');\n // Create a Promise for each button and push it into the array.\n let promise = pushMarks(assessmentmappingid, recordnonsubmission)\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(globalCourseid);\n}\n\n/**\n * Update the UI once a task is scheduled successfully.\n * e.g. hide change source button, show progress bar.\n *\n * @param {int} assessmentmappingid\n */\nfunction updateUIOnTaskScheduling(assessmentmappingid) {\n // Find the change source button.\n let changeSourceButton = document.getElementById('change-source-button-' + assessmentmappingid);\n if (changeSourceButton) {\n // Hide the change source button.\n changeSourceButton.style.display = 'none';\n }\n\n // Hide the transfer button and show the progress bar immediately.\n let assessments = [\n {task: {progress: 0}, assessmentmappingid: assessmentmappingid, markscount: 0, nonsubmittedcount: 0},\n ];\n updateMarksColumn(assessments);\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 updateMarksColumn(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 the marks' column for all assessments mappings.\n *\n * @param {object[]} assessments\n */\nfunction updateMarksColumn(assessments) {\n // Update assessment components which has mappings.\n assessments.forEach(assessment => {\n let marksColumnFieldId = 'marks-col-field-' + assessment.assessmentmappingid;\n let marksColumnField = document.getElementById(marksColumnFieldId);\n if (marksColumnField) {\n let marksContainer = marksColumnField.querySelector('.marks-container');\n let taskContainer = marksColumnField.querySelector('.task-status-container');\n\n // Set the marks count attribute.\n marksColumnField.setAttribute('data-markscount', assessment.markscount);\n\n // Set the non submitted count attribute.\n marksColumnField.setAttribute('data-nonsubmittedcount', assessment.nonsubmittedcount);\n\n // Marks count element that displays the number of marks.\n let marksCountElement = marksColumnField.querySelector('.marks-count');\n\n // Update the marks count.\n marksCountElement.innerHTML = assessment.markscount;\n\n // Get the transfer button.\n let transferButton = marksColumnField.querySelector('.js-btn-transfer-marks');\n\n // Show the transfer button if there are marks or non-submitted records.\n if (assessment.markscount > 0 || assessment.nonsubmittedcount > 0) {\n transferButton.classList.remove('d-none');\n } else {\n transferButton.classList.add('d-none');\n }\n\n // Show marks information if no task running.\n if (assessment.task === null) {\n marksColumnField.setAttribute('data-task-running', false);\n taskContainer.classList.add('d-none');\n marksContainer.classList.remove('d-none');\n } else {\n // Show task information if task running.\n marksColumnField.setAttribute('data-task-running', true);\n marksContainer.classList.add('d-none');\n taskContainer.classList.remove('d-none');\n updateProgressBar(taskContainer, assessment.task.progress);\n }\n }\n });\n}\n\n/**\n * Show an error message at the table row under the button.\n *\n * @param {int} assessmentmappingid\n * @param {string} message\n */\nfunction showTransferErrorMessage(assessmentmappingid, message) {\n // Find the marks column field.\n let marksColumnField = document.getElementById('marks-col-field-' + assessmentmappingid);\n\n // Find the closest row to the button.\n let currentrow = marksColumnField.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 if (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 // Insert the error message row after the current row.\n currentrow.insertAdjacentElement(\"afterend\", errormessagerow);\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\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","globalCourseid","pushMarks","assessmentmappingid","recordnonsubmission","result","success","changeSourceButton","document","getElementById","style","display","updateMarksColumn","task","progress","markscount","nonsubmittedcount","updateUIOnTaskScheduling","message","currentrow","closest","nextElementSibling","classList","contains","remove","errormessagerow","createElement","setAttribute","innerHTML","insertAdjacentElement","showTransferErrorMessage","error","window","console","updateAssessments","courseid","update","assessments","JSON","parse","length","clearInterval","forEach","assessment","marksColumnFieldId","marksColumnField","marksContainer","querySelector","taskContainer","transferButton","add","page","successMessage","localStorage","getItem","addNotification","type","removeItem","displayNotification","querySelectorAll","item","addEventListener","value","getAttribute","pagePosition","Window","scrollY","scrollTop","getPagePosition","selectedTable","offset","scrollPosition","getBoundingClientRect","top","scrollTo","behavior","setInterval","visibilityState","initAssessmentUpdate","confirmTransferButton","async","checked","assessmentmappings","Array","from","filter","element","marksCount","parseInt","nonSubmittedCount","taskRunning","total","count","promises","promise","then","catch","push","Promise","all","notification","pushAllMarks","initConfirmationModal"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/sitsgradepush_helper.min.js b/amd/build/sitsgradepush_helper.min.js index 32fdf3e..2c6876d 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.updateProgressBar=_exports.schedulePushTask=_exports.mapAssessment=_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,sourcetype,sourceid,mabid,reassess){let partid=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null;return new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_map_assessment",args:{courseid:courseid,sourcetype:sourcetype,sourceid:sourceid,mabid:mabid,reassess:reassess,partid:partid}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}))};_exports.getAssessmentsUpdate=async function(courseid){let sourcetype=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",sourceid=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_get_assessments_update",args:{courseid:courseid,sourcetype:sourcetype,sourceid:sourceid}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}))};_exports.updateProgressBar=(container,progress)=>{let progressLabel=container.querySelector("small"),progressBar=container.querySelector(".progress-bar");progressLabel&&progressBar&&(null===progress&&(progress=0),progressLabel.innerHTML="Progress: "+progress+"%",progressBar.setAttribute("aria-valuenow",progress),progressBar.style.width=progress+"%")}})); +define("local_sitsgradepush/sitsgradepush_helper",["exports","core/ajax"],(function(_exports,_ajax){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateProgressBar=_exports.schedulePushTask=_exports.mapAssessment=_exports.getAssessmentsUpdate=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.schedulePushTask=async function(assessmentmappingid){let recordnonsubmission=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_schedule_push_task",args:{assessmentmappingid:assessmentmappingid,recordnonsubmission:recordnonsubmission}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}))};_exports.mapAssessment=async function(courseid,sourcetype,sourceid,mabid,reassess){let partid=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null;return new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_map_assessment",args:{courseid:courseid,sourcetype:sourcetype,sourceid:sourceid,mabid:mabid,reassess:reassess,partid:partid}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}))};_exports.getAssessmentsUpdate=async function(courseid){let sourcetype=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",sourceid=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return new Promise(((resolve,reject)=>{_ajax.default.call([{methodname:"local_sitsgradepush_get_assessments_update",args:{courseid:courseid,sourcetype:sourcetype,sourceid:sourceid}}])[0].done((function(response){resolve(response)})).fail((function(err){window.console.log(err),reject(err)}))}))};_exports.updateProgressBar=(container,progress)=>{let progressLabel=container.querySelector("small"),progressBar=container.querySelector(".progress-bar");progressLabel&&progressBar&&(null===progress&&(progress=0),progressLabel.innerHTML="Progress: "+progress+"%",progressBar.setAttribute("aria-valuenow",progress),progressBar.style.width=progress+"%")}})); //# 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 29afd69..ffa7e18 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 {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 {string} sourcetype\n * @param {int} sourceid\n * @param {int} mabid\n * @param {int} reassess\n * @param {int|null} partid\n * @return {Promise}\n */\nexport const mapAssessment = async(courseid, sourcetype, sourceid, mabid, reassess, partid = null) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_map_assessment',\n args: {\n 'courseid': courseid,\n 'sourcetype': sourcetype,\n 'sourceid': sourceid,\n 'mabid': mabid,\n 'reassess': reassess,\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 * @param {string} sourcetype\n * @param {int} sourceid\n * @return {Promise}\n */\nexport const getAssessmentsUpdate = async(courseid, sourcetype = '', sourceid = 0) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_get_assessments_update',\n args: {\n 'courseid': courseid,\n 'sourcetype': sourcetype,\n 'sourceid': sourceid,\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 * Update the progress bar.\n *\n * @param {HTMLElement} container\n * @param {int} progress\n * @return {void}\n */\nexport const updateProgressBar = (container, progress) => {\n // Get the progress bar.\n let progressLabel = container.querySelector('small');\n let progressBar = container.querySelector('.progress-bar');\n\n if (progressLabel && progressBar) {\n if (progress === null) {\n progress = 0;\n }\n // Update the progress bar.\n progressLabel.innerHTML = 'Progress: ' + progress + '%';\n progressBar.setAttribute('aria-valuenow', progress);\n progressBar.style.width = progress + '%';\n }\n};\n"],"names":["async","Promise","resolve","reject","call","methodname","args","assessmentmappingid","done","response","fail","err","window","console","log","courseid","sourcetype","sourceid","mabid","reassess","partid","container","progress","progressLabel","querySelector","progressBar","innerHTML","setAttribute","style","width"],"mappings":"mWAQgCA,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,kCAgBUX,eAAMe,SAAUC,WAAYC,SAAUC,MAAOC,cAAUC,8DAAS,YAClF,IAAInB,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,qCACZC,KAAM,UACUS,oBACEC,oBACFC,eACHC,eACGC,gBACFC,WAEd,GAAGZ,MAAK,SAASC,UACjBP,QAAQO,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBR,OAAOQ,0CAciBX,eAAMe,cAAUC,kEAAa,GAAIC,gEAAW,SACrE,IAAIhB,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,6CACZC,KAAM,UACUS,oBACEC,oBACFC,aAEhB,GAAGT,MAAK,SAASC,UACjBP,QAAQO,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBR,OAAOQ,uCAYc,CAACU,UAAWC,gBAErCC,cAAgBF,UAAUG,cAAc,SACxCC,YAAcJ,UAAUG,cAAc,iBAEtCD,eAAiBE,cACA,OAAbH,WACAA,SAAW,GAGfC,cAAcG,UAAY,aAAeJ,SAAW,IACpDG,YAAYE,aAAa,gBAAiBL,UAC1CG,YAAYG,MAAMC,MAAQP,SAAW"} \ 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 * @param {boolean} recordnonsubmission Record non-submission.\n * @return {Promise} Promise.\n */\nexport const schedulePushTask = async(assessmentmappingid, recordnonsubmission = false) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_schedule_push_task',\n args: {\n 'assessmentmappingid': assessmentmappingid,\n 'recordnonsubmission': recordnonsubmission,\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 {string} sourcetype\n * @param {int} sourceid\n * @param {int} mabid\n * @param {int} reassess\n * @param {int|null} partid\n * @return {Promise}\n */\nexport const mapAssessment = async(courseid, sourcetype, sourceid, mabid, reassess, partid = null) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_map_assessment',\n args: {\n 'courseid': courseid,\n 'sourcetype': sourcetype,\n 'sourceid': sourceid,\n 'mabid': mabid,\n 'reassess': reassess,\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 * @param {string} sourcetype\n * @param {int} sourceid\n * @return {Promise}\n */\nexport const getAssessmentsUpdate = async(courseid, sourcetype = '', sourceid = 0) => {\n return new Promise((resolve, reject) => {\n Ajax.call([{\n methodname: 'local_sitsgradepush_get_assessments_update',\n args: {\n 'courseid': courseid,\n 'sourcetype': sourcetype,\n 'sourceid': sourceid,\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 * Update the progress bar.\n *\n * @param {HTMLElement} container\n * @param {int} progress\n * @return {void}\n */\nexport const updateProgressBar = (container, progress) => {\n // Get the progress bar.\n let progressLabel = container.querySelector('small');\n let progressBar = container.querySelector('.progress-bar');\n\n if (progressLabel && progressBar) {\n if (progress === null) {\n progress = 0;\n }\n // Update the progress bar.\n progressLabel.innerHTML = 'Progress: ' + progress + '%';\n progressBar.setAttribute('aria-valuenow', progress);\n progressBar.style.width = progress + '%';\n }\n};\n"],"names":["async","assessmentmappingid","recordnonsubmission","Promise","resolve","reject","call","methodname","args","done","response","fail","err","window","console","log","courseid","sourcetype","sourceid","mabid","reassess","partid","container","progress","progressLabel","querySelector","progressBar","innerHTML","setAttribute","style","width"],"mappings":"mWASgCA,eAAMC,yBAAqBC,mFAChD,IAAIC,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,yCACZC,KAAM,qBACqBP,wCACAC,wBAE3B,GAAGO,MAAK,SAASC,UACjBN,QAAQM,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBP,OAAOO,mCAgBUZ,eAAMgB,SAAUC,WAAYC,SAAUC,MAAOC,cAAUC,8DAAS,YAClF,IAAIlB,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,qCACZC,KAAM,UACUQ,oBACEC,oBACFC,eACHC,eACGC,gBACFC,WAEd,GAAGZ,MAAK,SAASC,UACjBN,QAAQM,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBP,OAAOO,0CAciBZ,eAAMgB,cAAUC,kEAAa,GAAIC,gEAAW,SACrE,IAAIf,SAAQ,CAACC,QAASC,wBACpBC,KAAK,CAAC,CACPC,WAAY,6CACZC,KAAM,UACUQ,oBACEC,oBACFC,aAEhB,GAAGT,MAAK,SAASC,UACjBN,QAAQM,aACTC,MAAK,SAASC,KACbC,OAAOC,QAAQC,IAAIH,KACnBP,OAAOO,uCAYc,CAACU,UAAWC,gBAErCC,cAAgBF,UAAUG,cAAc,SACxCC,YAAcJ,UAAUG,cAAc,iBAEtCD,eAAiBE,cACA,OAAbH,WACAA,SAAW,GAGfC,cAAcG,UAAY,aAAeJ,SAAW,IACpDG,YAAYE,aAAa,gBAAiBL,UAC1CG,YAAYG,MAAMC,MAAQP,SAAW"} \ No newline at end of file diff --git a/amd/src/dashboard.js b/amd/src/dashboard.js index 965a30b..6e3de6f 100644 --- a/amd/src/dashboard.js +++ b/amd/src/dashboard.js @@ -81,12 +81,16 @@ function initConfirmationModal(page) { // Add event listener to the confirmation modal. confirmTransferButton.addEventListener("click", async function() { let assessmentmappingid = confirmTransferButton.getAttribute('data-assessmentmappingid'); + + // Check should we record non-submission as 0 AB. + let recordnonsubmission = document.getElementById('recordnonsubmission').checked; + if (assessmentmappingid !== null && assessmentmappingid !== 'all') { // Single transfer. - await pushMarks(assessmentmappingid); + await pushMarks(assessmentmappingid, recordnonsubmission); } else if (assessmentmappingid === 'all') { // Bulk transfer. - await pushAllMarks(page); + await pushAllMarks(page, recordnonsubmission); } }); } @@ -121,12 +125,13 @@ function initAssessmentUpdate(courseid) { * Schedule a push task when the user clicks on a push button. * * @param {int} assessmentmappingid The button element. + * @param {boolean} recordnonsubmission Record non-submission as 0 AB. * @return {Promise|boolean} Promise. */ -async function pushMarks(assessmentmappingid) { +async function pushMarks(assessmentmappingid, recordnonsubmission) { try { // Schedule a push task. - let result = await schedulePushTask(assessmentmappingid); + let result = await schedulePushTask(assessmentmappingid, recordnonsubmission); // Check if the push task is successfully scheduled. if (result.success) { @@ -150,14 +155,31 @@ async function pushMarks(assessmentmappingid) { /** * * @param {HTMLElement} page + * @param {boolean} recordnonsubmission Record non-submission as 0 AB. * @return {Promise} */ -async function pushAllMarks(page) { +async function pushAllMarks(page, recordnonsubmission) { + // Find all the mappings that have marks to push based on the recordnonsubmission setting. let assessmentmappings = Array.from(document.querySelectorAll('.marks-col-field')) - .filter(element => - parseInt(element.getAttribute('data-markscount'), 10) > 0 && - element.getAttribute('data-task-running') === 'false' - ); + .filter(element => { + // Get current mapping statuses. + let marksCount = parseInt(element.getAttribute('data-markscount'), 10); + let nonSubmittedCount = parseInt(element.getAttribute('data-nonsubmittedcount'), 10); + let taskRunning = element.getAttribute('data-task-running') === 'true'; + + // Nothing to push if there are no marks and no non-submitted records. + if (marksCount === 0 && nonSubmittedCount === 0) { + return false; + } + + if (recordnonsubmission) { + // Record non-submission enabled, return true when mapping has marks or non-submitted records and no task running. + return (marksCount > 0 || nonSubmittedCount > 0) && !taskRunning; + } else { + // Record non-submission disabled, return true when mapping has marks and no task running. + return marksCount > 0 && !taskRunning; + } + }); // Number of not disabled push buttons. let total = assessmentmappings.length; @@ -171,7 +193,7 @@ async function pushAllMarks(page) { // Get the assessment mapping ID. let assessmentmappingid = element.getAttribute('data-assessmentmappingid'); // Create a Promise for each button and push it into the array. - let promise = pushMarks(assessmentmappingid) + let promise = pushMarks(assessmentmappingid, recordnonsubmission) .then(function(result) { if (result.success) { count = count + 1; @@ -216,7 +238,7 @@ function updateUIOnTaskScheduling(assessmentmappingid) { // Hide the transfer button and show the progress bar immediately. let assessments = [ - {task: {progress: 0}, assessmentmappingid: assessmentmappingid, markscount: 0}, + {task: {progress: 0}, assessmentmappingid: assessmentmappingid, markscount: 0, nonsubmittedcount: 0}, ]; updateMarksColumn(assessments); } @@ -263,15 +285,20 @@ function updateMarksColumn(assessments) { // Set the marks count attribute. marksColumnField.setAttribute('data-markscount', assessment.markscount); + // Set the non submitted count attribute. + marksColumnField.setAttribute('data-nonsubmittedcount', assessment.nonsubmittedcount); + // Marks count element that displays the number of marks. let marksCountElement = marksColumnField.querySelector('.marks-count'); // Update the marks count. marksCountElement.innerHTML = assessment.markscount; - // Show the transfer button if there are marks to transfer. + // Get the transfer button. let transferButton = marksColumnField.querySelector('.js-btn-transfer-marks'); - if (assessment.markscount > 0) { + + // Show the transfer button if there are marks or non-submitted records. + if (assessment.markscount > 0 || assessment.nonsubmittedcount > 0) { transferButton.classList.remove('d-none'); } else { transferButton.classList.add('d-none'); diff --git a/amd/src/sitsgradepush_helper.js b/amd/src/sitsgradepush_helper.js index 1ccc74a..ae49a09 100644 --- a/amd/src/sitsgradepush_helper.js +++ b/amd/src/sitsgradepush_helper.js @@ -4,14 +4,16 @@ import Ajax from 'core/ajax'; * Schedule a task to push grades to SITS. * * @param {int} assessmentmappingid The assessment mapping ID. + * @param {boolean} recordnonsubmission Record non-submission. * @return {Promise} Promise. */ -export const schedulePushTask = async(assessmentmappingid) => { +export const schedulePushTask = async(assessmentmappingid, recordnonsubmission = false) => { return new Promise((resolve, reject) => { Ajax.call([{ methodname: 'local_sitsgradepush_schedule_push_task', args: { 'assessmentmappingid': assessmentmappingid, + 'recordnonsubmission': recordnonsubmission, }, }])[0].done(function(response) { resolve(response); diff --git a/classes/assessment/assessment.php b/classes/assessment/assessment.php index 4640fca..2a0ae6c 100644 --- a/classes/assessment/assessment.php +++ b/classes/assessment/assessment.php @@ -31,6 +31,9 @@ abstract class assessment implements iassessment { /** @var string Grade failed */ const GRADE_FAIL = 'F'; + /** @var string Grade absent */ + const GRADE_ABSENT = 'AB'; + /** @var int Source instance id. E.g. course module id for activities, grade item id for grade items. */ public int $id; diff --git a/classes/external/schedule_push_task.php b/classes/external/schedule_push_task.php index 982cdb6..ef77c7f 100644 --- a/classes/external/schedule_push_task.php +++ b/classes/external/schedule_push_task.php @@ -39,6 +39,7 @@ class schedule_push_task extends external_api { public static function execute_parameters() { return new external_function_parameters([ 'assessmentmappingid' => new external_value(PARAM_INT, 'Assessment mapping ID', VALUE_REQUIRED), + 'recordnonsubmission' => new external_value(PARAM_BOOL, 'Record non-submission as 0 AB', VALUE_REQUIRED), ]); } @@ -58,17 +59,26 @@ public static function execute_returns() { /** * Schedule a push task. * - * @param int $assessmentmappingid + * @param int $assessmentmappingid Assessment mapping ID. + * @param bool $recordnonsubmission Record non-submission as 0 AB. * @return array */ - public static function execute(int $assessmentmappingid) { + public static function execute(int $assessmentmappingid, bool $recordnonsubmission) { try { $params = self::validate_parameters( self::execute_parameters(), - ['assessmentmappingid' => $assessmentmappingid] + [ + 'assessmentmappingid' => $assessmentmappingid, + 'recordnonsubmission' => $recordnonsubmission, + ] ); - taskmanager::schedule_push_task($params['assessmentmappingid']); + // Add task options. + $options = [ + 'recordnonsubmission' => $params['recordnonsubmission'], + ]; + + taskmanager::schedule_push_task($params['assessmentmappingid'], $options); return [ 'success' => true, diff --git a/classes/manager.php b/classes/manager.php index 05f19d9..5a50c15 100644 --- a/classes/manager.php +++ b/classes/manager.php @@ -660,13 +660,16 @@ public function get_students_from_sits(\stdClass $componentgrade): mixed { * * @param \stdClass $assessmentmapping * @param int $userid - * @param int|null $taskid + * @param \stdClass|null $task * @return bool * @throws \dml_exception * @throws \moodle_exception */ - public function push_grade_to_sits(\stdClass $assessmentmapping, int $userid, ?int $taskid = null): bool { + public function push_grade_to_sits(\stdClass $assessmentmapping, int $userid, ?\stdClass $task = null): bool { try { + // Get task id. + $taskid = (!empty($task)) ? $task->id : null; + // Check if last push was succeeded, exit if succeeded. if ($this->last_push_succeeded($assessmentmapping->id, $userid, self::PUSH_GRADE)) { return false; @@ -678,8 +681,27 @@ public function push_grade_to_sits(\stdClass $assessmentmapping, int $userid, ?i // Get grade. [$rawmarks, $equivalentgrade] = $assessmentmapping->source->get_user_grade($userid); + // Transfer marks through task, check task options. + if ($task && !empty($task->options)) { + $options = json_decode($task->options); + // Records non-submission. + if ($options->recordnonsubmission) { + // Get submission if the source is of type MOD, no submission for other types + // such as manual grade and category. + if ($assessmentmapping->source->get_type() == assessmentfactory::SOURCETYPE_MOD) { + $submission = submissionfactory::get_submission($assessmentmapping->source->get_coursemodule_id(), $userid); + } + + // If no submission and no marks found, set rawmarks to 0 and equivalent grade to absent. + if (empty($rawmarks) && (!isset($submission) || !$submission->get_submission_data())) { + $rawmarks = 0; + $equivalentgrade = assessment::GRADE_ABSENT; + } + } + } + // Push if grade is found. - if ($rawmarks) { + if (is_numeric($rawmarks) && $rawmarks >= 0) { $data->marks = $rawmarks; $data->grade = $equivalentgrade ?? ''; @@ -909,11 +931,13 @@ public function get_assessment_data(string $sourcetype, int $sourceid, ?int $ass // Fetch students from SITS. foreach ($mappings as $mapping) { $mabkey = $mapping->mapcode . '-' . $mapping->mabseq; - $studentsfromsits[$mabkey] = - array_column($this->get_students_from_sits($mapping), null, 'code'); - $assessmentdata['mappings'][$mabkey] = $mapping; - $assessmentdata['mappings'][$mabkey]->markscount = 0; - $assessmentdata['mappings'][$mabkey]->source = $assessment; + $studentsfromsits[$mabkey] = array_column($this->get_students_from_sits($mapping), null, 'code'); + + // Add additional properties to the $mapping object. + $mapping->markscount = 0; + $mapping->nonsubmittedcount = 0; + $mapping->source = $assessment; + $mapping->students = []; // Students here is all the participants in that assessment. foreach ($students as $key => $student) { @@ -922,13 +946,18 @@ public function get_assessment_data(string $sourcetype, int $sourceid, ?int $ass // are in the studentsfromsits array and valid to the mapping type, e.g. main or re-assessment. $validrecord = $studentrecord->check_record_from_sits($mapping, $studentsfromsits[$mabkey]); if ($studentrecord->componentgrade == $mabkey || $validrecord) { - $assessmentdata['mappings'][$mabkey]->students[] = $studentrecord; + $mapping->students[] = $studentrecord; if ($studentrecord->should_transfer_mark()) { - $assessmentdata['mappings'][$mabkey]->markscount++; + $mapping->markscount++; + } + if ($studentrecord->is_non_submitted()) { + $mapping->nonsubmittedcount++; } unset($students[$key]); } } + + $assessmentdata['mappings'][$mabkey] = $mapping; } // Remaining students are not valid for pushing. @@ -1268,6 +1297,7 @@ public function get_data_for_page_update(int $courseid, string $sourcetype = '', $result->sourcetype = $sourcetype; $result->sourceid = $sourceid; $result->markscount = $assessmentdata->markscount; + $result->nonsubmittedcount = $assessmentdata->nonsubmittedcount; $result->task = !empty($task) ? $task : null; $result->lasttransfertime = taskmanager::get_last_push_task_time($mapping->id); $results[] = $result; diff --git a/classes/output/pushrecord.php b/classes/output/pushrecord.php index 0ed17f8..8a1281c 100644 --- a/classes/output/pushrecord.php +++ b/classes/output/pushrecord.php @@ -31,6 +31,9 @@ * @author Alex Yeung */ class pushrecord { + /** @var string mark transferred as absent */ + public string $absent; + /** @var string SITS component grade */ public string $componentgrade = ''; @@ -179,6 +182,17 @@ public function should_transfer_mark(): bool { return $this->marks != '-' && !($this->isgradepushed && $this->lastgradepushresult === 'success'); } + /** + * Check if the student record is valid for non-submitted marks transfer. + * + * @return bool + */ + public function is_non_submitted(): bool { + // Student has not submitted, marks is not given and grade is not pushed successfully yet. + return $this->marks == '-' && $this->handindatetime == '-' && + !($this->isgradepushed && $this->lastgradepushresult === 'success'); + } + /** * Set grade. * @@ -239,43 +253,44 @@ protected function set_submission(assessment $source, int $studentid): void { */ protected function set_transfer_records(int $assessmentmappingid, int $studentid): void { $transferlogs = $this->manager->get_transfer_logs($assessmentmappingid, $studentid); - if (!empty($transferlogs)) { - foreach ($transferlogs as $log) { - $response = json_decode($log->response); - $result = ($response->code == '0') ? 'success' : 'failed'; - if (is_null($log->errlogid)) { - $errortype = 0; - } else { - $errortype = $log->errortype ?: errormanager::ERROR_UNKNOWN; - } - // Get - from request url. - // The Easikit Get Student API will remove the students whose marks had been transferred successfully. - // Find the assessment component - for that transfer log, - // so that we can display the transfer status of mark transfer in the corresponding assessment component mapping. - $mab = $this->manager->get_mab_by_mapping_id($assessmentmappingid); - if (!empty($mab)) { - $this->componentgrade = $mab->mapcode . '-' . $mab->mabseq; - } + // Exit if no transfer logs to set. + if (empty($transferlogs)) { + return; + } + + // The Easikit Get Student API will remove the students whose marks had been transferred successfully. + // Find the assessment component - for that transfer log, + // so that we can display the transfer status of mark transfer in the corresponding assessment component mapping. + $mab = $this->manager->get_mab_by_mapping_id($assessmentmappingid); + if (!empty($mab)) { + $this->componentgrade = $mab->mapcode . '-' . $mab->mabseq; + } - if ($log->type == manager::PUSH_GRADE) { - // Check if marks updated after transfer. - if ($response->code == '0') { - $requestbody = json_decode($log->requestbody); - $this->transferredmark = $this->manager->get_formatted_marks($this->courseid, $requestbody->actual_mark); - $this->marksupdatedaftertransfer = - $this->is_marks_updated_after_transfer($this->rawmarks, $requestbody->actual_mark); - } - $this->lastgradepushresult = $result; - $this->lastgradepusherrortype = $errortype; - $this->lastgradepushtimestring = date('Y-m-d H:i:s', $log->timecreated); - $this->lastgradepushtime = $log->timecreated; - } else if ($log->type == manager::PUSH_SUBMISSION_LOG) { - $this->lastsublogpushresult = $result; - $this->lastsublogpusherrortype = $errortype; - $this->lastsublogpushtimestring = date('Y-m-d H:i:s', $log->timecreated); - $this->lastsublogpushtime = $log->timecreated; + foreach ($transferlogs as $log) { + $response = json_decode($log->response); + $result = ($response->code == '0') ? 'success' : 'failed'; + $errortype = $log->errlogid ? ($log->errortype ?: errormanager::ERROR_UNKNOWN) : 0; + + if ($log->type == manager::PUSH_GRADE) { + // Check if marks updated after transfer. + if ($response->code == 0) { + $requestbody = json_decode($log->requestbody); + $this->transferredmark = $this->manager->get_formatted_marks($this->courseid, $requestbody->actual_mark); + $this->marksupdatedaftertransfer = + $this->is_marks_updated_after_transfer($this->rawmarks, $requestbody->actual_mark); + $this->absent = ($requestbody->actual_grade === assessment::GRADE_ABSENT); + $this->marks = $this->absent ? $this->manager->get_formatted_marks($this->courseid, 0) : $this->marks; } + $this->lastgradepushresult = $result; + $this->lastgradepusherrortype = $errortype; + $this->lastgradepushtimestring = date('Y-m-d H:i:s', $log->timecreated); + $this->lastgradepushtime = $log->timecreated; + } else if ($log->type == manager::PUSH_SUBMISSION_LOG) { + $this->lastsublogpushresult = $result; + $this->lastsublogpusherrortype = $errortype; + $this->lastsublogpushtimestring = date('Y-m-d H:i:s', $log->timecreated); + $this->lastsublogpushtime = $log->timecreated; } } } @@ -310,6 +325,12 @@ protected function set_is_submission_log_pushed(): void { * @return bool */ private function is_marks_updated_after_transfer(string $rawmarks, string $transferredmarks): bool { + // Return false if the raw marks is not numeric. + // For example, the raw marks is '-' when no mark is given. + if (!is_numeric($rawmarks)) { + return false; + } + // As some of the marks were not transferred in raw marks, e.g. 66.67 instead of 66.66666 // so need to format the raw marks to the same decimal places as the transferred marks for comparison. // Future marks transfer will be all in 5 decimal places as raw marks is stored in 5 decimal places. diff --git a/classes/output/renderer.php b/classes/output/renderer.php index 840945f..afe9237 100644 --- a/classes/output/renderer.php +++ b/classes/output/renderer.php @@ -208,6 +208,7 @@ public function render_dashboard(array $moduledeliveries, int $courseid, int $re $assessmentmapping = new \stdClass(); $assessmentmapping->markstotransfer = $assessmentdata->markscount ?? 0; + $assessmentmapping->nonsubmittedcount = $assessmentdata->nonsubmittedcount ?? 0; $assessmentmapping->id = $mapping->id; $assessmentmapping->type = $assessmentdata->source->get_display_type_name(); $assessmentmapping->name = $assessmentdata->source->get_assessment_name(); @@ -249,6 +250,7 @@ public function render_dashboard(array $moduledeliveries, int $courseid, int $re 'jump-to-label' => get_string('label:jumpto', 'local_sitsgradepush'), 'transfer-all-button-label' => get_string('label:pushall', 'local_sitsgradepush'), 'gradesneedregrading' => grade_needs_regrade_final_grades($courseid), + 'recordnonsubmission' => true, // Show the record non-submission as 0 AB checkbox. ] ); } diff --git a/classes/taskmanager.php b/classes/taskmanager.php index 1937575..9c58bdd 100644 --- a/classes/taskmanager.php +++ b/classes/taskmanager.php @@ -50,7 +50,7 @@ class taskmanager { * @param int $taskid * @throws \dml_exception|\moodle_exception */ - public static function run_task(int $taskid) { + public static function run_task(int $taskid): void { global $DB; $manager = manager::get_manager(); @@ -86,7 +86,7 @@ public static function run_task(int $taskid) { $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, $task->id); + $manager->push_grade_to_sits($mapping, $student->userid, $task); $manager->push_submission_log_to_sits($mapping, $student->userid, $task->id); $i++; @@ -229,11 +229,13 @@ public static function update_task_status(int $taskid, int $status, ?int $errlog * Schedule push task. * * @param int $assessmentmappingid Assessment mapping id + * @param array $options Extra options for the task, e.g. records non-submission as zero. + * * @return bool * @throws \dml_exception * @throws \moodle_exception */ - public static function schedule_push_task(int $assessmentmappingid) { + public static function schedule_push_task(int $assessmentmappingid, array $options) { global $DB, $USER; // Check if the assessment mapping exists. @@ -266,6 +268,7 @@ public static function schedule_push_task(int $assessmentmappingid) { $task->userid = $USER->id; $task->timescheduled = time(); $task->assessmentmappingid = $assessmentmappingid; + $task->options = json_encode($options); $task->status = self::PUSH_TASK_STATUS_REQUESTED; // Check the number of students in the mapping. diff --git a/db/install.xml b/db/install.xml index bca1c15..a3f288c 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -98,6 +98,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index 11258c5..bf50611 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -519,5 +519,20 @@ function xmldb_local_sitsgradepush_upgrade($oldversion) { } } + if ($oldversion < 2024101000) { + + // Define field options to be added to local_sitsgradepush_tasks. + $table = new xmldb_table('local_sitsgradepush_tasks'); + $field = new xmldb_field('options', XMLDB_TYPE_TEXT, null, null, null, null, null, 'assessmentmappingid'); + + // Conditionally launch add field options. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Sitsgradepush savepoint reached. + upgrade_plugin_savepoint(true, 2024101000, 'local', 'sitsgradepush'); + } + return true; } diff --git a/lang/en/local_sitsgradepush.php b/lang/en/local_sitsgradepush.php index cc5e66d..e10570e 100644 --- a/lang/en/local_sitsgradepush.php +++ b/lang/en/local_sitsgradepush.php @@ -38,6 +38,7 @@ $string['confirmmodal:cancel'] = 'Cancel'; $string['confirmmodal:confirm'] = 'Confirm'; $string['confirmmodal:header'] = 'Confirm mark transfer?'; +$string['confirmmodal:nonsubmission'] = 'Record non-submission as 0 AB - only use when certain all submissions are in'; $string['confirmmodal:warning'] = 'Warning'; $string['dashboard:academicyear'] = 'ACADEMIC YEAR: {$a}'; $string['dashboard:actions'] = 'ACTIONS'; @@ -129,6 +130,7 @@ $string['form:info_turnitin_numparts'] = 'Please note Turnitin assignment with multiple parts is not supported by Marks Transfer.'; $string['gradepushassessmentselect'] = 'Select SITS assessment'; $string['gradepushassessmentselect_help'] = 'Select SITS assessment to link to this activity.'; +$string['index:absent'] = 'Absent'; $string['index:grade'] = 'Mark'; $string['index:header'] = 'SITS Marks Transfer Status'; $string['index:lastmarktransfer'] = 'Mark Transferred'; diff --git a/templates/confirmation_modal.mustache b/templates/confirmation_modal.mustache index 9618d81..fb84845 100644 --- a/templates/confirmation_modal.mustache +++ b/templates/confirmation_modal.mustache @@ -50,11 +50,43 @@ {{#str}} confirmmodal:body:partone, local_sitsgradepush {{/str}} {{#str}} confirmmodal:body:parttwo, local_sitsgradepush {{/str}} + {{#recordnonsubmission}} +
+ + +
+ {{/recordnonsubmission}} + +{{#js}} + require(['jquery'], function($) { + $('#recordnonsubmission').on('change', function() { + // If there are marks to transfer, no need to check the record non-submission checkbox. + // The confirm button should be always enabled. + if (parseInt($('#js-transfer-modal-button').attr('data-markscount')) > 0) { + return; + } + + // No marks to transfer but there are some non submitted records, + // then enable the confirm button when the record non-submission checkbox is checked. + if (parseInt($('#js-transfer-modal-button').attr('data-nonsubmittedcount')) > 0) { + if ($(this).is(':checked')) { + $('#js-transfer-modal-button').prop('disabled', false); + } else { + $('#js-transfer-modal-button').prop('disabled', true); + } + } + }); + }); +{{/js}} diff --git a/templates/dashboard.mustache b/templates/dashboard.mustache index a2454b2..016055b 100644 --- a/templates/dashboard.mustache +++ b/templates/dashboard.mustache @@ -193,6 +193,7 @@ class="marks-col-field" data-assessmentmappingid="{{id}}" data-markscount="{{markstotransfer}}" + data-nonsubmittedcount="{{nonsubmittedcount}}" data-task-running="{{taskrunning}}" >
@@ -206,7 +207,7 @@

{{#currentacademicyear}} - @@ -252,14 +253,41 @@ {{#js}} require(['jquery'], function($) { + // Transfer marks button $('.js-btn-transfer-marks').on('click', function() { let mappingid = $(this).closest('.marks-col-field').attr('data-assessmentmappingid'); $('#js-transfer-modal-button').attr('data-assessmentmappingid', mappingid); + + // Get the marks count. + let markscount = $(this).closest('.marks-col-field').attr('data-markscount'); + $('#js-transfer-modal-button').attr('data-markscount', markscount); + + // Get the non-submitted count. + let nonsubmittedcount = $(this).closest('.marks-col-field').attr('data-nonsubmittedcount'); + $('#js-transfer-modal-button').attr('data-nonsubmittedcount', nonsubmittedcount); + + if (markscount > 0) { + // There are marks to transfer, enable the confirm button. + $('#js-transfer-modal-button').prop('disabled', false); + } else if (markscount == 0 && nonsubmittedcount > 0) { + // No marks to transfer but there are some non submitted records, disable the confirm button by default. + $('#js-transfer-modal-button').prop('disabled', true); + } + + // Reset the non-submission checkbox every time the modal is opened. + $('#recordnonsubmission').prop('checked', false); }); + // Transfer all marks button $('.js-btn-transfer-all-marks').on('click', function() { let mappingid = $(this).attr('data-assessmentmappingid'); $('#js-transfer-modal-button').attr('data-assessmentmappingid', mappingid); + + // Enable the confirm button + $('#js-transfer-modal-button').prop('disabled', false); + + // Reset the non-submission checkbox every time the modal is opened. + $('#recordnonsubmission').prop('checked', false); }); }); {{/js}} diff --git a/templates/marks_transfer_history_page.mustache b/templates/marks_transfer_history_page.mustache index 1e18d81..606edb6 100644 --- a/templates/marks_transfer_history_page.mustache +++ b/templates/marks_transfer_history_page.mustache @@ -140,6 +140,9 @@ require(['jquery'], function($) { $('.js-btn-transfer-all-marks').on('click', function() { $('#js-transfer-modal-button').attr('data-sync', $(this).attr('data-sync')); + + // Reset the non-submission checkbox + $('#recordnonsubmission').prop('checked', false); }); }); {{/js}} diff --git a/templates/marks_transfer_history_table.mustache b/templates/marks_transfer_history_table.mustache index 4d66ef8..cf21597 100644 --- a/templates/marks_transfer_history_table.mustache +++ b/templates/marks_transfer_history_table.mustache @@ -84,11 +84,18 @@ {{firstname}} {{lastname}} {{idnumber}} - {{^marksupdatedaftertransfer}}{{marks}}{{/marksupdatedaftertransfer}} - {{#marksupdatedaftertransfer}} - {{transferredmark}}
- {{#str}} index:mark_changed_to, local_sitsgradepush, {{marks}} {{/str}} - {{/marksupdatedaftertransfer}} + + {{#absent}} + {{marks}}
+ {{#str}} index:absent, local_sitsgradepush {{/str}} + {{/absent}} + {{^absent}} + {{^marksupdatedaftertransfer}}{{marks}}{{/marksupdatedaftertransfer}} + {{/absent}} + {{#marksupdatedaftertransfer}} + {{transferredmark}}
+ {{#str}} index:mark_changed_to, local_sitsgradepush, {{marks}} {{/str}} + {{/marksupdatedaftertransfer}} {{handindatetime}} {{{lastgradepushresultlabel}}}{{lastgradepushtimestring}} diff --git a/tests/manager_test.php b/tests/manager_test.php index c86a77b..6954300 100644 --- a/tests/manager_test.php +++ b/tests/manager_test.php @@ -1105,6 +1105,7 @@ public function test_get_data_for_page_update(): void { 'sourcetype' => 'mod', 'sourceid' => $this->assign1->cmid, 'markscount' => 1, + 'nonsubmittedcount' => 0, 'task' => null, 'lasttransfertime' => null], ]; @@ -1226,7 +1227,7 @@ public function test_can_change_source(): void { $this->setUser($this->teacher1); // Set the component grade as pushed. - taskmanager::schedule_push_task($this->mappingid1); + taskmanager::schedule_push_task($this->mappingid1, ['recordnonsubmission' => false]); // Test the component grade can not change source once it has been pushed. $this->assertFalse($this->manager->can_change_source($this->mab1->id, $type)); diff --git a/version.php b/version.php index 7c28f72..99d9ac5 100644 --- a/version.php +++ b/version.php @@ -27,7 +27,7 @@ $plugin->component = 'local_sitsgradepush'; $plugin->release = '0.1.0'; -$plugin->version = 2024100100; +$plugin->version = 2024101000; $plugin->requires = 2023100900; $plugin->maturity = MATURITY_ALPHA; $plugin->dependencies = [