diff --git a/public/mod/assign/amd/build/batch_set_marking_workflow_state.min.js b/public/mod/assign/amd/build/batch_set_marking_workflow_state.min.js new file mode 100644 index 00000000000..e02034211c2 --- /dev/null +++ b/public/mod/assign/amd/build/batch_set_marking_workflow_state.min.js @@ -0,0 +1,12 @@ +define("mod_assign/batch_set_marking_workflow_state",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; +/** + * Javascript controller for the "Actions" panel at the bottom of the page. + * + * @module mod_assign/batch_set_marking_workflow_state + * @copyright 2025 onwards Catalyst IT {@link http://www.catalyst-eu.net/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Conn Warwicker + */ +const filterOptions=(context,args)=>{const options=args[context];document.getElementById("id_markingworkflowstate").children.forEach((item=>{options.includes(item.value)?item.removeAttribute("disabled"):item.setAttribute("disabled","")}))};_exports.init=args=>{const workflowcontext=document.getElementById("id_workflowcontext");filterOptions(workflowcontext.value,args),workflowcontext.addEventListener("change",(e=>{filterOptions(e.target.value,args)}))}})); + +//# sourceMappingURL=batch_set_marking_workflow_state.min.js.map \ No newline at end of file diff --git a/public/mod/assign/amd/build/batch_set_marking_workflow_state.min.js.map b/public/mod/assign/amd/build/batch_set_marking_workflow_state.min.js.map new file mode 100644 index 00000000000..b3aa4939cea --- /dev/null +++ b/public/mod/assign/amd/build/batch_set_marking_workflow_state.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"batch_set_marking_workflow_state.min.js","sources":["../src/batch_set_marking_workflow_state.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript controller for the \"Actions\" panel at the bottom of the page.\n *\n * @module mod_assign/batch_set_marking_workflow_state\n * @copyright 2025 onwards Catalyst IT {@link http://www.catalyst-eu.net/}\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @author Conn Warwicker \n */\n\n/**\n * Filter which options are enabled in the select menu.\n *\n * @param {object} context\n * @param {array} args\n */\nconst filterOptions = (context, args) => {\n const options = args[context];\n const workflowstate = document.getElementById('id_markingworkflowstate');\n workflowstate.children.forEach((item) => {\n if (options.includes(item.value)) {\n item.removeAttribute('disabled');\n } else {\n item.setAttribute('disabled', '');\n }\n });\n};\n\n/**\n * Initialise scripts.\n *\n * @param {array} args\n */\nexport const init = (args) => {\n const workflowcontext = document.getElementById('id_workflowcontext');\n filterOptions(workflowcontext.value, args);\n workflowcontext.addEventListener('change', (e) => {\n filterOptions(e.target.value, args);\n });\n};"],"names":["filterOptions","context","args","options","document","getElementById","children","forEach","item","includes","value","removeAttribute","setAttribute","workflowcontext","addEventListener","e","target"],"mappings":";;;;;;;;;MA8BMA,cAAgB,CAACC,QAASC,cACtBC,QAAUD,KAAKD,SACCG,SAASC,eAAe,2BAChCC,SAASC,SAASC,OACxBL,QAAQM,SAASD,KAAKE,OACtBF,KAAKG,gBAAgB,YAErBH,KAAKI,aAAa,WAAY,sBAUrBV,aACXW,gBAAkBT,SAASC,eAAe,sBAChDL,cAAca,gBAAgBH,MAAOR,MACrCW,gBAAgBC,iBAAiB,UAAWC,IACxCf,cAAce,EAAEC,OAAON,MAAOR"} \ No newline at end of file diff --git a/public/mod/assign/amd/build/grading_navigation.min.js b/public/mod/assign/amd/build/grading_navigation.min.js index 91d80b63730..b006583f7aa 100644 --- a/public/mod/assign/amd/build/grading_navigation.min.js +++ b/public/mod/assign/amd/build/grading_navigation.min.js @@ -6,6 +6,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ -define("mod_assign/grading_navigation",["jquery","core/notification","core/str","core/form-autocomplete","core/ajax","core_user/repository","mod_assign/grading_form_change_checker"],(function($,notification,str,autocomplete,ajax,UserRepository,checker){var GradingNavigation=function(selector){this._regionSelector=selector,this._region=$(selector),this._filters=[],this._users=[],this._filteredUsers=[],this._lastXofYUpdate=0,this._firstLoadUsers=!0;let url=new URL(window.location);parseInt(url.searchParams.get("treset"))>0&&(url.searchParams.delete("treset"),window.history.replaceState({},"",url)),this._loadAllUsers(),this._region.find('[data-action="previous-user"]').on("click",this._handlePreviousUser.bind(this)),this._region.find('[data-action="next-user"]').on("click",this._handleNextUser.bind(this)),this._region.find('[data-action="change-user"]').on("change",this._handleChangeUser.bind(this)),this._region.find('[data-region="user-filters"]').on("click",this._toggleExpandFilters.bind(this)),this._region.find('[data-region="user-resettable"]').on("click",this._toggleResetTable.bind()),$(document).on("user-changed",this._refreshSelector.bind(this)),$(document).on("reset-table",this._toggleResetTable.bind(this)),$(document).on("done-saving-show-next",this._handleNextUser.bind(this));var toggleLink=this._region.find('[data-region="user-filters"]');$(document.getElementById(toggleLink.attr("aria-controls"))).on("change","select",this._filterChanged.bind(this));var userid=$('[data-region="grading-navigation-panel"]').data("first-userid");userid&&this._selectUserById(userid),str.get_string("changeuser","mod_assign").done((function(s){autocomplete.enhance("[data-action=change-user]",!1,"mod_assign/participant_selector",s)})).fail(notification.exception),$(document).bind("start-loading-user",function(){this._isLoading=!0}.bind(this)),$(document).bind("finish-loading-user",function(){this._isLoading=!1}.bind(this))};return GradingNavigation.prototype._isLoading=!1,GradingNavigation.prototype._regionSelector=null,GradingNavigation.prototype._filters=null,GradingNavigation.prototype._users=null,GradingNavigation.prototype._region=null,GradingNavigation.prototype._lastFilters="",GradingNavigation.prototype._loadAllUsers=function(){var select=this._region.find("[data-action=change-user]"),assignmentid=select.attr("data-assignmentid"),groupid=select.attr("data-groupid"),filterPanel=this._region.find('[data-region="configure-filters"]'),filter=filterPanel.find('select[name="filter"]').val(),workflowFilter=filterPanel.find('select[name="workflowfilter"]');workflowFilter&&(filter+=","+workflowFilter.val());var markerFilter=filterPanel.find('select[name="markerfilter"]');return markerFilter&&(filter+=","+markerFilter.val()),this._lastFilters!=filter&&(this._lastFilters=filter,ajax.call([{methodname:"mod_assign_list_participants",args:{assignid:assignmentid,groupid:groupid,filter:"",onlyids:!0,tablesort:!0},done:this._usersLoaded.bind(this),fail:notification.exception}]),!0)},GradingNavigation.prototype._usersLoaded=function(users){if(this._firstLoadUsers=!1,this._filteredUsers=this._users=users,this._users.length){var toggleLink=this._region.find('[data-region="user-filters"]');$(document.getElementById(toggleLink.attr("aria-controls"))).find('select[name="filter"]').trigger("change"),$('[data-region="grade-panel"]').show(),$('[data-region="grade-actions-panel"]').show()}else this._selectNoUser();this._triggerNextUserEvent()},GradingNavigation.prototype._checkClickOutsideConfigureFilters=function(event){var configPanel=this._region.find('[data-region="configure-filters"]');if(!configPanel.is(event.target)&&0===configPanel.has(event.target).length){var toggleLink=this._region.find('[data-region="user-filters"]');configPanel.hide(),configPanel.attr("aria-hidden","true"),toggleLink.attr("aria-expanded","false"),$(document).unbind("click.mod_assign_grading_navigation")}},GradingNavigation.prototype._updateFilterPreferences=function(userId,filterList,preferenceNames){var preferences=[],i=0;if(0==filterList.length||this._firstLoadUsers){var deferred=$.Deferred();return deferred.resolve(),deferred}for(i=0;i0&&!isNaN(useridnumber)&&useridnumber>0&&$(document).trigger("user-changed",useridnumber)))},GradingNavigation.prototype._toggleExpandFilters=function(event){event.preventDefault();var toggleLink=$(event.target).closest('[data-region="user-filters"]'),expanded="true"==toggleLink.attr("aria-expanded"),configPanel=$(document.getElementById(toggleLink.attr("aria-controls")));expanded?(configPanel.hide(),configPanel.attr("aria-hidden","true"),toggleLink.attr("aria-expanded","false"),$(document).unbind("click.mod_assign_grading_navigation")):(configPanel.css("display","inline-block"),configPanel.attr("aria-hidden","false"),toggleLink.attr("aria-expanded","true"),event.stopPropagation(),$(document).on("click.mod_assign_grading_navigation",this._checkClickOutsideConfigureFilters.bind(this)))},GradingNavigation.prototype._toggleResetTable=function(){let url=new URL(window.location);url.searchParams.set("treset","1"),window.location.href=url},GradingNavigation.prototype._handlePreviousUser=function(e){e.preventDefault();var currentUserId=this._region.find("[data-action=change-user]").attr("data-selected"),i=0,currentIndex=0;for(i=0;i0&&$(document).trigger("user-changed",userid)}else count&&this._selectUserById(this._filteredUsers[newIndex].id)},GradingNavigation.prototype._setCountString=function(x,y){var updateNumber;this._lastXofYUpdate++,updateNumber=this._lastXofYUpdate;var param={x:x,y:y};str.get_string("xofy","mod_assign",param).done(function(s){updateNumber==this._lastXofYUpdate&&this._region.find('[data-region="user-count-summary"]').text(s)}.bind(this)).fail(notification.exception)},GradingNavigation.prototype._refreshCount=function(){var userid=this._region.find("[data-action=change-user]").attr("data-selected"),i=0,currentIndex=0;if(isNaN(userid)||userid<=0)this._region.find('[data-region="user-count"]').hide();else{for(this._region.find('[data-region="user-count"]').show(),i=0;i0){var url=new URL(window.location);if(parseInt(url.searchParams.get("blindid"))>0){var newid=this._filteredUsers[currentIndex-1].recordid;url.searchParams.set("blindid",newid)}else url.searchParams.set("userid",userid);window.history.replaceState({},"",url)}}},GradingNavigation.prototype._refreshSelector=function(event,userid){var select=this._region.find("[data-action=change-user]");userid=parseInt(userid,10),!isNaN(userid)&&userid>0&&select.attr("data-selected",userid),this._refreshCount()},GradingNavigation.prototype._triggerNextUserEvent=function(){this._filteredUsers.length>1?$(document).trigger("next-user",{nextUserId:null,nextUser:!0}):$(document).trigger("next-user",{nextUser:!1})},GradingNavigation.prototype._handleChangeUser=function(){var select=this._region.find("[data-action=change-user]"),userid=parseInt(select.val(),10);this._isLoading||(checker.checkFormForChanges('[data-region="grade-panel"] .gradeform')?str.get_strings([{key:"unsavedchanges",component:"mod_assign"},{key:"unsavedchangesquestion",component:"mod_assign"},{key:"saveandcontinue",component:"mod_assign"},{key:"cancel",component:"core"}]).done((function(strs){notification.confirm(strs[0],strs[1],strs[2],strs[3],(function(){$(document).trigger("save-changes",userid)}))})):!isNaN(userid)&&userid>0&&(select.attr("data-selected",userid),$(document).trigger("user-changed",userid)))},GradingNavigation})); +define("mod_assign/grading_navigation",["jquery","core/notification","core/str","core/form-autocomplete","core/ajax","core_user/repository","mod_assign/grading_form_change_checker"],(function($,notification,str,autocomplete,ajax,UserRepository,checker){var GradingNavigation=function(selector){this._regionSelector=selector,this._region=$(selector),this._filters=[],this._users=[],this._filteredUsers=[],this._lastXofYUpdate=0,this._firstLoadUsers=!0,this._isMarking=!1;let url=new URL(window.location);parseInt(url.searchParams.get("treset"))>0&&(url.searchParams.delete("treset"),window.history.replaceState({},"",url)),this._loadAllUsers(),this._region.find('[data-action="previous-user"]').on("click",this._handlePreviousUser.bind(this)),this._region.find('[data-action="next-user"]').on("click",this._handleNextUser.bind(this)),this._region.find('[data-action="change-user"]').on("change",this._handleChangeUser.bind(this)),this._region.find('[data-region="user-filters"]').on("click",this._toggleExpandFilters.bind(this)),this._region.find('[data-region="user-resettable"]').on("click",this._toggleResetTable.bind()),$(document).on("user-changed",this._refreshSelector.bind(this)),$(document).on("reset-table",this._toggleResetTable.bind(this)),$(document).on("done-saving-show-next",this._handleNextUser.bind(this));var toggleLink=this._region.find('[data-region="user-filters"]');$(document.getElementById(toggleLink.attr("aria-controls"))).on("change","select",this._filterChanged.bind(this));var userid=$('[data-region="grading-navigation-panel"]').data("first-userid");userid&&this._selectUserById(userid),str.get_string("changeuser","mod_assign").done((function(s){autocomplete.enhance("[data-action=change-user]",!1,"mod_assign/participant_selector",s)})).fail(notification.exception),$(document).bind("start-loading-user",function(){this._isLoading=!0}.bind(this)),$(document).bind("finish-loading-user",function(){this._isLoading=!1}.bind(this))};return GradingNavigation.prototype._isLoading=!1,GradingNavigation.prototype._regionSelector=null,GradingNavigation.prototype._filters=null,GradingNavigation.prototype._users=null,GradingNavigation.prototype._region=null,GradingNavigation.prototype._lastFilters="",GradingNavigation.prototype._loadAllUsers=function(){var _select$attr,select=this._region.find("[data-action=change-user]"),assignmentid=select.attr("data-assignmentid"),groupid=select.attr("data-groupid"),filterPanel=this._region.find('[data-region="configure-filters"]'),filter=filterPanel.find('select[name="filter"]').val(),workflowFilter=filterPanel.find('select[name="workflowfilter"]');workflowFilter&&(filter+=","+workflowFilter.val());var markerFilter=filterPanel.find('select[name="markerfilter"]');return markerFilter&&(filter+=","+markerFilter.val()),this._lastFilters!=filter&&(this._lastFilters=filter,this._isMarking=null!==(_select$attr=select.attr("data-ismarking"))&&void 0!==_select$attr&&_select$attr,ajax.call([{methodname:"mod_assign_list_participants",args:{assignid:assignmentid,groupid:groupid,filter:"",onlyids:!0,tablesort:!0,marking:this._isMarking},done:this._usersLoaded.bind(this),fail:notification.exception}]),!0)},GradingNavigation.prototype._usersLoaded=function(users){if(this._firstLoadUsers=!1,this._filteredUsers=this._users=users,this._users.length){var toggleLink=this._region.find('[data-region="user-filters"]');$(document.getElementById(toggleLink.attr("aria-controls"))).find('select[name="filter"]').trigger("change"),$('[data-region="grade-panel"]').show(),$('[data-region="grade-actions-panel"]').show()}else this._selectNoUser();this._triggerNextUserEvent()},GradingNavigation.prototype._checkClickOutsideConfigureFilters=function(event){var configPanel=this._region.find('[data-region="configure-filters"]');if(!configPanel.is(event.target)&&0===configPanel.has(event.target).length){var toggleLink=this._region.find('[data-region="user-filters"]');configPanel.hide(),configPanel.attr("aria-hidden","true"),toggleLink.attr("aria-expanded","false"),$(document).unbind("click.mod_assign_grading_navigation")}},GradingNavigation.prototype._updateFilterPreferences=function(userId,filterList,preferenceNames){var preferences=[],i=0;if(0==filterList.length||this._firstLoadUsers){var deferred=$.Deferred();return deferred.resolve(),deferred}for(i=0;i0&&!isNaN(useridnumber)&&useridnumber>0&&$(document).trigger("user-changed",useridnumber)))},GradingNavigation.prototype._toggleExpandFilters=function(event){event.preventDefault();var toggleLink=$(event.target).closest('[data-region="user-filters"]'),expanded="true"==toggleLink.attr("aria-expanded"),configPanel=$(document.getElementById(toggleLink.attr("aria-controls")));expanded?(configPanel.hide(),configPanel.attr("aria-hidden","true"),toggleLink.attr("aria-expanded","false"),$(document).unbind("click.mod_assign_grading_navigation")):(configPanel.css("display","inline-block"),configPanel.attr("aria-hidden","false"),toggleLink.attr("aria-expanded","true"),event.stopPropagation(),$(document).on("click.mod_assign_grading_navigation",this._checkClickOutsideConfigureFilters.bind(this)))},GradingNavigation.prototype._toggleResetTable=function(){let url=new URL(window.location);url.searchParams.set("treset","1"),window.location.href=url},GradingNavigation.prototype._handlePreviousUser=function(e){e.preventDefault();var currentUserId=this._region.find("[data-action=change-user]").attr("data-selected"),i=0,currentIndex=0;for(i=0;i0&&$(document).trigger("user-changed",userid)}else count&&this._selectUserById(this._filteredUsers[newIndex].id)},GradingNavigation.prototype._setCountString=function(x,y){var updateNumber;this._lastXofYUpdate++,updateNumber=this._lastXofYUpdate;var param={x:x,y:y};str.get_string("xofy","mod_assign",param).done(function(s){updateNumber==this._lastXofYUpdate&&this._region.find('[data-region="user-count-summary"]').text(s)}.bind(this)).fail(notification.exception)},GradingNavigation.prototype._refreshCount=function(){var userid=this._region.find("[data-action=change-user]").attr("data-selected"),i=0,currentIndex=0;if(isNaN(userid)||userid<=0)this._region.find('[data-region="user-count"]').hide();else{for(this._region.find('[data-region="user-count"]').show(),i=0;i0){var url=new URL(window.location);if(parseInt(url.searchParams.get("blindid"))>0){var newid=this._filteredUsers[currentIndex-1].recordid;url.searchParams.set("blindid",newid)}else url.searchParams.set("userid",userid);window.history.replaceState({},"",url)}}},GradingNavigation.prototype._refreshSelector=function(event,userid){var select=this._region.find("[data-action=change-user]");userid=parseInt(userid,10),!isNaN(userid)&&userid>0&&select.attr("data-selected",userid),this._refreshCount()},GradingNavigation.prototype._triggerNextUserEvent=function(){this._filteredUsers.length>1?$(document).trigger("next-user",{nextUserId:null,nextUser:!0}):$(document).trigger("next-user",{nextUser:!1})},GradingNavigation.prototype._handleChangeUser=function(){var select=this._region.find("[data-action=change-user]"),userid=parseInt(select.val(),10);this._isLoading||(checker.checkFormForChanges('[data-region="grade-panel"] .gradeform')?str.get_strings([{key:"unsavedchanges",component:"mod_assign"},{key:"unsavedchangesquestion",component:"mod_assign"},{key:"saveandcontinue",component:"mod_assign"},{key:"cancel",component:"core"}]).done((function(strs){notification.confirm(strs[0],strs[1],strs[2],strs[3],(function(){$(document).trigger("save-changes",userid)}))})):!isNaN(userid)&&userid>0&&(select.attr("data-selected",userid),$(document).trigger("user-changed",userid)))},GradingNavigation})); //# sourceMappingURL=grading_navigation.min.js.map \ No newline at end of file diff --git a/public/mod/assign/amd/build/grading_navigation.min.js.map b/public/mod/assign/amd/build/grading_navigation.min.js.map index 4613eece0d3..43aaf1d292a 100644 --- a/public/mod/assign/amd/build/grading_navigation.min.js.map +++ b/public/mod/assign/amd/build/grading_navigation.min.js.map @@ -1 +1 @@ -{"version":3,"file":"grading_navigation.min.js","sources":["../src/grading_navigation.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript to handle changing users via the user selector in the header.\n *\n * @module mod_assign/grading_navigation\n * @copyright 2016 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete',\n 'core/ajax', 'core_user/repository', 'mod_assign/grading_form_change_checker'],\n function($, notification, str, autocomplete, ajax, UserRepository, checker) {\n\n /**\n * GradingNavigation class.\n *\n * @class mod_assign/grading_navigation\n * @param {String} selector The selector for the page region containing the user navigation.\n */\n var GradingNavigation = function(selector) {\n this._regionSelector = selector;\n this._region = $(selector);\n this._filters = [];\n this._users = [];\n this._filteredUsers = [];\n this._lastXofYUpdate = 0;\n this._firstLoadUsers = true;\n\n let url = new URL(window.location);\n if (parseInt(url.searchParams.get('treset')) > 0) {\n // Remove 'treset' url parameter to make sure that\n // table preferences won't be reset on page refresh.\n url.searchParams.delete('treset');\n window.history.replaceState({}, \"\", url);\n }\n\n // Get the current user list from a webservice.\n this._loadAllUsers();\n\n // We do not allow navigation while ajax requests are pending.\n // Attach listeners to the select and arrow buttons.\n\n this._region.find('[data-action=\"previous-user\"]').on('click', this._handlePreviousUser.bind(this));\n this._region.find('[data-action=\"next-user\"]').on('click', this._handleNextUser.bind(this));\n this._region.find('[data-action=\"change-user\"]').on('change', this._handleChangeUser.bind(this));\n this._region.find('[data-region=\"user-filters\"]').on('click', this._toggleExpandFilters.bind(this));\n this._region.find('[data-region=\"user-resettable\"]').on('click', this._toggleResetTable.bind());\n\n $(document).on('user-changed', this._refreshSelector.bind(this));\n $(document).on('reset-table', this._toggleResetTable.bind(this));\n $(document).on('done-saving-show-next', this._handleNextUser.bind(this));\n\n // Position the configure filters panel under the link that expands it.\n var toggleLink = this._region.find('[data-region=\"user-filters\"]');\n var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));\n\n configPanel.on('change', 'select', this._filterChanged.bind(this));\n\n var userid = $('[data-region=\"grading-navigation-panel\"]').data('first-userid');\n if (userid) {\n this._selectUserById(userid);\n }\n\n str.get_string('changeuser', 'mod_assign').done(function(s) {\n autocomplete.enhance('[data-action=change-user]', false, 'mod_assign/participant_selector', s);\n }\n ).fail(notification.exception);\n\n $(document).bind(\"start-loading-user\", function() {\n this._isLoading = true;\n }.bind(this));\n $(document).bind(\"finish-loading-user\", function() {\n this._isLoading = false;\n }.bind(this));\n };\n\n /** @property {Boolean} Boolean tracking active ajax requests. */\n GradingNavigation.prototype._isLoading = false;\n\n /** @property {String} Selector for the page region containing the user navigation. */\n GradingNavigation.prototype._regionSelector = null;\n\n /** @property {Array} The list of active filter keys */\n GradingNavigation.prototype._filters = null;\n\n /** @property {Array} The list of users */\n GradingNavigation.prototype._users = null;\n\n /** @property {JQuery} JQuery node for the page region containing the user navigation. */\n GradingNavigation.prototype._region = null;\n\n /** @property {String} Last active filters */\n GradingNavigation.prototype._lastFilters = '';\n\n /**\n * Load the list of all users for this assignment.\n *\n * @private\n * @method _loadAllUsers\n * @return {Boolean} True if the user list was fetched.\n */\n GradingNavigation.prototype._loadAllUsers = function() {\n var select = this._region.find('[data-action=change-user]');\n var assignmentid = select.attr('data-assignmentid');\n var groupid = select.attr('data-groupid');\n\n var filterPanel = this._region.find('[data-region=\"configure-filters\"]');\n var filter = filterPanel.find('select[name=\"filter\"]').val();\n var workflowFilter = filterPanel.find('select[name=\"workflowfilter\"]');\n if (workflowFilter) {\n filter += ',' + workflowFilter.val();\n }\n var markerFilter = filterPanel.find('select[name=\"markerfilter\"]');\n if (markerFilter) {\n filter += ',' + markerFilter.val();\n }\n\n if (this._lastFilters == filter) {\n return false;\n }\n this._lastFilters = filter;\n\n ajax.call([{\n methodname: 'mod_assign_list_participants',\n args: {assignid: assignmentid, groupid: groupid, filter: '', onlyids: true, tablesort: true},\n done: this._usersLoaded.bind(this),\n fail: notification.exception\n }]);\n return true;\n };\n\n /**\n * Call back to rebuild the user selector and x of y info when the user list is updated.\n *\n * @private\n * @method _usersLoaded\n * @param {Array} users\n */\n GradingNavigation.prototype._usersLoaded = function(users) {\n this._firstLoadUsers = false;\n this._filteredUsers = this._users = users;\n if (this._users.length) {\n // Position the configure filters panel under the link that expands it.\n var toggleLink = this._region.find('[data-region=\"user-filters\"]');\n var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));\n\n configPanel.find('select[name=\"filter\"]').trigger('change');\n\n $('[data-region=\"grade-panel\"]').show();\n $('[data-region=\"grade-actions-panel\"]').show();\n } else {\n this._selectNoUser();\n }\n this._triggerNextUserEvent();\n };\n\n /**\n * Close the configure filters panel if a click is detected outside of it.\n *\n * @private\n * @method _checkClickOutsideConfigureFilters\n * @param {Event} event\n */\n GradingNavigation.prototype._checkClickOutsideConfigureFilters = function(event) {\n var configPanel = this._region.find('[data-region=\"configure-filters\"]');\n\n if (!configPanel.is(event.target) && configPanel.has(event.target).length === 0) {\n var toggleLink = this._region.find('[data-region=\"user-filters\"]');\n\n configPanel.hide();\n configPanel.attr('aria-hidden', 'true');\n toggleLink.attr('aria-expanded', 'false');\n $(document).unbind('click.mod_assign_grading_navigation');\n }\n };\n\n /**\n * Close the configure filters panel if a click is detected outside of it.\n *\n * @private\n * @method _updateFilterPreference\n * @param {Number} userId The current user id.\n * @param {Array} filterList The list of current filter values.\n * @param {Array} preferenceNames The names of the preferences to update\n * @return {Promise} Resolved when all the preferences are updated.\n */\n GradingNavigation.prototype._updateFilterPreferences = function(userId, filterList, preferenceNames) {\n var preferences = [],\n i = 0;\n\n if (filterList.length == 0 || this._firstLoadUsers) {\n // Nothing to update.\n var deferred = $.Deferred();\n deferred.resolve();\n return deferred;\n }\n // General filter.\n // Set the user preferences to the current filters.\n for (i = 0; i < filterList.length; i++) {\n var newValue = filterList[i];\n if (newValue == 'none') {\n newValue = '';\n }\n\n preferences.push({\n userid: userId,\n name: preferenceNames[i],\n value: newValue\n });\n }\n\n return UserRepository.setUserPreferences(preferences);\n };\n /**\n * Turn a filter on or off.\n *\n * @private\n * @method _filterChanged\n */\n GradingNavigation.prototype._filterChanged = function() {\n // There are 3 types of filter right now.\n var filterPanel = this._region.find('[data-region=\"configure-filters\"]');\n var filters = filterPanel.find('select');\n var preferenceNames = [];\n\n this._filters = [];\n filters.each(function(idx, ele) {\n var element = $(ele);\n this._filters.push(element.val());\n preferenceNames.push('assign_' + element.prop('name'));\n }.bind(this));\n\n // Update the active filter string.\n var filterlist = [];\n filterPanel.find('option:checked').each(function(idx, ele) {\n filterlist[filterlist.length] = $(ele).text();\n });\n if (filterlist.length) {\n this._region.find('[data-region=\"user-filters\"] span').text(filterlist.join(', '));\n } else {\n str.get_string('nofilters', 'mod_assign').done(function(s) {\n this._region.find('[data-region=\"user-filters\"] span').text(s);\n }.bind(this)).fail(notification.exception);\n }\n\n var select = this._region.find('[data-action=change-user]');\n var currentUserID = select.data('currentuserid');\n this._updateFilterPreferences(currentUserID, this._filters, preferenceNames).then(function() {\n // Reload the list of users to apply the new filters.\n if (!this._loadAllUsers()) {\n var userid = parseInt(select.attr('data-selected'));\n let foundIndex = null;\n // Search the returned users for the current selection.\n $.each(this._filteredUsers, function(index, user) {\n if (userid == user.id) {\n foundIndex = index;\n }\n });\n\n if (this._filteredUsers.length) {\n this._selectUserById(this._filteredUsers[foundIndex ?? 0].id);\n } else {\n this._selectNoUser();\n }\n\n }\n }.bind(this)).catch(notification.exception);\n this._refreshCount();\n };\n\n /**\n * Select no users, because no users match the filters.\n *\n * @private\n * @method _selectNoUser\n */\n GradingNavigation.prototype._selectNoUser = function() {\n // Detect unsaved changes, and offer to save them - otherwise change user right now.\n if (this._isLoading) {\n return;\n }\n\n $('[data-region=\"grade-panel\"]').hide();\n $('[data-region=\"grade-actions-panel\"]').hide();\n\n if (checker.checkFormForChanges('[data-region=\"grade-panel\"] .gradeform')) {\n // Form has changes, so we need to confirm before switching users.\n str.get_strings([\n {key: 'unsavedchanges', component: 'mod_assign'},\n {key: 'unsavedchangesquestion', component: 'mod_assign'},\n {key: 'saveandcontinue', component: 'mod_assign'},\n {key: 'cancel', component: 'core'},\n ]).done(function(strs) {\n notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {\n $(document).trigger('save-changes', -1);\n });\n });\n } else {\n $(document).trigger('user-changed', -1);\n }\n };\n\n /**\n * Select the specified user by id.\n *\n * @private\n * @method _selectUserById\n * @param {Number} userid\n */\n GradingNavigation.prototype._selectUserById = function(userid) {\n var select = this._region.find('[data-action=change-user]');\n var useridnumber = parseInt(userid, 10);\n\n // Detect unsaved changes, and offer to save them - otherwise change user right now.\n if (this._isLoading) {\n return;\n }\n if (checker.checkFormForChanges('[data-region=\"grade-panel\"] .gradeform')) {\n // Form has changes, so we need to confirm before switching users.\n str.get_strings([\n {key: 'unsavedchanges', component: 'mod_assign'},\n {key: 'unsavedchangesquestion', component: 'mod_assign'},\n {key: 'saveandcontinue', component: 'mod_assign'},\n {key: 'cancel', component: 'core'},\n ]).done(function(strs) {\n notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {\n $(document).trigger('save-changes', useridnumber);\n });\n });\n } else {\n select.attr('data-selected', userid);\n\n // If we have some filtered users, and userid is specified, then trigger change.\n if (this._filteredUsers.length > 0 && !isNaN(useridnumber) && useridnumber > 0) {\n $(document).trigger('user-changed', useridnumber);\n }\n }\n };\n\n /**\n * Expand or collapse the filter config panel.\n *\n * @private\n * @method _toggleExpandFilters\n * @param {Event} event\n */\n GradingNavigation.prototype._toggleExpandFilters = function(event) {\n event.preventDefault();\n var toggleLink = $(event.target).closest('[data-region=\"user-filters\"]');\n var expanded = toggleLink.attr('aria-expanded') == 'true';\n var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));\n\n if (expanded) {\n configPanel.hide();\n configPanel.attr('aria-hidden', 'true');\n toggleLink.attr('aria-expanded', 'false');\n $(document).unbind('click.mod_assign_grading_navigation');\n } else {\n configPanel.css('display', 'inline-block');\n configPanel.attr('aria-hidden', 'false');\n toggleLink.attr('aria-expanded', 'true');\n event.stopPropagation();\n $(document).on('click.mod_assign_grading_navigation', this._checkClickOutsideConfigureFilters.bind(this));\n }\n };\n\n /**\n * Reset table preferences.\n *\n * @private\n * @method _toggleResetTable\n */\n GradingNavigation.prototype._toggleResetTable = function() {\n let url = new URL(window.location);\n url.searchParams.set('treset', '1');\n window.location.href = url;\n };\n\n /**\n * Change to the previous user in the grading list.\n *\n * @private\n * @method _handlePreviousUser\n * @param {Event} e\n */\n GradingNavigation.prototype._handlePreviousUser = function(e) {\n e.preventDefault();\n var select = this._region.find('[data-action=change-user]');\n var currentUserId = select.attr('data-selected');\n var i = 0;\n var currentIndex = 0;\n\n for (i = 0; i < this._filteredUsers.length; i++) {\n if (this._filteredUsers[i].id == currentUserId) {\n currentIndex = i;\n break;\n }\n }\n\n var count = this._filteredUsers.length;\n var newIndex = (currentIndex - 1);\n if (newIndex < 0) {\n newIndex = count - 1;\n }\n\n if (count) {\n this._selectUserById(this._filteredUsers[newIndex].id);\n }\n };\n\n /**\n * Change to the next user in the grading list.\n *\n * @param {Event} e\n * @param {Boolean} saved Has the form already been saved? Skips checking for changes if true.\n */\n GradingNavigation.prototype._handleNextUser = function(e, saved) {\n e.preventDefault();\n var select = this._region.find('[data-action=change-user]');\n var currentUserId = select.attr('data-selected');\n var i = 0;\n var currentIndex = 0;\n\n for (i = 0; i < this._filteredUsers.length; i++) {\n if (this._filteredUsers[i].id == currentUserId) {\n currentIndex = i;\n break;\n }\n }\n\n var count = this._filteredUsers.length;\n var newIndex = (currentIndex + 1) % count;\n\n if (saved && count) {\n // If we've already saved the grade, skip checking if we've made any changes.\n var userid = this._filteredUsers[newIndex].id;\n var useridnumber = parseInt(userid, 10);\n select.attr('data-selected', userid);\n if (!isNaN(useridnumber) && useridnumber > 0) {\n $(document).trigger('user-changed', userid);\n }\n } else if (count) {\n this._selectUserById(this._filteredUsers[newIndex].id);\n }\n };\n\n /**\n * Set count string. This method only sets the value for the last time it was ever called to deal\n * with promises that return in a non-predictable order.\n *\n * @private\n * @method _setCountString\n * @param {Number} x\n * @param {Number} y\n */\n GradingNavigation.prototype._setCountString = function(x, y) {\n var updateNumber = 0;\n this._lastXofYUpdate++;\n updateNumber = this._lastXofYUpdate;\n\n var param = {x: x, y: y};\n str.get_string('xofy', 'mod_assign', param).done(function(s) {\n if (updateNumber == this._lastXofYUpdate) {\n this._region.find('[data-region=\"user-count-summary\"]').text(s);\n }\n }.bind(this)).fail(notification.exception);\n };\n\n /**\n * Rebuild the x of y string.\n *\n * @private\n * @method _refreshCount\n */\n GradingNavigation.prototype._refreshCount = function() {\n var select = this._region.find('[data-action=change-user]');\n var userid = select.attr('data-selected');\n var i = 0;\n var currentIndex = 0;\n\n if (isNaN(userid) || userid <= 0) {\n this._region.find('[data-region=\"user-count\"]').hide();\n } else {\n this._region.find('[data-region=\"user-count\"]').show();\n\n for (i = 0; i < this._filteredUsers.length; i++) {\n if (this._filteredUsers[i].id == userid) {\n currentIndex = i;\n break;\n }\n }\n var count = this._filteredUsers.length;\n if (count) {\n currentIndex += 1;\n }\n this._setCountString(currentIndex, count);\n // Update window URL\n if (currentIndex > 0) {\n var url = new URL(window.location);\n if (parseInt(url.searchParams.get('blindid')) > 0) {\n var newid = this._filteredUsers[currentIndex - 1].recordid;\n url.searchParams.set('blindid', newid);\n } else {\n url.searchParams.set('userid', userid);\n }\n // We do this so a browser refresh will return to the same user.\n window.history.replaceState({}, \"\", url);\n }\n }\n };\n\n /**\n * Respond to a user-changed event by updating the selector.\n *\n * @private\n * @method _refreshSelector\n * @param {Event} event\n * @param {String} userid\n */\n GradingNavigation.prototype._refreshSelector = function(event, userid) {\n var select = this._region.find('[data-action=change-user]');\n userid = parseInt(userid, 10);\n\n if (!isNaN(userid) && userid > 0) {\n select.attr('data-selected', userid);\n }\n this._refreshCount();\n };\n\n /**\n * Trigger the next user event depending on the number of filtered users\n *\n * @private\n * @method _triggerNextUserEvent\n */\n GradingNavigation.prototype._triggerNextUserEvent = function() {\n if (this._filteredUsers.length > 1) {\n $(document).trigger('next-user', {nextUserId: null, nextUser: true});\n } else {\n $(document).trigger('next-user', {nextUser: false});\n }\n };\n\n /**\n * Change to a different user in the grading list.\n *\n * @private\n * @method _handleChangeUser\n */\n GradingNavigation.prototype._handleChangeUser = function() {\n var select = this._region.find('[data-action=change-user]');\n var userid = parseInt(select.val(), 10);\n\n if (this._isLoading) {\n return;\n }\n if (checker.checkFormForChanges('[data-region=\"grade-panel\"] .gradeform')) {\n // Form has changes, so we need to confirm before switching users.\n str.get_strings([\n {key: 'unsavedchanges', component: 'mod_assign'},\n {key: 'unsavedchangesquestion', component: 'mod_assign'},\n {key: 'saveandcontinue', component: 'mod_assign'},\n {key: 'cancel', component: 'core'},\n ]).done(function(strs) {\n notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {\n $(document).trigger('save-changes', userid);\n });\n });\n } else {\n if (!isNaN(userid) && userid > 0) {\n select.attr('data-selected', userid);\n\n $(document).trigger('user-changed', userid);\n }\n }\n };\n\n return GradingNavigation;\n});\n"],"names":["define","$","notification","str","autocomplete","ajax","UserRepository","checker","GradingNavigation","selector","_regionSelector","_region","_filters","_users","_filteredUsers","_lastXofYUpdate","_firstLoadUsers","url","URL","window","location","parseInt","searchParams","get","delete","history","replaceState","_loadAllUsers","find","on","this","_handlePreviousUser","bind","_handleNextUser","_handleChangeUser","_toggleExpandFilters","_toggleResetTable","document","_refreshSelector","toggleLink","getElementById","attr","_filterChanged","userid","data","_selectUserById","get_string","done","s","enhance","fail","exception","_isLoading","prototype","_lastFilters","select","assignmentid","groupid","filterPanel","filter","val","workflowFilter","markerFilter","call","methodname","args","assignid","onlyids","tablesort","_usersLoaded","users","length","trigger","show","_selectNoUser","_triggerNextUserEvent","_checkClickOutsideConfigureFilters","event","configPanel","is","target","has","hide","unbind","_updateFilterPreferences","userId","filterList","preferenceNames","preferences","i","deferred","Deferred","resolve","newValue","push","name","value","setUserPreferences","filters","each","idx","ele","element","prop","filterlist","text","join","currentUserID","then","foundIndex","index","user","id","catch","_refreshCount","checkFormForChanges","get_strings","key","component","strs","confirm","useridnumber","isNaN","preventDefault","closest","expanded","css","stopPropagation","set","href","e","currentUserId","currentIndex","count","newIndex","saved","_setCountString","x","y","updateNumber","param","newid","recordid","nextUserId","nextUser"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"grading_navigation.min.js","sources":["../src/grading_navigation.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript to handle changing users via the user selector in the header.\n *\n * @module mod_assign/grading_navigation\n * @copyright 2016 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete',\n 'core/ajax', 'core_user/repository', 'mod_assign/grading_form_change_checker'],\n function($, notification, str, autocomplete, ajax, UserRepository, checker) {\n\n /**\n * GradingNavigation class.\n *\n * @class mod_assign/grading_navigation\n * @param {String} selector The selector for the page region containing the user navigation.\n */\n var GradingNavigation = function(selector) {\n this._regionSelector = selector;\n this._region = $(selector);\n this._filters = [];\n this._users = [];\n this._filteredUsers = [];\n this._lastXofYUpdate = 0;\n this._firstLoadUsers = true;\n this._isMarking = false;\n\n let url = new URL(window.location);\n if (parseInt(url.searchParams.get('treset')) > 0) {\n // Remove 'treset' url parameter to make sure that\n // table preferences won't be reset on page refresh.\n url.searchParams.delete('treset');\n window.history.replaceState({}, \"\", url);\n }\n\n // Get the current user list from a webservice.\n this._loadAllUsers();\n\n // We do not allow navigation while ajax requests are pending.\n // Attach listeners to the select and arrow buttons.\n\n this._region.find('[data-action=\"previous-user\"]').on('click', this._handlePreviousUser.bind(this));\n this._region.find('[data-action=\"next-user\"]').on('click', this._handleNextUser.bind(this));\n this._region.find('[data-action=\"change-user\"]').on('change', this._handleChangeUser.bind(this));\n this._region.find('[data-region=\"user-filters\"]').on('click', this._toggleExpandFilters.bind(this));\n this._region.find('[data-region=\"user-resettable\"]').on('click', this._toggleResetTable.bind());\n\n $(document).on('user-changed', this._refreshSelector.bind(this));\n $(document).on('reset-table', this._toggleResetTable.bind(this));\n $(document).on('done-saving-show-next', this._handleNextUser.bind(this));\n\n // Position the configure filters panel under the link that expands it.\n var toggleLink = this._region.find('[data-region=\"user-filters\"]');\n var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));\n\n configPanel.on('change', 'select', this._filterChanged.bind(this));\n\n var userid = $('[data-region=\"grading-navigation-panel\"]').data('first-userid');\n if (userid) {\n this._selectUserById(userid);\n }\n\n str.get_string('changeuser', 'mod_assign').done(function(s) {\n autocomplete.enhance('[data-action=change-user]', false, 'mod_assign/participant_selector', s);\n }\n ).fail(notification.exception);\n\n $(document).bind(\"start-loading-user\", function() {\n this._isLoading = true;\n }.bind(this));\n $(document).bind(\"finish-loading-user\", function() {\n this._isLoading = false;\n }.bind(this));\n };\n\n /** @property {Boolean} Boolean tracking active ajax requests. */\n GradingNavigation.prototype._isLoading = false;\n\n /** @property {String} Selector for the page region containing the user navigation. */\n GradingNavigation.prototype._regionSelector = null;\n\n /** @property {Array} The list of active filter keys */\n GradingNavigation.prototype._filters = null;\n\n /** @property {Array} The list of users */\n GradingNavigation.prototype._users = null;\n\n /** @property {JQuery} JQuery node for the page region containing the user navigation. */\n GradingNavigation.prototype._region = null;\n\n /** @property {String} Last active filters */\n GradingNavigation.prototype._lastFilters = '';\n\n /**\n * Load the list of all users for this assignment.\n *\n * @private\n * @method _loadAllUsers\n * @return {Boolean} True if the user list was fetched.\n */\n GradingNavigation.prototype._loadAllUsers = function() {\n var select = this._region.find('[data-action=change-user]');\n var assignmentid = select.attr('data-assignmentid');\n var groupid = select.attr('data-groupid');\n\n var filterPanel = this._region.find('[data-region=\"configure-filters\"]');\n var filter = filterPanel.find('select[name=\"filter\"]').val();\n var workflowFilter = filterPanel.find('select[name=\"workflowfilter\"]');\n if (workflowFilter) {\n filter += ',' + workflowFilter.val();\n }\n var markerFilter = filterPanel.find('select[name=\"markerfilter\"]');\n if (markerFilter) {\n filter += ',' + markerFilter.val();\n }\n\n if (this._lastFilters == filter) {\n return false;\n }\n this._lastFilters = filter;\n this._isMarking = select.attr('data-ismarking') ?? false;\n\n ajax.call([{\n methodname: 'mod_assign_list_participants',\n args: {assignid: assignmentid, groupid: groupid, filter: '', onlyids: true, tablesort: true, marking: this._isMarking},\n done: this._usersLoaded.bind(this),\n fail: notification.exception\n }]);\n return true;\n };\n\n /**\n * Call back to rebuild the user selector and x of y info when the user list is updated.\n *\n * @private\n * @method _usersLoaded\n * @param {Array} users\n */\n GradingNavigation.prototype._usersLoaded = function(users) {\n this._firstLoadUsers = false;\n this._filteredUsers = this._users = users;\n if (this._users.length) {\n // Position the configure filters panel under the link that expands it.\n var toggleLink = this._region.find('[data-region=\"user-filters\"]');\n var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));\n\n configPanel.find('select[name=\"filter\"]').trigger('change');\n\n $('[data-region=\"grade-panel\"]').show();\n $('[data-region=\"grade-actions-panel\"]').show();\n } else {\n this._selectNoUser();\n }\n this._triggerNextUserEvent();\n };\n\n /**\n * Close the configure filters panel if a click is detected outside of it.\n *\n * @private\n * @method _checkClickOutsideConfigureFilters\n * @param {Event} event\n */\n GradingNavigation.prototype._checkClickOutsideConfigureFilters = function(event) {\n var configPanel = this._region.find('[data-region=\"configure-filters\"]');\n\n if (!configPanel.is(event.target) && configPanel.has(event.target).length === 0) {\n var toggleLink = this._region.find('[data-region=\"user-filters\"]');\n\n configPanel.hide();\n configPanel.attr('aria-hidden', 'true');\n toggleLink.attr('aria-expanded', 'false');\n $(document).unbind('click.mod_assign_grading_navigation');\n }\n };\n\n /**\n * Close the configure filters panel if a click is detected outside of it.\n *\n * @private\n * @method _updateFilterPreference\n * @param {Number} userId The current user id.\n * @param {Array} filterList The list of current filter values.\n * @param {Array} preferenceNames The names of the preferences to update\n * @return {Promise} Resolved when all the preferences are updated.\n */\n GradingNavigation.prototype._updateFilterPreferences = function(userId, filterList, preferenceNames) {\n var preferences = [],\n i = 0;\n\n if (filterList.length == 0 || this._firstLoadUsers) {\n // Nothing to update.\n var deferred = $.Deferred();\n deferred.resolve();\n return deferred;\n }\n // General filter.\n // Set the user preferences to the current filters.\n for (i = 0; i < filterList.length; i++) {\n var newValue = filterList[i];\n if (newValue == 'none') {\n newValue = '';\n }\n\n preferences.push({\n userid: userId,\n name: preferenceNames[i],\n value: newValue\n });\n }\n\n return UserRepository.setUserPreferences(preferences);\n };\n /**\n * Turn a filter on or off.\n *\n * @private\n * @method _filterChanged\n */\n GradingNavigation.prototype._filterChanged = function() {\n // There are 3 types of filter right now.\n var filterPanel = this._region.find('[data-region=\"configure-filters\"]');\n var filters = filterPanel.find('select');\n var preferenceNames = [];\n\n this._filters = [];\n filters.each(function(idx, ele) {\n var element = $(ele);\n this._filters.push(element.val());\n preferenceNames.push('assign_' + element.prop('name'));\n }.bind(this));\n\n // Update the active filter string.\n var filterlist = [];\n filterPanel.find('option:checked').each(function(idx, ele) {\n filterlist[filterlist.length] = $(ele).text();\n });\n if (filterlist.length) {\n this._region.find('[data-region=\"user-filters\"] span').text(filterlist.join(', '));\n } else {\n str.get_string('nofilters', 'mod_assign').done(function(s) {\n this._region.find('[data-region=\"user-filters\"] span').text(s);\n }.bind(this)).fail(notification.exception);\n }\n\n var select = this._region.find('[data-action=change-user]');\n var currentUserID = select.data('currentuserid');\n this._updateFilterPreferences(currentUserID, this._filters, preferenceNames).then(function() {\n // Reload the list of users to apply the new filters.\n if (!this._loadAllUsers()) {\n var userid = parseInt(select.attr('data-selected'));\n let foundIndex = null;\n // Search the returned users for the current selection.\n $.each(this._filteredUsers, function(index, user) {\n if (userid == user.id) {\n foundIndex = index;\n }\n });\n\n if (this._filteredUsers.length) {\n this._selectUserById(this._filteredUsers[foundIndex ?? 0].id);\n } else {\n this._selectNoUser();\n }\n\n }\n }.bind(this)).catch(notification.exception);\n this._refreshCount();\n };\n\n /**\n * Select no users, because no users match the filters.\n *\n * @private\n * @method _selectNoUser\n */\n GradingNavigation.prototype._selectNoUser = function() {\n // Detect unsaved changes, and offer to save them - otherwise change user right now.\n if (this._isLoading) {\n return;\n }\n\n $('[data-region=\"grade-panel\"]').hide();\n $('[data-region=\"grade-actions-panel\"]').hide();\n\n if (checker.checkFormForChanges('[data-region=\"grade-panel\"] .gradeform')) {\n // Form has changes, so we need to confirm before switching users.\n str.get_strings([\n {key: 'unsavedchanges', component: 'mod_assign'},\n {key: 'unsavedchangesquestion', component: 'mod_assign'},\n {key: 'saveandcontinue', component: 'mod_assign'},\n {key: 'cancel', component: 'core'},\n ]).done(function(strs) {\n notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {\n $(document).trigger('save-changes', -1);\n });\n });\n } else {\n $(document).trigger('user-changed', -1);\n }\n };\n\n /**\n * Select the specified user by id.\n *\n * @private\n * @method _selectUserById\n * @param {Number} userid\n */\n GradingNavigation.prototype._selectUserById = function(userid) {\n var select = this._region.find('[data-action=change-user]');\n var useridnumber = parseInt(userid, 10);\n\n // Detect unsaved changes, and offer to save them - otherwise change user right now.\n if (this._isLoading) {\n return;\n }\n if (checker.checkFormForChanges('[data-region=\"grade-panel\"] .gradeform')) {\n // Form has changes, so we need to confirm before switching users.\n str.get_strings([\n {key: 'unsavedchanges', component: 'mod_assign'},\n {key: 'unsavedchangesquestion', component: 'mod_assign'},\n {key: 'saveandcontinue', component: 'mod_assign'},\n {key: 'cancel', component: 'core'},\n ]).done(function(strs) {\n notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {\n $(document).trigger('save-changes', useridnumber);\n });\n });\n } else {\n select.attr('data-selected', userid);\n\n // If we have some filtered users, and userid is specified, then trigger change.\n if (this._filteredUsers.length > 0 && !isNaN(useridnumber) && useridnumber > 0) {\n $(document).trigger('user-changed', useridnumber);\n }\n }\n };\n\n /**\n * Expand or collapse the filter config panel.\n *\n * @private\n * @method _toggleExpandFilters\n * @param {Event} event\n */\n GradingNavigation.prototype._toggleExpandFilters = function(event) {\n event.preventDefault();\n var toggleLink = $(event.target).closest('[data-region=\"user-filters\"]');\n var expanded = toggleLink.attr('aria-expanded') == 'true';\n var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));\n\n if (expanded) {\n configPanel.hide();\n configPanel.attr('aria-hidden', 'true');\n toggleLink.attr('aria-expanded', 'false');\n $(document).unbind('click.mod_assign_grading_navigation');\n } else {\n configPanel.css('display', 'inline-block');\n configPanel.attr('aria-hidden', 'false');\n toggleLink.attr('aria-expanded', 'true');\n event.stopPropagation();\n $(document).on('click.mod_assign_grading_navigation', this._checkClickOutsideConfigureFilters.bind(this));\n }\n };\n\n /**\n * Reset table preferences.\n *\n * @private\n * @method _toggleResetTable\n */\n GradingNavigation.prototype._toggleResetTable = function() {\n let url = new URL(window.location);\n url.searchParams.set('treset', '1');\n window.location.href = url;\n };\n\n /**\n * Change to the previous user in the grading list.\n *\n * @private\n * @method _handlePreviousUser\n * @param {Event} e\n */\n GradingNavigation.prototype._handlePreviousUser = function(e) {\n e.preventDefault();\n var select = this._region.find('[data-action=change-user]');\n var currentUserId = select.attr('data-selected');\n var i = 0;\n var currentIndex = 0;\n\n for (i = 0; i < this._filteredUsers.length; i++) {\n if (this._filteredUsers[i].id == currentUserId) {\n currentIndex = i;\n break;\n }\n }\n\n var count = this._filteredUsers.length;\n var newIndex = (currentIndex - 1);\n if (newIndex < 0) {\n newIndex = count - 1;\n }\n\n if (count) {\n this._selectUserById(this._filteredUsers[newIndex].id);\n }\n };\n\n /**\n * Change to the next user in the grading list.\n *\n * @param {Event} e\n * @param {Boolean} saved Has the form already been saved? Skips checking for changes if true.\n */\n GradingNavigation.prototype._handleNextUser = function(e, saved) {\n e.preventDefault();\n var select = this._region.find('[data-action=change-user]');\n var currentUserId = select.attr('data-selected');\n var i = 0;\n var currentIndex = 0;\n\n for (i = 0; i < this._filteredUsers.length; i++) {\n if (this._filteredUsers[i].id == currentUserId) {\n currentIndex = i;\n break;\n }\n }\n\n var count = this._filteredUsers.length;\n var newIndex = (currentIndex + 1) % count;\n\n if (saved && count) {\n // If we've already saved the grade, skip checking if we've made any changes.\n var userid = this._filteredUsers[newIndex].id;\n var useridnumber = parseInt(userid, 10);\n select.attr('data-selected', userid);\n if (!isNaN(useridnumber) && useridnumber > 0) {\n $(document).trigger('user-changed', userid);\n }\n } else if (count) {\n this._selectUserById(this._filteredUsers[newIndex].id);\n }\n };\n\n /**\n * Set count string. This method only sets the value for the last time it was ever called to deal\n * with promises that return in a non-predictable order.\n *\n * @private\n * @method _setCountString\n * @param {Number} x\n * @param {Number} y\n */\n GradingNavigation.prototype._setCountString = function(x, y) {\n var updateNumber = 0;\n this._lastXofYUpdate++;\n updateNumber = this._lastXofYUpdate;\n\n var param = {x: x, y: y};\n str.get_string('xofy', 'mod_assign', param).done(function(s) {\n if (updateNumber == this._lastXofYUpdate) {\n this._region.find('[data-region=\"user-count-summary\"]').text(s);\n }\n }.bind(this)).fail(notification.exception);\n };\n\n /**\n * Rebuild the x of y string.\n *\n * @private\n * @method _refreshCount\n */\n GradingNavigation.prototype._refreshCount = function() {\n var select = this._region.find('[data-action=change-user]');\n var userid = select.attr('data-selected');\n var i = 0;\n var currentIndex = 0;\n\n if (isNaN(userid) || userid <= 0) {\n this._region.find('[data-region=\"user-count\"]').hide();\n } else {\n this._region.find('[data-region=\"user-count\"]').show();\n\n for (i = 0; i < this._filteredUsers.length; i++) {\n if (this._filteredUsers[i].id == userid) {\n currentIndex = i;\n break;\n }\n }\n var count = this._filteredUsers.length;\n if (count) {\n currentIndex += 1;\n }\n this._setCountString(currentIndex, count);\n // Update window URL\n if (currentIndex > 0) {\n var url = new URL(window.location);\n if (parseInt(url.searchParams.get('blindid')) > 0) {\n var newid = this._filteredUsers[currentIndex - 1].recordid;\n url.searchParams.set('blindid', newid);\n } else {\n url.searchParams.set('userid', userid);\n }\n // We do this so a browser refresh will return to the same user.\n window.history.replaceState({}, \"\", url);\n }\n }\n };\n\n /**\n * Respond to a user-changed event by updating the selector.\n *\n * @private\n * @method _refreshSelector\n * @param {Event} event\n * @param {String} userid\n */\n GradingNavigation.prototype._refreshSelector = function(event, userid) {\n var select = this._region.find('[data-action=change-user]');\n userid = parseInt(userid, 10);\n\n if (!isNaN(userid) && userid > 0) {\n select.attr('data-selected', userid);\n }\n this._refreshCount();\n };\n\n /**\n * Trigger the next user event depending on the number of filtered users\n *\n * @private\n * @method _triggerNextUserEvent\n */\n GradingNavigation.prototype._triggerNextUserEvent = function() {\n if (this._filteredUsers.length > 1) {\n $(document).trigger('next-user', {nextUserId: null, nextUser: true});\n } else {\n $(document).trigger('next-user', {nextUser: false});\n }\n };\n\n /**\n * Change to a different user in the grading list.\n *\n * @private\n * @method _handleChangeUser\n */\n GradingNavigation.prototype._handleChangeUser = function() {\n var select = this._region.find('[data-action=change-user]');\n var userid = parseInt(select.val(), 10);\n\n if (this._isLoading) {\n return;\n }\n if (checker.checkFormForChanges('[data-region=\"grade-panel\"] .gradeform')) {\n // Form has changes, so we need to confirm before switching users.\n str.get_strings([\n {key: 'unsavedchanges', component: 'mod_assign'},\n {key: 'unsavedchangesquestion', component: 'mod_assign'},\n {key: 'saveandcontinue', component: 'mod_assign'},\n {key: 'cancel', component: 'core'},\n ]).done(function(strs) {\n notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {\n $(document).trigger('save-changes', userid);\n });\n });\n } else {\n if (!isNaN(userid) && userid > 0) {\n select.attr('data-selected', userid);\n\n $(document).trigger('user-changed', userid);\n }\n }\n };\n\n return GradingNavigation;\n});\n"],"names":["define","$","notification","str","autocomplete","ajax","UserRepository","checker","GradingNavigation","selector","_regionSelector","_region","_filters","_users","_filteredUsers","_lastXofYUpdate","_firstLoadUsers","_isMarking","url","URL","window","location","parseInt","searchParams","get","delete","history","replaceState","_loadAllUsers","find","on","this","_handlePreviousUser","bind","_handleNextUser","_handleChangeUser","_toggleExpandFilters","_toggleResetTable","document","_refreshSelector","toggleLink","getElementById","attr","_filterChanged","userid","data","_selectUserById","get_string","done","s","enhance","fail","exception","_isLoading","prototype","_lastFilters","select","assignmentid","groupid","filterPanel","filter","val","workflowFilter","markerFilter","call","methodname","args","assignid","onlyids","tablesort","marking","_usersLoaded","users","length","trigger","show","_selectNoUser","_triggerNextUserEvent","_checkClickOutsideConfigureFilters","event","configPanel","is","target","has","hide","unbind","_updateFilterPreferences","userId","filterList","preferenceNames","preferences","i","deferred","Deferred","resolve","newValue","push","name","value","setUserPreferences","filters","each","idx","ele","element","prop","filterlist","text","join","currentUserID","then","foundIndex","index","user","id","catch","_refreshCount","checkFormForChanges","get_strings","key","component","strs","confirm","useridnumber","isNaN","preventDefault","closest","expanded","css","stopPropagation","set","href","e","currentUserId","currentIndex","count","newIndex","saved","_setCountString","x","y","updateNumber","param","newid","recordid","nextUserId","nextUser"],"mappings":"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"} \ No newline at end of file diff --git a/public/mod/assign/amd/build/grading_panel.min.js b/public/mod/assign/amd/build/grading_panel.min.js index 5ae74134d02..3e2bbd7da6e 100644 --- a/public/mod/assign/amd/build/grading_panel.min.js +++ b/public/mod/assign/amd/build/grading_panel.min.js @@ -6,6 +6,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ -define("mod_assign/grading_panel",["jquery","core/yui","core/notification","core/templates","core/fragment","core/ajax","core/str","mod_assign/grading_form_change_checker","mod_assign/grading_events","core_form/events","core/toast","core_form/changechecker"],(function($,Y,notification,templates,fragment,ajax,str,checker,GradingEvents,FormEvents,Toast,FormChangeChecker){var GradingPanel=function(selector){this._regionSelector=selector,this._region=$(selector),this._userCache=[],this.registerEventListeners()};return GradingPanel.prototype._regionSelector=null,GradingPanel.prototype._lastUserId=0,GradingPanel.prototype._lastAttemptNumber=-1,GradingPanel.prototype._region=null,GradingPanel.prototype.nextUserId=null,GradingPanel.prototype.nextUser=!1,GradingPanel.prototype._niceReplaceNodeContents=function(node,html,js){var promise=$.Deferred();return node.fadeOut("fast",(function(){templates.replaceNodeContents(node,html,js),node.fadeIn("fast",(function(){promise.resolve()}))})),promise.promise()},GradingPanel.prototype._saveFormState=function(){var checked=$('[data-region="grading-actions-form"] [name="sendstudentnotifications"]').prop("checked");$('.gradeform [name="sendstudentnotifications"]').val(checked)},GradingPanel.prototype._submitForm=function(event,nextUserId,nextUser){var commentAreaElement=document.querySelector(".comment-area");commentAreaElement&&(""!==commentAreaElement.querySelector(".db > textarea").value&&commentAreaElement.querySelector('.fd a[id^="comment-action-post-"]').click());var form=$(this._region.find("form.gradeform"));$('[data-region="overlay"]').show(),FormChangeChecker.markFormSubmitted(form[0]),form.trigger("save-form-state"),FormEvents.notifyFormSubmittedByJavascript(form[0]);var data=form.serialize(),assignmentid=this._region.attr("data-assignmentid");ajax.call([{methodname:"mod_assign_submit_grading_form",args:{assignmentid:assignmentid,userid:this._lastUserId,jsonformdata:JSON.stringify(data)},done:this._handleFormSubmissionResponse.bind(this,data,nextUserId,nextUser),fail:notification.exception}])},GradingPanel.prototype._handleFormSubmissionResponse=function(formdata,nextUserId,nextUser,response){if(void 0===nextUserId&&(nextUserId=this._lastUserId),response.length)str.get_string("errorgradechangessaveddetail","mod_assign").then((function(str){return Toast.add(str,{type:"danger",delay:4e3}),str})).catch(notification.exception),$(document).trigger("reset",[this._lastUserId,formdata,!0]);else{str.get_string("gradechangessaveddetail","mod_assign").then((function(str){return Toast.add(str),str})).catch(notification.exception);var form=$(this._region.find("form.gradeform"));FormChangeChecker.resetFormDirtyState(form[0]),nextUserId==this._lastUserId?$(document).trigger("reset",nextUserId):nextUser?$(document).trigger("done-saving-show-next",!0):$(document).trigger("user-changed",nextUserId)}$('[data-region="overlay"]').hide()},GradingPanel.prototype._resetForm=function(e,userid,formdata,unresolvederror){var event=$.Event("custom");void 0===userid&&(userid=this._lastUserId),this._lastUserId=0,this._refreshGradingPanel(event,userid,formdata,-1,unresolvederror)},GradingPanel.prototype._chooseAttempt=function(e){var submissionsId=$(e.target).data("submissions"),formhtml=$(document.getElementById(submissionsId)).clone().wrap($("
")).html();str.get_strings([{key:"viewadifferentattempt",component:"mod_assign"},{key:"view",component:"core"},{key:"cancel",component:"core"}]).done(function(strs){notification.confirm(strs[0],formhtml,strs[1],strs[2],function(){var attemptnumber=$("input:radio[name='select-attemptnumber']:checked").val();this._refreshGradingPanel(null,this._lastUserId,"",attemptnumber)}.bind(this))}.bind(this)).fail(notification.exception)},GradingPanel.prototype._addPopoutButtons=function(selector){var region=$(selector);templates.render("mod_assign/popout_button",{}).done(function(html){region.find('[data-fieldtype="filemanager"],[data-fieldtype="editor"],[data-fieldtype="grading"]').closest(".fitem").addClass("has-popout").find("label:first").parent().append(html),region.on("click",'[data-region="popout-button"]',this._togglePopout.bind(this))}.bind(this)).fail(notification.exception)},GradingPanel.prototype._togglePopout=function(event){event.preventDefault();var container=$(event.target).closest(".fitem");container.hasClass("popout")?$(".popout").removeClass("popout"):($(".popout").removeClass("popout"),container.addClass("popout"),container.addClass("moodle-has-zindex"))},GradingPanel.prototype._refreshGradingPanel=function(event,userid,submissiondata,attemptnumber,unresolvederror){var contextid=this._region.attr("data-contextid");void 0===submissiondata&&(submissiondata=""),void 0===attemptnumber&&(attemptnumber=-1),void 0===unresolvederror&&(unresolvederror=!1),this._lastUserId==userid&&this._lastAttemptNumber==attemptnumber&&""===submissiondata||(this._lastUserId=userid,this._lastAttemptNumber=attemptnumber,$(document).trigger("start-loading-user"),window.M.util.js_pending("mod-assign-loading-user"),templates.render("mod_assign/loading",{}).done(function(html,js){this._niceReplaceNodeContents(this._region,html,js).done(function(){if(userid>0){this._region.show();var params={userid:userid,attemptnumber:attemptnumber,jsonformdata:JSON.stringify(submissiondata)};fragment.loadFragment("mod_assign","gradingpanel",contextid,params).done(function(html,js){""===html&&$(document).trigger("reset-table",!0),this._niceReplaceNodeContents(this._region,html,js).done(function(){checker.saveFormState('[data-region="grade-panel"] .gradeform'),$(document).on("editor-content-restored",(function(){checker.saveFormState('[data-region="grade-panel"] .gradeform')})),$('[data-region="attempt-chooser"]').on("click",this._chooseAttempt.bind(this)),this._addPopoutButtons('[data-region="grade-panel"] .gradeform'),unresolvederror&&$('[data-region="grade-panel"] .gradeform').data("unresolved-error",!0),$(document).trigger("finish-loading-user"),window.M.util.js_complete("mod-assign-loading-user")}.bind(this)).fail(notification.exception)}.bind(this)).fail(notification.exception),$('[data-region="review-panel"]').show()}else this._region.hide(),$('[data-region="review-panel"]').hide(),$(document).trigger("finish-loading-user"),window.M.util.js_complete("mod-assign-loading-user")}.bind(this))}.bind(this)).fail(notification.exception))},GradingPanel.prototype._getNextUser=function(event,data){this.nextUserId=data.nextUserId,this.nextUser=data.nextUser},GradingPanel.prototype._handleSaveAndShowNext=function(){this._submitForm(null,this.nextUserId,this.nextUser)},GradingPanel.prototype.getPanelElement=function(){return $('[data-region="grade-panel"]')},GradingPanel.prototype.collapsePanel=function(){this.getPanelElement().addClass("collapsed")},GradingPanel.prototype.expandPanel=function(){this.getPanelElement().removeClass("collapsed")},GradingPanel.prototype.registerEventListeners=function(){var docElement=$(document);$(this._region).on("submit","form",(function(e){e.preventDefault()})),docElement.on("next-user",this._getNextUser.bind(this)),docElement.on("user-changed",this._refreshGradingPanel.bind(this)),docElement.on("save-changes",this._submitForm.bind(this)),docElement.on("save-and-show-next",this._handleSaveAndShowNext.bind(this)),docElement.on("reset",this._resetForm.bind(this)),docElement.on("save-form-state",this._saveFormState.bind(this)),docElement.on(GradingEvents.COLLAPSE_GRADE_PANEL,function(){this.collapsePanel()}.bind(this)),docElement.on(GradingEvents.COLLAPSE_REVIEW_PANEL,function(){this.expandPanel()}.bind(this)),docElement.on(GradingEvents.EXPAND_GRADE_PANEL,function(){this.expandPanel()}.bind(this))},GradingPanel})); +define("mod_assign/grading_panel",["jquery","core/yui","core/notification","core/templates","core/fragment","core/ajax","core/str","mod_assign/grading_form_change_checker","mod_assign/grading_events","core_form/events","core/toast","core_form/changechecker"],(function($,Y,notification,templates,fragment,ajax,str,checker,GradingEvents,FormEvents,Toast,FormChangeChecker){var GradingPanel=function(selector){this._regionSelector=selector,this._region=$(selector),this._userCache=[],this.registerEventListeners()};return GradingPanel.prototype._regionSelector=null,GradingPanel.prototype._lastUserId=0,GradingPanel.prototype._lastAttemptNumber=-1,GradingPanel.prototype._region=null,GradingPanel.prototype.nextUserId=null,GradingPanel.prototype.nextUser=!1,GradingPanel.prototype._niceReplaceNodeContents=function(node,html,js){var promise=$.Deferred();return node.fadeOut("fast",(function(){templates.replaceNodeContents(node,html,js),node.fadeIn("fast",(function(){promise.resolve()}))})),promise.promise()},GradingPanel.prototype._saveFormState=function(){var checked=$('[data-region="grading-actions-form"] [name="sendstudentnotifications"]').prop("checked");$('.gradeform [name="sendstudentnotifications"]').val(checked)},GradingPanel.prototype._submitForm=function(event,nextUserId,nextUser){var commentAreaElement=document.querySelector(".comment-area");commentAreaElement&&(""!==commentAreaElement.querySelector(".db > textarea").value&&commentAreaElement.querySelector('.fd a[id^="comment-action-post-"]').click());var form=$(this._region.find("form.gradeform"));$('[data-region="overlay"]').show(),FormChangeChecker.markFormSubmitted(form[0]),form.trigger("save-form-state"),FormEvents.notifyFormSubmittedByJavascript(form[0]);var data=form.serialize(),assignmentid=this._region.attr("data-assignmentid"),marker=window.location.search.substring(1).split("&").includes("action=marker");ajax.call([{methodname:"mod_assign_submit_grading_form",args:{assignmentid:assignmentid,userid:this._lastUserId,marker:marker,jsonformdata:JSON.stringify(data)},done:this._handleFormSubmissionResponse.bind(this,data,nextUserId,nextUser),fail:notification.exception}])},GradingPanel.prototype._handleFormSubmissionResponse=function(formdata,nextUserId,nextUser,response){if(void 0===nextUserId&&(nextUserId=this._lastUserId),response.length)str.get_string("errorgradechangessaveddetail","mod_assign").then((function(str){return Toast.add(str,{type:"danger",delay:4e3}),str})).catch(notification.exception),$(document).trigger("reset",[this._lastUserId,formdata,!0]);else{str.get_string("gradechangessaveddetail","mod_assign").then((function(str){return Toast.add(str),str})).catch(notification.exception);var form=$(this._region.find("form.gradeform"));FormChangeChecker.resetFormDirtyState(form[0]),nextUserId==this._lastUserId?$(document).trigger("reset",nextUserId):nextUser?$(document).trigger("done-saving-show-next",!0):$(document).trigger("user-changed",nextUserId)}$('[data-region="overlay"]').hide()},GradingPanel.prototype._resetForm=function(e,userid,formdata,unresolvederror){var event=$.Event("custom");void 0===userid&&(userid=this._lastUserId),this._lastUserId=0,this._refreshGradingPanel(event,userid,formdata,-1,unresolvederror)},GradingPanel.prototype._chooseAttempt=function(e){var submissionsId=$(e.target).data("submissions"),formhtml=$(document.getElementById(submissionsId)).clone().wrap($("")).html();str.get_strings([{key:"viewadifferentattempt",component:"mod_assign"},{key:"view",component:"core"},{key:"cancel",component:"core"}]).done(function(strs){notification.confirm(strs[0],formhtml,strs[1],strs[2],function(){var attemptnumber=$("input:radio[name='select-attemptnumber']:checked").val();this._refreshGradingPanel(null,this._lastUserId,"",attemptnumber)}.bind(this))}.bind(this)).fail(notification.exception)},GradingPanel.prototype._addPopoutButtons=function(selector){var region=$(selector);templates.render("mod_assign/popout_button",{}).done(function(html){region.find('[data-fieldtype="filemanager"],[data-fieldtype="editor"],[data-fieldtype="grading"]').closest(".fitem").addClass("has-popout").find("label:first").parent().append(html),region.on("click",'[data-region="popout-button"]',this._togglePopout.bind(this))}.bind(this)).fail(notification.exception)},GradingPanel.prototype._togglePopout=function(event){event.preventDefault();var container=$(event.target).closest(".fitem");container.hasClass("popout")?$(".popout").removeClass("popout"):($(".popout").removeClass("popout"),container.addClass("popout"),container.addClass("moodle-has-zindex"))},GradingPanel.prototype._refreshGradingPanel=function(event,userid,submissiondata,attemptnumber,unresolvederror){var contextid=this._region.attr("data-contextid");void 0===submissiondata&&(submissiondata=""),void 0===attemptnumber&&(attemptnumber=-1),void 0===unresolvederror&&(unresolvederror=!1),this._lastUserId==userid&&this._lastAttemptNumber==attemptnumber&&""===submissiondata||(this._lastUserId=userid,this._lastAttemptNumber=attemptnumber,$(document).trigger("start-loading-user"),window.M.util.js_pending("mod-assign-loading-user"),templates.render("mod_assign/loading",{}).done(function(html,js){this._niceReplaceNodeContents(this._region,html,js).done(function(){if(userid>0){this._region.show();var marker=window.location.search.substring(1).split("&").includes("action=marker"),params={userid:userid,attemptnumber:attemptnumber,jsonformdata:JSON.stringify(submissiondata),marker:marker};fragment.loadFragment("mod_assign","gradingpanel",contextid,params).done(function(html,js){""===html&&$(document).trigger("reset-table",!0),this._niceReplaceNodeContents(this._region,html,js).done(function(){checker.saveFormState('[data-region="grade-panel"] .gradeform'),$(document).on("editor-content-restored",(function(){checker.saveFormState('[data-region="grade-panel"] .gradeform')})),$('[data-region="attempt-chooser"]').on("click",this._chooseAttempt.bind(this)),this._addPopoutButtons('[data-region="grade-panel"] .gradeform'),unresolvederror&&$('[data-region="grade-panel"] .gradeform').data("unresolved-error",!0),$(document).trigger("finish-loading-user"),window.M.util.js_complete("mod-assign-loading-user")}.bind(this)).fail(notification.exception)}.bind(this)).fail(notification.exception),$('[data-region="review-panel"]').show()}else this._region.hide(),$('[data-region="review-panel"]').hide(),$(document).trigger("finish-loading-user"),window.M.util.js_complete("mod-assign-loading-user")}.bind(this))}.bind(this)).fail(notification.exception))},GradingPanel.prototype._getNextUser=function(event,data){this.nextUserId=data.nextUserId,this.nextUser=data.nextUser},GradingPanel.prototype._handleSaveAndShowNext=function(){this._submitForm(null,this.nextUserId,this.nextUser)},GradingPanel.prototype.getPanelElement=function(){return $('[data-region="grade-panel"]')},GradingPanel.prototype.collapsePanel=function(){this.getPanelElement().addClass("collapsed")},GradingPanel.prototype.expandPanel=function(){this.getPanelElement().removeClass("collapsed")},GradingPanel.prototype.registerEventListeners=function(){var docElement=$(document);$(this._region).on("submit","form",(function(e){e.preventDefault()})),docElement.on("next-user",this._getNextUser.bind(this)),docElement.on("user-changed",this._refreshGradingPanel.bind(this)),docElement.on("save-changes",this._submitForm.bind(this)),docElement.on("save-and-show-next",this._handleSaveAndShowNext.bind(this)),docElement.on("reset",this._resetForm.bind(this)),docElement.on("save-form-state",this._saveFormState.bind(this)),docElement.on(GradingEvents.COLLAPSE_GRADE_PANEL,function(){this.collapsePanel()}.bind(this)),docElement.on(GradingEvents.COLLAPSE_REVIEW_PANEL,function(){this.expandPanel()}.bind(this)),docElement.on(GradingEvents.EXPAND_GRADE_PANEL,function(){this.expandPanel()}.bind(this))},GradingPanel})); //# sourceMappingURL=grading_panel.min.js.map \ No newline at end of file diff --git a/public/mod/assign/amd/build/grading_panel.min.js.map b/public/mod/assign/amd/build/grading_panel.min.js.map index 661ebf97af1..b531c6bf9f1 100644 --- a/public/mod/assign/amd/build/grading_panel.min.js.map +++ b/public/mod/assign/amd/build/grading_panel.min.js.map @@ -1 +1 @@ -{"version":3,"file":"grading_panel.min.js","sources":["../src/grading_panel.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript controller for the \"Grading\" panel at the right of the page.\n *\n * @module mod_assign/grading_panel\n * @copyright 2016 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine([\n 'jquery',\n 'core/yui',\n 'core/notification',\n 'core/templates',\n 'core/fragment',\n 'core/ajax',\n 'core/str',\n 'mod_assign/grading_form_change_checker',\n 'mod_assign/grading_events',\n 'core_form/events',\n 'core/toast',\n 'core_form/changechecker',\n], function(\n $,\n Y,\n notification,\n templates,\n fragment,\n ajax,\n str,\n checker,\n GradingEvents,\n FormEvents,\n Toast,\n FormChangeChecker\n) {\n\n /**\n * GradingPanel class.\n *\n * @class mod_assign/grading_panel\n * @param {String} selector The selector for the page region containing the user navigation.\n */\n var GradingPanel = function(selector) {\n this._regionSelector = selector;\n this._region = $(selector);\n this._userCache = [];\n\n this.registerEventListeners();\n };\n\n /** @property {String} Selector for the page region containing the user navigation. */\n GradingPanel.prototype._regionSelector = null;\n\n /** @property {Integer} Remember the last user id to prevent unnessecary reloads. */\n GradingPanel.prototype._lastUserId = 0;\n\n /** @property {Integer} Remember the last attempt number to prevent unnessecary reloads. */\n GradingPanel.prototype._lastAttemptNumber = -1;\n\n /** @property {JQuery} JQuery node for the page region containing the user navigation. */\n GradingPanel.prototype._region = null;\n\n /** @property {Integer} The id of the next user in the grading list */\n GradingPanel.prototype.nextUserId = null;\n\n /** @property {Boolean} Next user exists in the grading list */\n GradingPanel.prototype.nextUser = false;\n\n /**\n * Fade the dom node out, update it, and fade it back.\n *\n * @private\n * @method _niceReplaceNodeContents\n * @param {JQuery} node\n * @param {String} html\n * @param {String} js\n * @return {Deferred} promise resolved when the animations are complete.\n */\n GradingPanel.prototype._niceReplaceNodeContents = function(node, html, js) {\n var promise = $.Deferred();\n\n node.fadeOut(\"fast\", function() {\n templates.replaceNodeContents(node, html, js);\n node.fadeIn(\"fast\", function() {\n promise.resolve();\n });\n });\n\n return promise.promise();\n };\n\n /**\n * Make sure all form fields have the latest saved state.\n * @private\n * @method _saveFormState\n */\n GradingPanel.prototype._saveFormState = function() {\n // Copy data from notify students checkbox which was moved out of the form.\n var checked = $('[data-region=\"grading-actions-form\"] [name=\"sendstudentnotifications\"]').prop(\"checked\");\n $('.gradeform [name=\"sendstudentnotifications\"]').val(checked);\n };\n\n /**\n * Make form submit via ajax.\n *\n * @private\n * @param {Object} event\n * @param {Integer} nextUserId\n * @param {Boolean} nextUser optional. Load next user in the grading list.\n * @method _submitForm\n * @fires event:formSubmittedByJavascript\n */\n GradingPanel.prototype._submitForm = function(event, nextUserId, nextUser) {\n // If the form has data in comment-area, then we need to save that comment\n var commentAreaElement = document.querySelector('.comment-area');\n if (commentAreaElement) {\n var commentTextAreaElement = commentAreaElement.querySelector('.db > textarea');\n if (commentTextAreaElement.value !== '') {\n var commentActionPostElement = commentAreaElement.querySelector('.fd a[id^=\"comment-action-post-\"]');\n commentActionPostElement.click();\n }\n }\n\n // The form was submitted - send it via ajax instead.\n var form = $(this._region.find('form.gradeform'));\n\n $('[data-region=\"overlay\"]').show();\n\n // Mark the form as submitted in the change checker.\n FormChangeChecker.markFormSubmitted(form[0]);\n\n // We call this, so other modules can update the form with the latest state.\n form.trigger('save-form-state');\n\n // Tell all form fields we are about to submit the form.\n FormEvents.notifyFormSubmittedByJavascript(form[0]);\n\n // Now we get all the current values from the form.\n var data = form.serialize();\n var assignmentid = this._region.attr('data-assignmentid');\n\n // Now we can continue...\n ajax.call([{\n methodname: 'mod_assign_submit_grading_form',\n args: {assignmentid: assignmentid, userid: this._lastUserId, jsonformdata: JSON.stringify(data)},\n done: this._handleFormSubmissionResponse.bind(this, data, nextUserId, nextUser),\n fail: notification.exception\n }]);\n };\n\n /**\n * Handle form submission response.\n *\n * @private\n * @method _handleFormSubmissionResponse\n * @param {Array} formdata - submitted values\n * @param {Number} [nextUserId] The id of the user to load after the form is saved\n * @param {Boolean} [nextUser] - Whether to switch to next user in the grading list.\n * @param {Array} response List of errors.\n */\n GradingPanel.prototype._handleFormSubmissionResponse = function(formdata, nextUserId, nextUser, response) {\n if (typeof nextUserId === \"undefined\") {\n nextUserId = this._lastUserId;\n }\n if (response.length) {\n str.get_string('errorgradechangessaveddetail', 'mod_assign')\n .then(function(str) {\n Toast.add(str, {type: 'danger', delay: 4000});\n return str;\n })\n .catch(notification.exception);\n\n // There was an error saving the grade. Re-render the form using the submitted data so we can show\n // validation errors.\n $(document).trigger('reset', [this._lastUserId, formdata, true]);\n } else {\n str.get_string('gradechangessaveddetail', 'mod_assign')\n .then(function(str) {\n Toast.add(str);\n return str;\n })\n .catch(notification.exception);\n\n // Reset the form state.\n var form = $(this._region.find('form.gradeform'));\n FormChangeChecker.resetFormDirtyState(form[0]);\n\n if (nextUserId == this._lastUserId) {\n $(document).trigger('reset', nextUserId);\n } else if (nextUser) {\n $(document).trigger('done-saving-show-next', true);\n } else {\n $(document).trigger('user-changed', nextUserId);\n }\n }\n $('[data-region=\"overlay\"]').hide();\n };\n\n /**\n * Refresh form with default values.\n *\n * @private\n * @method _resetForm\n * @param {Event} e\n * @param {Integer} userid\n * @param {Array} formdata\n * @param {Boolean} unresolvederror\n */\n GradingPanel.prototype._resetForm = function(e, userid, formdata, unresolvederror) {\n // The form was cancelled - refresh with default values.\n var event = $.Event(\"custom\");\n if (typeof userid == \"undefined\") {\n userid = this._lastUserId;\n }\n this._lastUserId = 0;\n this._refreshGradingPanel(event, userid, formdata, -1, unresolvederror);\n };\n\n /**\n * Open a picker to choose an older attempt.\n *\n * @private\n * @param {Object} e\n * @method _chooseAttempt\n */\n GradingPanel.prototype._chooseAttempt = function(e) {\n // Show a dialog.\n\n // The form is in the element pointed to by data-submissions.\n var link = $(e.target);\n var submissionsId = link.data('submissions');\n var submissionsform = $(document.getElementById(submissionsId));\n var formcopy = submissionsform.clone();\n var formhtml = formcopy.wrap($('')).html();\n\n str.get_strings([\n {key: 'viewadifferentattempt', component: 'mod_assign'},\n {key: 'view', component: 'core'},\n {key: 'cancel', component: 'core'},\n ]).done(function(strs) {\n notification.confirm(strs[0], formhtml, strs[1], strs[2], function() {\n var attemptnumber = $(\"input:radio[name='select-attemptnumber']:checked\").val();\n\n this._refreshGradingPanel(null, this._lastUserId, '', attemptnumber);\n }.bind(this));\n }.bind(this)).fail(notification.exception);\n };\n\n /**\n * Add popout buttons\n *\n * @private\n * @method _addPopoutButtons\n * @param {JQuery} selector The region selector to add popout buttons to.\n */\n GradingPanel.prototype._addPopoutButtons = function(selector) {\n var region = $(selector);\n\n templates.render('mod_assign/popout_button', {}).done(function(html) {\n var parents = region.find('[data-fieldtype=\"filemanager\"],[data-fieldtype=\"editor\"],[data-fieldtype=\"grading\"]')\n .closest('.fitem');\n parents.addClass('has-popout').find('label:first').parent().append(html);\n\n region.on('click', '[data-region=\"popout-button\"]', this._togglePopout.bind(this));\n }.bind(this)).fail(notification.exception);\n };\n\n /**\n * Make a div \"popout\" or \"popback\".\n *\n * @private\n * @method _togglePopout\n * @param {Event} event\n */\n GradingPanel.prototype._togglePopout = function(event) {\n event.preventDefault();\n var container = $(event.target).closest('.fitem');\n if (container.hasClass('popout')) {\n $('.popout').removeClass('popout');\n } else {\n $('.popout').removeClass('popout');\n container.addClass('popout');\n container.addClass('moodle-has-zindex');\n }\n };\n\n /**\n * Get the user context - re-render the template in the page.\n *\n * @private\n * @method _refreshGradingPanel\n * @param {Event} event\n * @param {Number} userid\n * @param {String} submissiondata serialised submission data.\n * @param {Integer} attemptnumber\n * @param {Boolean} unresolvederror\n */\n GradingPanel.prototype._refreshGradingPanel = function(event, userid, submissiondata, attemptnumber, unresolvederror) {\n var contextid = this._region.attr('data-contextid');\n if (typeof submissiondata === 'undefined') {\n submissiondata = '';\n }\n if (typeof attemptnumber === 'undefined') {\n attemptnumber = -1;\n }\n if (typeof unresolvederror === 'undefined') {\n unresolvederror = false;\n }\n // Skip reloading if it is the same user.\n if (this._lastUserId == userid && this._lastAttemptNumber == attemptnumber && submissiondata === '') {\n return;\n }\n this._lastUserId = userid;\n this._lastAttemptNumber = attemptnumber;\n $(document).trigger('start-loading-user');\n // Tell behat to back off too.\n window.M.util.js_pending('mod-assign-loading-user');\n // First insert the loading template.\n templates.render('mod_assign/loading', {}).done(function(html, js) {\n // Update the page.\n this._niceReplaceNodeContents(this._region, html, js).done(function() {\n if (userid > 0) {\n this._region.show();\n // Reload the grading form \"fragment\" for this user.\n var params = {userid: userid, attemptnumber: attemptnumber, jsonformdata: JSON.stringify(submissiondata)};\n fragment.loadFragment('mod_assign', 'gradingpanel', contextid, params).done(function(html, js) {\n\n // Reset whole grading page when there is a failure in retrieving the html\n // i.e. user no longer under \"requires grading\" filter when graded\n if (html === '') {\n $(document).trigger('reset-table', true);\n }\n\n this._niceReplaceNodeContents(this._region, html, js)\n .done(function() {\n checker.saveFormState('[data-region=\"grade-panel\"] .gradeform');\n $(document).on('editor-content-restored', function() {\n // If the editor has some content that has been restored\n // then save the form state again for comparison.\n checker.saveFormState('[data-region=\"grade-panel\"] .gradeform');\n });\n $('[data-region=\"attempt-chooser\"]').on('click', this._chooseAttempt.bind(this));\n this._addPopoutButtons('[data-region=\"grade-panel\"] .gradeform');\n if (unresolvederror) {\n $('[data-region=\"grade-panel\"] .gradeform').data('unresolved-error', true);\n }\n $(document).trigger('finish-loading-user');\n // Tell behat we are friends again.\n window.M.util.js_complete('mod-assign-loading-user');\n }.bind(this))\n .fail(notification.exception);\n }.bind(this)).fail(notification.exception);\n $('[data-region=\"review-panel\"]').show();\n } else {\n this._region.hide();\n $('[data-region=\"review-panel\"]').hide();\n $(document).trigger('finish-loading-user');\n // Tell behat we are friends again.\n window.M.util.js_complete('mod-assign-loading-user');\n }\n }.bind(this));\n }.bind(this)).fail(notification.exception);\n };\n\n /**\n * Get next user data and store it in global variables\n *\n * @private\n * @method _getNextUser\n * @param {Event} event\n * @param {Object} data Next user's data\n */\n GradingPanel.prototype._getNextUser = function(event, data) {\n this.nextUserId = data.nextUserId;\n this.nextUser = data.nextUser;\n };\n\n /**\n * Handle the save-and-show-next event\n *\n * @private\n * @method _handleSaveAndShowNext\n */\n GradingPanel.prototype._handleSaveAndShowNext = function() {\n this._submitForm(null, this.nextUserId, this.nextUser);\n };\n\n /**\n * Get the grade panel element.\n *\n * @method getPanelElement\n * @return {jQuery}\n */\n GradingPanel.prototype.getPanelElement = function() {\n return $('[data-region=\"grade-panel\"]');\n };\n\n /**\n * Hide the grade panel.\n *\n * @method collapsePanel\n */\n GradingPanel.prototype.collapsePanel = function() {\n this.getPanelElement().addClass('collapsed');\n };\n\n /**\n * Show the grade panel.\n *\n * @method expandPanel\n */\n GradingPanel.prototype.expandPanel = function() {\n this.getPanelElement().removeClass('collapsed');\n };\n\n /**\n * Register event listeners for the grade panel.\n *\n * @method registerEventListeners\n */\n GradingPanel.prototype.registerEventListeners = function() {\n var docElement = $(document);\n var region = $(this._region);\n // Add an event listener to prevent form submission when pressing enter key.\n region.on('submit', 'form', function(e) {\n e.preventDefault();\n });\n\n docElement.on('next-user', this._getNextUser.bind(this));\n docElement.on('user-changed', this._refreshGradingPanel.bind(this));\n docElement.on('save-changes', this._submitForm.bind(this));\n docElement.on('save-and-show-next', this._handleSaveAndShowNext.bind(this));\n docElement.on('reset', this._resetForm.bind(this));\n\n docElement.on('save-form-state', this._saveFormState.bind(this));\n\n docElement.on(GradingEvents.COLLAPSE_GRADE_PANEL, function() {\n this.collapsePanel();\n }.bind(this));\n\n // We should expand if the review panel is collapsed.\n docElement.on(GradingEvents.COLLAPSE_REVIEW_PANEL, function() {\n this.expandPanel();\n }.bind(this));\n\n docElement.on(GradingEvents.EXPAND_GRADE_PANEL, function() {\n this.expandPanel();\n }.bind(this));\n };\n\n return GradingPanel;\n});\n"],"names":["define","$","Y","notification","templates","fragment","ajax","str","checker","GradingEvents","FormEvents","Toast","FormChangeChecker","GradingPanel","selector","_regionSelector","_region","_userCache","registerEventListeners","prototype","_lastUserId","_lastAttemptNumber","nextUserId","nextUser","_niceReplaceNodeContents","node","html","js","promise","Deferred","fadeOut","replaceNodeContents","fadeIn","resolve","_saveFormState","checked","prop","val","_submitForm","event","commentAreaElement","document","querySelector","value","click","form","this","find","show","markFormSubmitted","trigger","notifyFormSubmittedByJavascript","data","serialize","assignmentid","attr","call","methodname","args","userid","jsonformdata","JSON","stringify","done","_handleFormSubmissionResponse","bind","fail","exception","formdata","response","length","get_string","then","add","type","delay","catch","resetFormDirtyState","hide","_resetForm","e","unresolvederror","Event","_refreshGradingPanel","_chooseAttempt","submissionsId","target","formhtml","getElementById","clone","wrap","get_strings","key","component","strs","confirm","attemptnumber","_addPopoutButtons","region","render","closest","addClass","parent","append","on","_togglePopout","preventDefault","container","hasClass","removeClass","submissiondata","contextid","window","M","util","js_pending","params","loadFragment","saveFormState","js_complete","_getNextUser","_handleSaveAndShowNext","getPanelElement","collapsePanel","expandPanel","docElement","COLLAPSE_GRADE_PANEL","COLLAPSE_REVIEW_PANEL","EXPAND_GRADE_PANEL"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"grading_panel.min.js","sources":["../src/grading_panel.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript controller for the \"Grading\" panel at the right of the page.\n *\n * @module mod_assign/grading_panel\n * @copyright 2016 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine([\n 'jquery',\n 'core/yui',\n 'core/notification',\n 'core/templates',\n 'core/fragment',\n 'core/ajax',\n 'core/str',\n 'mod_assign/grading_form_change_checker',\n 'mod_assign/grading_events',\n 'core_form/events',\n 'core/toast',\n 'core_form/changechecker',\n], function(\n $,\n Y,\n notification,\n templates,\n fragment,\n ajax,\n str,\n checker,\n GradingEvents,\n FormEvents,\n Toast,\n FormChangeChecker\n) {\n\n /**\n * GradingPanel class.\n *\n * @class mod_assign/grading_panel\n * @param {String} selector The selector for the page region containing the user navigation.\n */\n var GradingPanel = function(selector) {\n this._regionSelector = selector;\n this._region = $(selector);\n this._userCache = [];\n\n this.registerEventListeners();\n };\n\n /** @property {String} Selector for the page region containing the user navigation. */\n GradingPanel.prototype._regionSelector = null;\n\n /** @property {Integer} Remember the last user id to prevent unnessecary reloads. */\n GradingPanel.prototype._lastUserId = 0;\n\n /** @property {Integer} Remember the last attempt number to prevent unnessecary reloads. */\n GradingPanel.prototype._lastAttemptNumber = -1;\n\n /** @property {JQuery} JQuery node for the page region containing the user navigation. */\n GradingPanel.prototype._region = null;\n\n /** @property {Integer} The id of the next user in the grading list */\n GradingPanel.prototype.nextUserId = null;\n\n /** @property {Boolean} Next user exists in the grading list */\n GradingPanel.prototype.nextUser = false;\n\n /**\n * Fade the dom node out, update it, and fade it back.\n *\n * @private\n * @method _niceReplaceNodeContents\n * @param {JQuery} node\n * @param {String} html\n * @param {String} js\n * @return {Deferred} promise resolved when the animations are complete.\n */\n GradingPanel.prototype._niceReplaceNodeContents = function(node, html, js) {\n var promise = $.Deferred();\n\n node.fadeOut(\"fast\", function() {\n templates.replaceNodeContents(node, html, js);\n node.fadeIn(\"fast\", function() {\n promise.resolve();\n });\n });\n\n return promise.promise();\n };\n\n /**\n * Make sure all form fields have the latest saved state.\n * @private\n * @method _saveFormState\n */\n GradingPanel.prototype._saveFormState = function() {\n // Copy data from notify students checkbox which was moved out of the form.\n var checked = $('[data-region=\"grading-actions-form\"] [name=\"sendstudentnotifications\"]').prop(\"checked\");\n $('.gradeform [name=\"sendstudentnotifications\"]').val(checked);\n };\n\n /**\n * Make form submit via ajax.\n *\n * @private\n * @param {Object} event\n * @param {Integer} nextUserId\n * @param {Boolean} nextUser optional. Load next user in the grading list.\n * @method _submitForm\n * @fires event:formSubmittedByJavascript\n */\n GradingPanel.prototype._submitForm = function(event, nextUserId, nextUser) {\n // If the form has data in comment-area, then we need to save that comment\n var commentAreaElement = document.querySelector('.comment-area');\n if (commentAreaElement) {\n var commentTextAreaElement = commentAreaElement.querySelector('.db > textarea');\n if (commentTextAreaElement.value !== '') {\n var commentActionPostElement = commentAreaElement.querySelector('.fd a[id^=\"comment-action-post-\"]');\n commentActionPostElement.click();\n }\n }\n\n // The form was submitted - send it via ajax instead.\n var form = $(this._region.find('form.gradeform'));\n\n $('[data-region=\"overlay\"]').show();\n\n // Mark the form as submitted in the change checker.\n FormChangeChecker.markFormSubmitted(form[0]);\n\n // We call this, so other modules can update the form with the latest state.\n form.trigger('save-form-state');\n\n // Tell all form fields we are about to submit the form.\n FormEvents.notifyFormSubmittedByJavascript(form[0]);\n\n // Now we get all the current values from the form.\n var data = form.serialize();\n var assignmentid = this._region.attr('data-assignmentid');\n\n var vars = window.location.search.substring(1).split('&');\n var marker = vars.includes('action=marker');\n\n // Now we can continue...\n ajax.call([{\n methodname: 'mod_assign_submit_grading_form',\n args: {assignmentid: assignmentid, userid: this._lastUserId, marker: marker, jsonformdata: JSON.stringify(data)},\n done: this._handleFormSubmissionResponse.bind(this, data, nextUserId, nextUser),\n fail: notification.exception\n }]);\n };\n\n /**\n * Handle form submission response.\n *\n * @private\n * @method _handleFormSubmissionResponse\n * @param {Array} formdata - submitted values\n * @param {Number} [nextUserId] The id of the user to load after the form is saved\n * @param {Boolean} [nextUser] - Whether to switch to next user in the grading list.\n * @param {Array} response List of errors.\n */\n GradingPanel.prototype._handleFormSubmissionResponse = function(formdata, nextUserId, nextUser, response) {\n if (typeof nextUserId === \"undefined\") {\n nextUserId = this._lastUserId;\n }\n if (response.length) {\n str.get_string('errorgradechangessaveddetail', 'mod_assign')\n .then(function(str) {\n Toast.add(str, {type: 'danger', delay: 4000});\n return str;\n })\n .catch(notification.exception);\n\n // There was an error saving the grade. Re-render the form using the submitted data so we can show\n // validation errors.\n $(document).trigger('reset', [this._lastUserId, formdata, true]);\n } else {\n str.get_string('gradechangessaveddetail', 'mod_assign')\n .then(function(str) {\n Toast.add(str);\n return str;\n })\n .catch(notification.exception);\n\n // Reset the form state.\n var form = $(this._region.find('form.gradeform'));\n FormChangeChecker.resetFormDirtyState(form[0]);\n\n if (nextUserId == this._lastUserId) {\n $(document).trigger('reset', nextUserId);\n } else if (nextUser) {\n $(document).trigger('done-saving-show-next', true);\n } else {\n $(document).trigger('user-changed', nextUserId);\n }\n }\n $('[data-region=\"overlay\"]').hide();\n };\n\n /**\n * Refresh form with default values.\n *\n * @private\n * @method _resetForm\n * @param {Event} e\n * @param {Integer} userid\n * @param {Array} formdata\n * @param {Boolean} unresolvederror\n */\n GradingPanel.prototype._resetForm = function(e, userid, formdata, unresolvederror) {\n // The form was cancelled - refresh with default values.\n var event = $.Event(\"custom\");\n if (typeof userid == \"undefined\") {\n userid = this._lastUserId;\n }\n this._lastUserId = 0;\n this._refreshGradingPanel(event, userid, formdata, -1, unresolvederror);\n };\n\n /**\n * Open a picker to choose an older attempt.\n *\n * @private\n * @param {Object} e\n * @method _chooseAttempt\n */\n GradingPanel.prototype._chooseAttempt = function(e) {\n // Show a dialog.\n\n // The form is in the element pointed to by data-submissions.\n var link = $(e.target);\n var submissionsId = link.data('submissions');\n var submissionsform = $(document.getElementById(submissionsId));\n var formcopy = submissionsform.clone();\n var formhtml = formcopy.wrap($('')).html();\n\n str.get_strings([\n {key: 'viewadifferentattempt', component: 'mod_assign'},\n {key: 'view', component: 'core'},\n {key: 'cancel', component: 'core'},\n ]).done(function(strs) {\n notification.confirm(strs[0], formhtml, strs[1], strs[2], function() {\n var attemptnumber = $(\"input:radio[name='select-attemptnumber']:checked\").val();\n\n this._refreshGradingPanel(null, this._lastUserId, '', attemptnumber);\n }.bind(this));\n }.bind(this)).fail(notification.exception);\n };\n\n /**\n * Add popout buttons\n *\n * @private\n * @method _addPopoutButtons\n * @param {JQuery} selector The region selector to add popout buttons to.\n */\n GradingPanel.prototype._addPopoutButtons = function(selector) {\n var region = $(selector);\n\n templates.render('mod_assign/popout_button', {}).done(function(html) {\n var parents = region.find('[data-fieldtype=\"filemanager\"],[data-fieldtype=\"editor\"],[data-fieldtype=\"grading\"]')\n .closest('.fitem');\n parents.addClass('has-popout').find('label:first').parent().append(html);\n\n region.on('click', '[data-region=\"popout-button\"]', this._togglePopout.bind(this));\n }.bind(this)).fail(notification.exception);\n };\n\n /**\n * Make a div \"popout\" or \"popback\".\n *\n * @private\n * @method _togglePopout\n * @param {Event} event\n */\n GradingPanel.prototype._togglePopout = function(event) {\n event.preventDefault();\n var container = $(event.target).closest('.fitem');\n if (container.hasClass('popout')) {\n $('.popout').removeClass('popout');\n } else {\n $('.popout').removeClass('popout');\n container.addClass('popout');\n container.addClass('moodle-has-zindex');\n }\n };\n\n /**\n * Get the user context - re-render the template in the page.\n *\n * @private\n * @method _refreshGradingPanel\n * @param {Event} event\n * @param {Number} userid\n * @param {String} submissiondata serialised submission data.\n * @param {Integer} attemptnumber\n * @param {Boolean} unresolvederror\n */\n GradingPanel.prototype._refreshGradingPanel = function(event, userid, submissiondata, attemptnumber, unresolvederror) {\n var contextid = this._region.attr('data-contextid');\n if (typeof submissiondata === 'undefined') {\n submissiondata = '';\n }\n if (typeof attemptnumber === 'undefined') {\n attemptnumber = -1;\n }\n if (typeof unresolvederror === 'undefined') {\n unresolvederror = false;\n }\n // Skip reloading if it is the same user.\n if (this._lastUserId == userid && this._lastAttemptNumber == attemptnumber && submissiondata === '') {\n return;\n }\n this._lastUserId = userid;\n this._lastAttemptNumber = attemptnumber;\n $(document).trigger('start-loading-user');\n // Tell behat to back off too.\n window.M.util.js_pending('mod-assign-loading-user');\n // First insert the loading template.\n templates.render('mod_assign/loading', {}).done(function(html, js) {\n // Update the page.\n this._niceReplaceNodeContents(this._region, html, js).done(function() {\n if (userid > 0) {\n this._region.show();\n var vars = window.location.search.substring(1).split('&');\n var marker = vars.includes('action=marker');\n // Reload the grading form \"fragment\" for this user.\n var params = {userid: userid, attemptnumber: attemptnumber,\n jsonformdata: JSON.stringify(submissiondata),\n marker: marker};\n fragment.loadFragment('mod_assign', 'gradingpanel', contextid, params).done(function(html, js) {\n\n // Reset whole grading page when there is a failure in retrieving the html\n // i.e. user no longer under \"requires grading\" filter when graded\n if (html === '') {\n $(document).trigger('reset-table', true);\n }\n\n this._niceReplaceNodeContents(this._region, html, js)\n .done(function() {\n checker.saveFormState('[data-region=\"grade-panel\"] .gradeform');\n $(document).on('editor-content-restored', function() {\n // If the editor has some content that has been restored\n // then save the form state again for comparison.\n checker.saveFormState('[data-region=\"grade-panel\"] .gradeform');\n });\n $('[data-region=\"attempt-chooser\"]').on('click', this._chooseAttempt.bind(this));\n this._addPopoutButtons('[data-region=\"grade-panel\"] .gradeform');\n if (unresolvederror) {\n $('[data-region=\"grade-panel\"] .gradeform').data('unresolved-error', true);\n }\n $(document).trigger('finish-loading-user');\n // Tell behat we are friends again.\n window.M.util.js_complete('mod-assign-loading-user');\n }.bind(this))\n .fail(notification.exception);\n }.bind(this)).fail(notification.exception);\n $('[data-region=\"review-panel\"]').show();\n } else {\n this._region.hide();\n $('[data-region=\"review-panel\"]').hide();\n $(document).trigger('finish-loading-user');\n // Tell behat we are friends again.\n window.M.util.js_complete('mod-assign-loading-user');\n }\n }.bind(this));\n }.bind(this)).fail(notification.exception);\n };\n\n /**\n * Get next user data and store it in global variables\n *\n * @private\n * @method _getNextUser\n * @param {Event} event\n * @param {Object} data Next user's data\n */\n GradingPanel.prototype._getNextUser = function(event, data) {\n this.nextUserId = data.nextUserId;\n this.nextUser = data.nextUser;\n };\n\n /**\n * Handle the save-and-show-next event\n *\n * @private\n * @method _handleSaveAndShowNext\n */\n GradingPanel.prototype._handleSaveAndShowNext = function() {\n this._submitForm(null, this.nextUserId, this.nextUser);\n };\n\n /**\n * Get the grade panel element.\n *\n * @method getPanelElement\n * @return {jQuery}\n */\n GradingPanel.prototype.getPanelElement = function() {\n return $('[data-region=\"grade-panel\"]');\n };\n\n /**\n * Hide the grade panel.\n *\n * @method collapsePanel\n */\n GradingPanel.prototype.collapsePanel = function() {\n this.getPanelElement().addClass('collapsed');\n };\n\n /**\n * Show the grade panel.\n *\n * @method expandPanel\n */\n GradingPanel.prototype.expandPanel = function() {\n this.getPanelElement().removeClass('collapsed');\n };\n\n /**\n * Register event listeners for the grade panel.\n *\n * @method registerEventListeners\n */\n GradingPanel.prototype.registerEventListeners = function() {\n var docElement = $(document);\n var region = $(this._region);\n // Add an event listener to prevent form submission when pressing enter key.\n region.on('submit', 'form', function(e) {\n e.preventDefault();\n });\n\n docElement.on('next-user', this._getNextUser.bind(this));\n docElement.on('user-changed', this._refreshGradingPanel.bind(this));\n docElement.on('save-changes', this._submitForm.bind(this));\n docElement.on('save-and-show-next', this._handleSaveAndShowNext.bind(this));\n docElement.on('reset', this._resetForm.bind(this));\n\n docElement.on('save-form-state', this._saveFormState.bind(this));\n\n docElement.on(GradingEvents.COLLAPSE_GRADE_PANEL, function() {\n this.collapsePanel();\n }.bind(this));\n\n // We should expand if the review panel is collapsed.\n docElement.on(GradingEvents.COLLAPSE_REVIEW_PANEL, function() {\n this.expandPanel();\n }.bind(this));\n\n docElement.on(GradingEvents.EXPAND_GRADE_PANEL, function() {\n this.expandPanel();\n }.bind(this));\n };\n\n return GradingPanel;\n});\n"],"names":["define","$","Y","notification","templates","fragment","ajax","str","checker","GradingEvents","FormEvents","Toast","FormChangeChecker","GradingPanel","selector","_regionSelector","_region","_userCache","registerEventListeners","prototype","_lastUserId","_lastAttemptNumber","nextUserId","nextUser","_niceReplaceNodeContents","node","html","js","promise","Deferred","fadeOut","replaceNodeContents","fadeIn","resolve","_saveFormState","checked","prop","val","_submitForm","event","commentAreaElement","document","querySelector","value","click","form","this","find","show","markFormSubmitted","trigger","notifyFormSubmittedByJavascript","data","serialize","assignmentid","attr","marker","window","location","search","substring","split","includes","call","methodname","args","userid","jsonformdata","JSON","stringify","done","_handleFormSubmissionResponse","bind","fail","exception","formdata","response","length","get_string","then","add","type","delay","catch","resetFormDirtyState","hide","_resetForm","e","unresolvederror","Event","_refreshGradingPanel","_chooseAttempt","submissionsId","target","formhtml","getElementById","clone","wrap","get_strings","key","component","strs","confirm","attemptnumber","_addPopoutButtons","region","render","closest","addClass","parent","append","on","_togglePopout","preventDefault","container","hasClass","removeClass","submissiondata","contextid","M","util","js_pending","params","loadFragment","saveFormState","js_complete","_getNextUser","_handleSaveAndShowNext","getPanelElement","collapsePanel","expandPanel","docElement","COLLAPSE_GRADE_PANEL","COLLAPSE_REVIEW_PANEL","EXPAND_GRADE_PANEL"],"mappings":"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"} \ No newline at end of file diff --git a/public/mod/assign/amd/build/participant_selector.min.js b/public/mod/assign/amd/build/participant_selector.min.js index 5d5530765d1..f22cd594fe7 100644 --- a/public/mod/assign/amd/build/participant_selector.min.js +++ b/public/mod/assign/amd/build/participant_selector.min.js @@ -5,6 +5,6 @@ * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("mod_assign/participant_selector",["core/ajax","jquery","core/templates"],(function(ajax,$,templates){return{processResults:function(selector,data){return data},transport:function(selector,query,success,failure){var assignmentid=$(selector).attr("data-assignmentid"),groupid=$(selector).attr("data-groupid"),filters=$('[data-region="configure-filters"] input[type="checkbox"]'),filterstrings=[];filters.each((function(index,element){filterstrings[$(element).attr("name")]=$(element).prop("checked")})),ajax.call([{methodname:"mod_assign_list_participants",args:{assignid:assignmentid,groupid:groupid,filter:query,limit:30,includeenrolments:!1,tablesort:!0}}])[0].then((function(results){var promises=[],identityfields=$("[data-showuseridentity]").data("showuseridentity").split(",");return $.each(results,(function(index,user){var ctx=user,identity=[],show=!0;filterstrings.filter_submitted&&!user.submitted&&(show=!1),filterstrings.filter_notsubmitted&&user.submitted&&(show=!1),filterstrings.filter_requiregrading&&!user.requiregrading&&(show=!1),filterstrings.filter_grantedextension&&!user.grantedextension&&(show=!1),show&&($.each(identityfields,(function(i,k){void 0!==user[k]&&""!==user[k]&&(ctx.hasidentity=!0,identity.push(user[k]))})),ctx.identity=identity.join(", "),promises.push(templates.render("mod_assign/list_participant_user_summary",ctx).then((function(html){return{value:user.id,label:html}}))))})),$.when.apply($,promises)})).then((function(){var users=[];arguments[0]&&(users=Array.prototype.slice.call(arguments)),success(users)})).catch(failure)}}})); +define("mod_assign/participant_selector",["core/ajax","core/templates"],(function(ajax,templates){return{processResults:function(selector,data){return data},transport:function(selector,query,success,failure){const element=document.querySelector(selector);var assignmentid=element.getAttribute("data-assignmentid"),groupid=element.getAttribute("data-groupid"),filters=document.querySelectorAll('[data-region="configure-filters"] input[type="checkbox"]'),filterstrings=[];filters.forEach((e=>{let filterelement=document.querySelector(e);filterstrings[filterelement.getAttribute("name")]=filterelement.checked}));var marking=element.getAttribute("data-ismarking");ajax.call([{methodname:"mod_assign_list_participants",args:{assignid:assignmentid,groupid:groupid,filter:query,limit:30,includeenrolments:!1,tablesort:!0,marking:marking}}])[0].then((function(results){var promises=[],identityfields=document.querySelector("[data-showuseridentity]").dataset.showuseridentity.split(",");return results.forEach((user=>{var ctx=user,identity=[],show=!0;filterstrings.filter_submitted&&!user.submitted&&(show=!1),filterstrings.filter_notsubmitted&&user.submitted&&(show=!1),filterstrings.filter_requiregrading&&!user.requiregrading&&(show=!1),filterstrings.filter_grantedextension&&!user.grantedextension&&(show=!1),show&&(identityfields.forEach((k=>{void 0!==user[k]&&""!==user[k]&&(ctx.hasidentity=!0,identity.push(user[k]))})),ctx.identity=identity.join(", "),promises.push(templates.render("mod_assign/list_participant_user_summary",ctx).then((function(html){return{value:user.id,label:html}}))))})),Promise.all(promises)})).then((function(users){success(users)})).catch(failure)}}})); //# sourceMappingURL=participant_selector.min.js.map \ No newline at end of file diff --git a/public/mod/assign/amd/build/participant_selector.min.js.map b/public/mod/assign/amd/build/participant_selector.min.js.map index aa62f33934f..55d2a427116 100644 --- a/public/mod/assign/amd/build/participant_selector.min.js.map +++ b/public/mod/assign/amd/build/participant_selector.min.js.map @@ -1 +1 @@ -{"version":3,"file":"participant_selector.min.js","sources":["../src/participant_selector.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Custom auto-complete adapter to load users from the assignment list_participants webservice.\n *\n * @module mod_assign/participant_selector\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) {\n\n\n return /** @alias module:mod_assign/participants_selector */ {\n\n // Public variables and functions.\n /**\n * Process the results returned from transport (convert to value + label)\n *\n * @method processResults\n * @param {String} selector\n * @param {Array} data\n * @return {Array}\n */\n processResults: function(selector, data) {\n return data;\n },\n\n /**\n * Fetch results based on the current query. This also renders each result from a template before returning them.\n *\n * @method transport\n * @param {String} selector Selector for the original select element\n * @param {String} query Current search string\n * @param {Function} success Success handler\n * @param {Function} failure Failure handler\n */\n transport: function(selector, query, success, failure) {\n var assignmentid = $(selector).attr('data-assignmentid');\n var groupid = $(selector).attr('data-groupid');\n var filters = $('[data-region=\"configure-filters\"] input[type=\"checkbox\"]');\n var filterstrings = [];\n\n filters.each(function(index, element) {\n filterstrings[$(element).attr('name')] = $(element).prop('checked');\n });\n\n ajax.call([{\n methodname: 'mod_assign_list_participants',\n args: {\n assignid: assignmentid,\n groupid: groupid,\n filter: query,\n limit: 30,\n includeenrolments: false,\n tablesort: true\n }\n }])[0].then(function(results) {\n var promises = [];\n var identityfields = $('[data-showuseridentity]').data('showuseridentity').split(',');\n\n // We got the results, now we loop over them and render each one from a template.\n $.each(results, function(index, user) {\n var ctx = user,\n identity = [],\n show = true;\n\n if (filterstrings.filter_submitted && !user.submitted) {\n show = false;\n }\n if (filterstrings.filter_notsubmitted && user.submitted) {\n show = false;\n }\n if (filterstrings.filter_requiregrading && !user.requiregrading) {\n show = false;\n }\n if (filterstrings.filter_grantedextension && !user.grantedextension) {\n show = false;\n }\n if (show) {\n $.each(identityfields, function(i, k) {\n if (typeof user[k] !== 'undefined' && user[k] !== '') {\n ctx.hasidentity = true;\n identity.push(user[k]);\n }\n });\n ctx.identity = identity.join(', ');\n promises.push(templates.render('mod_assign/list_participant_user_summary', ctx).then(function(html) {\n return {value: user.id, label: html};\n }));\n }\n });\n // Do the dance for $.when()\n return $.when.apply($, promises);\n }).then(function() {\n var users = [];\n\n // Determine if we've been passed any arguments..\n if (arguments[0]) {\n // Undo the $.when() dance from arguments object into an array..\n users = Array.prototype.slice.call(arguments);\n }\n\n success(users);\n return;\n }).catch(failure);\n }\n };\n});\n"],"names":["define","ajax","$","templates","processResults","selector","data","transport","query","success","failure","assignmentid","attr","groupid","filters","filterstrings","each","index","element","prop","call","methodname","args","assignid","filter","limit","includeenrolments","tablesort","then","results","promises","identityfields","split","user","ctx","identity","show","filter_submitted","submitted","filter_notsubmitted","filter_requiregrading","requiregrading","filter_grantedextension","grantedextension","i","k","hasidentity","push","join","render","html","value","id","label","when","apply","users","arguments","Array","prototype","slice","catch"],"mappings":";;;;;;;AAsBAA,yCAAO,CAAC,YAAa,SAAU,mBAAmB,SAASC,KAAMC,EAAGC,iBAGH,CAWzDC,eAAgB,SAASC,SAAUC,aACxBA,MAYXC,UAAW,SAASF,SAAUG,MAAOC,QAASC,aACtCC,aAAeT,EAAEG,UAAUO,KAAK,qBAChCC,QAAUX,EAAEG,UAAUO,KAAK,gBAC3BE,QAAUZ,EAAE,4DACZa,cAAgB,GAEpBD,QAAQE,MAAK,SAASC,MAAOC,SACzBH,cAAcb,EAAEgB,SAASN,KAAK,SAAWV,EAAEgB,SAASC,KAAK,cAG7DlB,KAAKmB,KAAK,CAAC,CACPC,WAAY,+BACZC,KAAM,CACFC,SAAUZ,aACVE,QAASA,QACTW,OAAQhB,MACRiB,MAAO,GACPC,mBAAmB,EACnBC,WAAW,MAEf,GAAGC,MAAK,SAASC,aACbC,SAAW,GACXC,eAAiB7B,EAAE,2BAA2BI,KAAK,oBAAoB0B,MAAM,YAGjF9B,EAAEc,KAAKa,SAAS,SAASZ,MAAOgB,UACxBC,IAAMD,KACNE,SAAW,GACXC,MAAO,EAEPrB,cAAcsB,mBAAqBJ,KAAKK,YACxCF,MAAO,GAEPrB,cAAcwB,qBAAuBN,KAAKK,YAC1CF,MAAO,GAEPrB,cAAcyB,wBAA0BP,KAAKQ,iBAC7CL,MAAO,GAEPrB,cAAc2B,0BAA4BT,KAAKU,mBAC/CP,MAAO,GAEPA,OACAlC,EAAEc,KAAKe,gBAAgB,SAASa,EAAGC,QACR,IAAZZ,KAAKY,IAAkC,KAAZZ,KAAKY,KACvCX,IAAIY,aAAc,EAClBX,SAASY,KAAKd,KAAKY,QAG3BX,IAAIC,SAAWA,SAASa,KAAK,MAC7BlB,SAASiB,KAAK5C,UAAU8C,OAAO,2CAA4Cf,KAAKN,MAAK,SAASsB,YACnF,CAACC,MAAOlB,KAAKmB,GAAIC,MAAOH,cAKpChD,EAAEoD,KAAKC,MAAMrD,EAAG4B,aACxBF,MAAK,eACA4B,MAAQ,GAGRC,UAAU,KAEVD,MAAQE,MAAMC,UAAUC,MAAMxC,KAAKqC,YAGvChD,QAAQ+C,UAETK,MAAMnD"} \ No newline at end of file +{"version":3,"file":"participant_selector.min.js","sources":["../src/participant_selector.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Custom auto-complete adapter to load users from the assignment list_participants webservice.\n *\n * @module mod_assign/participant_selector\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['core/ajax', 'core/templates'], function(ajax, templates) {\n\n\n return /** @alias module:mod_assign/participants_selector */ {\n\n // Public variables and functions.\n /**\n * Process the results returned from transport (convert to value + label)\n *\n * @method processResults\n * @param {String} selector\n * @param {Array} data\n * @return {Array}\n */\n processResults: function(selector, data) {\n return data;\n },\n\n /**\n * Fetch results based on the current query. This also renders each result from a template before returning them.\n *\n * @method transport\n * @param {String} selector Selector for the original select element\n * @param {String} query Current search string\n * @param {Function} success Success handler\n * @param {Function} failure Failure handler\n */\n transport: function(selector, query, success, failure) {\n const element = document.querySelector(selector);\n var assignmentid = element.getAttribute('data-assignmentid');\n var groupid = element.getAttribute('data-groupid');\n var filters = document.querySelectorAll('[data-region=\"configure-filters\"] input[type=\"checkbox\"]');\n var filterstrings = [];\n\n filters.forEach((e) => {\n let filterelement = document.querySelector(e);\n filterstrings[filterelement.getAttribute('name')] = filterelement.checked;\n });\n\n var marking = element.getAttribute('data-ismarking');\n\n ajax.call([{\n methodname: 'mod_assign_list_participants',\n args: {\n assignid: assignmentid,\n groupid: groupid,\n filter: query,\n limit: 30,\n includeenrolments: false,\n tablesort: true,\n marking: marking,\n }\n }])[0].then(function(results) {\n var promises = [];\n var identityfields = document.querySelector('[data-showuseridentity]').dataset.showuseridentity.split(',');\n\n // We got the results, now we loop over them and render each one from a template.\n results.forEach((user) => {\n var ctx = user,\n identity = [],\n show = true;\n\n if (filterstrings.filter_submitted && !user.submitted) {\n show = false;\n }\n if (filterstrings.filter_notsubmitted && user.submitted) {\n show = false;\n }\n if (filterstrings.filter_requiregrading && !user.requiregrading) {\n show = false;\n }\n if (filterstrings.filter_grantedextension && !user.grantedextension) {\n show = false;\n }\n if (show) {\n identityfields.forEach((k) => {\n if (typeof user[k] !== 'undefined' && user[k] !== '') {\n ctx.hasidentity = true;\n identity.push(user[k]);\n }\n });\n ctx.identity = identity.join(', ');\n promises.push(templates.render('mod_assign/list_participant_user_summary', ctx).then(function(html) {\n return {value: user.id, label: html};\n }));\n }\n });\n return Promise.all(promises);\n }).then(function(users) {\n success(users);\n return;\n }).catch(failure);\n }\n };\n});\n"],"names":["define","ajax","templates","processResults","selector","data","transport","query","success","failure","element","document","querySelector","assignmentid","getAttribute","groupid","filters","querySelectorAll","filterstrings","forEach","e","filterelement","checked","marking","call","methodname","args","assignid","filter","limit","includeenrolments","tablesort","then","results","promises","identityfields","dataset","showuseridentity","split","user","ctx","identity","show","filter_submitted","submitted","filter_notsubmitted","filter_requiregrading","requiregrading","filter_grantedextension","grantedextension","k","hasidentity","push","join","render","html","value","id","label","Promise","all","users","catch"],"mappings":";;;;;;;AAsBAA,yCAAO,CAAC,YAAa,mBAAmB,SAASC,KAAMC,iBAGU,CAWzDC,eAAgB,SAASC,SAAUC,aACxBA,MAYXC,UAAW,SAASF,SAAUG,MAAOC,QAASC,eACpCC,QAAUC,SAASC,cAAcR,cACnCS,aAAeH,QAAQI,aAAa,qBACpCC,QAAUL,QAAQI,aAAa,gBAC/BE,QAAUL,SAASM,iBAAiB,4DACpCC,cAAgB,GAEpBF,QAAQG,SAASC,QACTC,cAAgBV,SAASC,cAAcQ,GAC3CF,cAAcG,cAAcP,aAAa,SAAWO,cAAcC,eAGlEC,QAAUb,QAAQI,aAAa,kBAEnCb,KAAKuB,KAAK,CAAC,CACPC,WAAY,+BACZC,KAAM,CACFC,SAAUd,aACVE,QAASA,QACTa,OAAQrB,MACRsB,MAAO,GACPC,mBAAmB,EACnBC,WAAW,EACXR,QAASA,YAEb,GAAGS,MAAK,SAASC,aACbC,SAAW,GACXC,eAAiBxB,SAASC,cAAc,2BAA2BwB,QAAQC,iBAAiBC,MAAM,YAGtGL,QAAQd,SAASoB,WACTC,IAAMD,KACNE,SAAW,GACXC,MAAO,EAEPxB,cAAcyB,mBAAqBJ,KAAKK,YACxCF,MAAO,GAEPxB,cAAc2B,qBAAuBN,KAAKK,YAC1CF,MAAO,GAEPxB,cAAc4B,wBAA0BP,KAAKQ,iBAC7CL,MAAO,GAEPxB,cAAc8B,0BAA4BT,KAAKU,mBAC/CP,MAAO,GAEPA,OACAP,eAAehB,SAAS+B,SACG,IAAZX,KAAKW,IAAkC,KAAZX,KAAKW,KACvCV,IAAIW,aAAc,EAClBV,SAASW,KAAKb,KAAKW,QAG3BV,IAAIC,SAAWA,SAASY,KAAK,MAC7BnB,SAASkB,KAAKlD,UAAUoD,OAAO,2CAA4Cd,KAAKR,MAAK,SAASuB,YACnF,CAACC,MAAOjB,KAAKkB,GAAIC,MAAOH,cAIpCI,QAAQC,IAAI1B,aACpBF,MAAK,SAAS6B,OACbrD,QAAQqD,UAETC,MAAMrD"} \ No newline at end of file diff --git a/public/mod/assign/amd/src/batch_set_marking_workflow_state.js b/public/mod/assign/amd/src/batch_set_marking_workflow_state.js new file mode 100644 index 00000000000..959ebe74b57 --- /dev/null +++ b/public/mod/assign/amd/src/batch_set_marking_workflow_state.js @@ -0,0 +1,54 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Javascript controller for the "Actions" panel at the bottom of the page. + * + * @module mod_assign/batch_set_marking_workflow_state + * @copyright 2025 onwards Catalyst IT {@link http://www.catalyst-eu.net/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Conn Warwicker + */ + +/** + * Filter which options are enabled in the select menu. + * + * @param {object} context + * @param {array} args + */ +const filterOptions = (context, args) => { + const options = args[context]; + const workflowstate = document.getElementById('id_markingworkflowstate'); + workflowstate.children.forEach((item) => { + if (options.includes(item.value)) { + item.removeAttribute('disabled'); + } else { + item.setAttribute('disabled', ''); + } + }); +}; + +/** + * Initialise scripts. + * + * @param {array} args + */ +export const init = (args) => { + const workflowcontext = document.getElementById('id_workflowcontext'); + filterOptions(workflowcontext.value, args); + workflowcontext.addEventListener('change', (e) => { + filterOptions(e.target.value, args); + }); +}; \ No newline at end of file diff --git a/public/mod/assign/amd/src/grading_navigation.js b/public/mod/assign/amd/src/grading_navigation.js index ecab6584594..3b82db7dc3c 100644 --- a/public/mod/assign/amd/src/grading_navigation.js +++ b/public/mod/assign/amd/src/grading_navigation.js @@ -39,6 +39,7 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', this._filteredUsers = []; this._lastXofYUpdate = 0; this._firstLoadUsers = true; + this._isMarking = false; let url = new URL(window.location); if (parseInt(url.searchParams.get('treset')) > 0) { @@ -133,10 +134,11 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete', return false; } this._lastFilters = filter; + this._isMarking = select.attr('data-ismarking') ?? false; ajax.call([{ methodname: 'mod_assign_list_participants', - args: {assignid: assignmentid, groupid: groupid, filter: '', onlyids: true, tablesort: true}, + args: {assignid: assignmentid, groupid: groupid, filter: '', onlyids: true, tablesort: true, marking: this._isMarking}, done: this._usersLoaded.bind(this), fail: notification.exception }]); diff --git a/public/mod/assign/amd/src/grading_panel.js b/public/mod/assign/amd/src/grading_panel.js index e9ce0618fe4..441476cb1da 100644 --- a/public/mod/assign/amd/src/grading_panel.js +++ b/public/mod/assign/amd/src/grading_panel.js @@ -154,10 +154,13 @@ define([ var data = form.serialize(); var assignmentid = this._region.attr('data-assignmentid'); + var vars = window.location.search.substring(1).split('&'); + var marker = vars.includes('action=marker'); + // Now we can continue... ajax.call([{ methodname: 'mod_assign_submit_grading_form', - args: {assignmentid: assignmentid, userid: this._lastUserId, jsonformdata: JSON.stringify(data)}, + args: {assignmentid: assignmentid, userid: this._lastUserId, marker: marker, jsonformdata: JSON.stringify(data)}, done: this._handleFormSubmissionResponse.bind(this, data, nextUserId, nextUser), fail: notification.exception }]); @@ -336,8 +339,12 @@ define([ this._niceReplaceNodeContents(this._region, html, js).done(function() { if (userid > 0) { this._region.show(); + var vars = window.location.search.substring(1).split('&'); + var marker = vars.includes('action=marker'); // Reload the grading form "fragment" for this user. - var params = {userid: userid, attemptnumber: attemptnumber, jsonformdata: JSON.stringify(submissiondata)}; + var params = {userid: userid, attemptnumber: attemptnumber, + jsonformdata: JSON.stringify(submissiondata), + marker: marker}; fragment.loadFragment('mod_assign', 'gradingpanel', contextid, params).done(function(html, js) { // Reset whole grading page when there is a failure in retrieving the html diff --git a/public/mod/assign/amd/src/participant_selector.js b/public/mod/assign/amd/src/participant_selector.js index 00026d2e8d7..8db039e5970 100644 --- a/public/mod/assign/amd/src/participant_selector.js +++ b/public/mod/assign/amd/src/participant_selector.js @@ -20,7 +20,7 @@ * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) { +define(['core/ajax', 'core/templates'], function(ajax, templates) { return /** @alias module:mod_assign/participants_selector */ { @@ -48,15 +48,19 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) { * @param {Function} failure Failure handler */ transport: function(selector, query, success, failure) { - var assignmentid = $(selector).attr('data-assignmentid'); - var groupid = $(selector).attr('data-groupid'); - var filters = $('[data-region="configure-filters"] input[type="checkbox"]'); + const element = document.querySelector(selector); + var assignmentid = element.getAttribute('data-assignmentid'); + var groupid = element.getAttribute('data-groupid'); + var filters = document.querySelectorAll('[data-region="configure-filters"] input[type="checkbox"]'); var filterstrings = []; - filters.each(function(index, element) { - filterstrings[$(element).attr('name')] = $(element).prop('checked'); + filters.forEach((e) => { + let filterelement = document.querySelector(e); + filterstrings[filterelement.getAttribute('name')] = filterelement.checked; }); + var marking = element.getAttribute('data-ismarking'); + ajax.call([{ methodname: 'mod_assign_list_participants', args: { @@ -65,14 +69,15 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) { filter: query, limit: 30, includeenrolments: false, - tablesort: true + tablesort: true, + marking: marking, } }])[0].then(function(results) { var promises = []; - var identityfields = $('[data-showuseridentity]').data('showuseridentity').split(','); + var identityfields = document.querySelector('[data-showuseridentity]').dataset.showuseridentity.split(','); // We got the results, now we loop over them and render each one from a template. - $.each(results, function(index, user) { + results.forEach((user) => { var ctx = user, identity = [], show = true; @@ -90,7 +95,7 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) { show = false; } if (show) { - $.each(identityfields, function(i, k) { + identityfields.forEach((k) => { if (typeof user[k] !== 'undefined' && user[k] !== '') { ctx.hasidentity = true; identity.push(user[k]); @@ -102,17 +107,8 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) { })); } }); - // Do the dance for $.when() - return $.when.apply($, promises); - }).then(function() { - var users = []; - - // Determine if we've been passed any arguments.. - if (arguments[0]) { - // Undo the $.when() dance from arguments object into an array.. - users = Array.prototype.slice.call(arguments); - } - + return Promise.all(promises); + }).then(function(users) { success(users); return; }).catch(failure); diff --git a/public/mod/assign/backup/moodle2/backup_assign_stepslib.php b/public/mod/assign/backup/moodle2/backup_assign_stepslib.php index 62a53c8957f..68cacc9562a 100644 --- a/public/mod/assign/backup/moodle2/backup_assign_stepslib.php +++ b/public/mod/assign/backup/moodle2/backup_assign_stepslib.php @@ -91,6 +91,8 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st 'maxattempts', 'markingworkflow', 'markingallocation', + 'markercount', + 'multimarkmethod', 'markinganonymous', 'preventsubmissionnotingroup', 'activity', @@ -101,14 +103,30 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st $userflags = new backup_nested_element('userflags'); - $userflag = new backup_nested_element('userflag', array('id'), - array('userid', - 'assignment', - 'mailed', - 'locked', - 'extensionduedate', - 'workflowstate', - 'allocatedmarker')); + $userflag = new backup_nested_element( + 'userflag', + ['id'], + [ + 'userid', + 'assignment', + 'mailed', + 'locked', + 'extensionduedate', + 'workflowstate', + ] + ); + + $allocatedmarkers = new backup_nested_element('allocatedmarkers'); + + $allocatedmarker = new backup_nested_element( + 'allocatedmarker', + ['id'], + [ + 'student', + 'assignment', + 'marker', + ] + ); $submissions = new backup_nested_element('submissions'); @@ -133,6 +151,22 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st 'penalty', 'attemptnumber')); + $marks = new backup_nested_element('marks'); + + $mark = new backup_nested_element( + 'mark', + ['id'], + [ + 'assignment', + 'gradeid', + 'timecreated', + 'timemodified', + 'marker', + 'mark', + 'workflowstate', + ] + ); + $pluginconfigs = new backup_nested_element('plugin_configs'); $pluginconfig = new backup_nested_element('plugin_config', array('id'), @@ -148,10 +182,14 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st // Build the tree. $assign->add_child($userflags); $userflags->add_child($userflag); + $assign->add_child($allocatedmarkers); + $allocatedmarkers->add_child($allocatedmarker); $assign->add_child($submissions); $submissions->add_child($submission); $assign->add_child($grades); $grades->add_child($grade); + $assign->add_child($marks); + $marks->add_child($mark); $assign->add_child($pluginconfigs); $pluginconfigs->add_child($pluginconfig); $assign->add_child($overrides); @@ -172,6 +210,15 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st if ($userinfo) { $userflag->set_source_table('assign_user_flags', array('assignment' => backup::VAR_PARENTID)); + $allocatedmarker->set_source_table( + 'assign_allocated_marker', + ['assignment' => backup::VAR_PARENTID] + ); + + $mark->set_source_table( + 'assign_mark', + ['assignment' => backup::VAR_PARENTID] + ); $submissionparams = array('assignment' => backup::VAR_PARENTID); if (!$groupinfo) { @@ -198,7 +245,9 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st // Define id annotations. $userflag->annotate_ids('user', 'userid'); - $userflag->annotate_ids('user', 'allocatedmarker'); + $allocatedmarker->annotate_ids('user', 'student'); + $allocatedmarker->annotate_ids('user', 'marker'); + $mark->annotate_ids('user', 'marker'); $submission->annotate_ids('user', 'userid'); $submission->annotate_ids('group', 'groupid'); $grade->annotate_ids('user', 'userid'); diff --git a/public/mod/assign/backup/moodle2/restore_assign_stepslib.php b/public/mod/assign/backup/moodle2/restore_assign_stepslib.php index f33aed15256..71083eefe6e 100644 --- a/public/mod/assign/backup/moodle2/restore_assign_stepslib.php +++ b/public/mod/assign/backup/moodle2/restore_assign_stepslib.php @@ -71,6 +71,16 @@ class restore_assign_activity_structure_step extends restore_activity_structure_ $userflag = new restore_path_element('assign_userflag', '/activity/assign/userflags/userflag'); $paths[] = $userflag; + $allocatedmarker = new restore_path_element( + 'assign_allocatedmarker', + '/activity/assign/allocatedmarkers/allocatedmarker' + ); + $paths[] = $allocatedmarker; + $mark = new restore_path_element( + 'assign_mark', + '/activity/assign/marks/mark' + ); + $paths[] = $mark; } $paths[] = new restore_path_element('assign_override', '/activity/assign/overrides/override'); @@ -131,6 +141,9 @@ class restore_assign_activity_structure_step extends restore_activity_structure_ if (!isset($data->markingallocation)) { $data->markingallocation = 0; } + if (!isset($data->markercount)) { + $data->markercount = 1; + } if (!isset($data->markinganonymous)) { $data->markinganonymous = 0; } @@ -213,9 +226,6 @@ class restore_assign_activity_structure_step extends restore_activity_structure_ $data->assignment = $this->get_new_parentid('assign'); $data->userid = $this->get_mappingid('user', $data->userid); - if (!empty($data->allocatedmarker)) { - $data->allocatedmarker = $this->get_mappingid('user', $data->allocatedmarker); - } if (!empty($data->extensionduedate)) { $data->extensionduedate = $this->apply_date_offset($data->extensionduedate); } else { @@ -226,6 +236,40 @@ class restore_assign_activity_structure_step extends restore_activity_structure_ $newitemid = $DB->insert_record('assign_user_flags', $data); } + /** + * Process an allocated_marker restore. + * + * @param object $data The data in object form. + */ + protected function process_assign_allocatedmarker($data): void { + global $DB; + + $data = (object)$data; + + $data->assignment = $this->get_new_parentid('assign'); + $data->student = $this->get_mappingid('user', $data->student); + $data->marker = $this->get_mappingid('user', $data->marker); + + $DB->insert_record('assign_allocated_marker', $data); + } + + /** + * Process mark restore. + * + * @param object $data The data in object form. + */ + protected function process_assign_mark($data): void { + global $DB; + + $data = (object)$data; + + $data->assignment = $this->get_new_parentid('assign'); + $data->marker = $this->get_mappingid('user', $data->marker); + $data->gradeid = $this->get_mappingid('grade', $data->gradeid); + + $DB->insert_record('assign_mark', $data); + } + /** * Process a grade restore * @param object $data The data in object form diff --git a/public/mod/assign/batchsetallocatedmarkerform.php b/public/mod/assign/batchsetallocatedmarkerform.php index a0bcac44e75..6385dcdbcb9 100644 --- a/public/mod/assign/batchsetallocatedmarkerform.php +++ b/public/mod/assign/batchsetallocatedmarkerform.php @@ -22,6 +22,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->dirroot . '/mod/assign/feedback/file/locallib.php'); /** * Set allocated marker form. * @@ -32,6 +36,7 @@ class mod_assign_batch_set_allocatedmarker_form extends moodleform { /** * Define this form - called by the parent constructor + * @throws moodle_exception */ public function definition() { $mform = $this->_form; @@ -41,7 +46,23 @@ class mod_assign_batch_set_allocatedmarker_form extends moodleform { $mform->addElement('static', 'userslist', get_string('selectedusers', 'assign'), $params['usershtml']); $options = $params['markers']; - $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $options); + + $markercount = (!empty($params['markercount'])) ? $params['markercount'] : 1; + $markerids = array_keys($options); + + // If we do not have enough markers to meet the requested number, throw an exception with a meaningful message. + if (count($markerids) < $markercount) { + throw new \core\exception\moodle_exception('invalidmarkerallocation:notenoughmarkers', 'assign', '', [ + 'markers' => count($markerids), + 'requested' => $markercount, + ]); + } + + $options = ['' => get_string('choosemarker', 'assign')] + $options; + + for ($i = 1; $i <= $markercount; $i++) { + $mform->addElement('select', "allocatedmarker{$i}", get_string('allocatedmarker', 'assign') . ' ' . $i, $options); + } $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); diff --git a/public/mod/assign/batchsetmarkingworkflowstateform.php b/public/mod/assign/batchsetmarkingworkflowstateform.php index b5c2c6c2eee..7eec075f65b 100644 --- a/public/mod/assign/batchsetmarkingworkflowstateform.php +++ b/public/mod/assign/batchsetmarkingworkflowstateform.php @@ -34,6 +34,7 @@ class mod_assign_batch_set_marking_workflow_state_form extends moodleform { * Define this form - called by the parent constructor */ public function definition() { + global $PAGE; $mform = $this->_form; $params = $this->_customdata; $formheader = get_string('batchsetmarkingworkflowstateforusers', 'assign', $params['userscount']); @@ -41,8 +42,38 @@ class mod_assign_batch_set_marking_workflow_state_form extends moodleform { $mform->addElement('header', 'general', $formheader); $mform->addElement('static', 'userslist', get_string('selectedusers', 'assign'), $params['usershtml']); - $options = $params['markingworkflowstates']; - $mform->addElement('select', 'markingworkflowstate', get_string('markingworkflowstate', 'assign'), $options); + $states = $params['markingworkflowstates']; + + // If the assignment is using multi marking, do we want to set this workflow as the overall workflow for the submissions? + // Or for our allocated mark on them? + if ($params['assignment'] && $params['assignment']->is_using_multiple_marking()) { + $options = new core\output\choicelist(); + $options->add_option( + 'mark', + get_string('markverb', 'assign'), + [ + 'description' => get_string('workflowcontext_help_mark', 'assign'), + ], + ); + $options->add_option( + 'grade', + get_string('gradenoun'), + [ + 'description' => get_string('workflowcontext_help_grade', 'assign'), + ], + ); + $mform->addElement('choicedropdown', 'workflowcontext', get_string('workflowcontext', 'assign'), $options); + $PAGE->requires->js_call_amd('mod_assign/batch_set_marking_workflow_state', 'init', [[ + 'mark' => [ + ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED, + ASSIGN_MARKING_WORKFLOW_STATE_INMARKING, + ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW, + ], + 'grade' => array_keys($states), + ]]); + } + + $mform->addElement('select', 'markingworkflowstate', get_string('markingworkflowstate', 'assign'), $states); // Don't allow notification to be sent until in "Released" state. $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign')); diff --git a/public/mod/assign/classes/output/actionmenu.php b/public/mod/assign/classes/output/actionmenu.php index d93420bf176..967ff651c06 100644 --- a/public/mod/assign/classes/output/actionmenu.php +++ b/public/mod/assign/classes/output/actionmenu.php @@ -30,15 +30,20 @@ use moodle_url; class actionmenu implements templatable, renderable { /** @var int The course module ID. */ - private $cmid; + private int $cmid; + + /** @var bool Are we using marking workflow? */ + private bool $markingworkflow; /** * Constructor for this object. * * @param int $cmid The course module ID. + * @param bool $markingworkflow Are we using marking workflow? */ - public function __construct(int $cmid) { + public function __construct(int $cmid, bool $markingworkflow) { $this->cmid = $cmid; + $this->markingworkflow = $markingworkflow; } /** @@ -51,8 +56,14 @@ class actionmenu implements templatable, renderable { $return = []; if (has_capability('mod/assign:grade', \context_module::instance($this->cmid))) { - $gradelink = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'grader']); - $return['gradelink'] = $gradelink->out(false); + // If we are using multiple markers, then we'll have marker columns which need links to the marker page. + if ($this->markingworkflow) { + $gradelink = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'marker']); + $return['marklink'] = $gradelink->out(false); + } else { + $gradelink = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'grader']); + $return['gradelink'] = $gradelink->out(false); + } } return $return; diff --git a/public/mod/assign/classes/output/grading_app.php b/public/mod/assign/classes/output/grading_app.php index 23978bbba88..2bd2b8fc75f 100644 --- a/public/mod/assign/classes/output/grading_app.php +++ b/public/mod/assign/classes/output/grading_app.php @@ -61,18 +61,27 @@ class grading_app implements templatable, renderable { */ public $participants = []; + /** + * @var bool - True to show fields for marking, false to show fields for + * grading (used when there are multiple markers). + */ + public $marker = false; + /** * Constructor for this renderable. * * @param int $userid The user we will open the grading app too. * @param int $groupid If groups are enabled this is the current course group. * @param \assign $assignment The assignment class + * @param bool $marker If assignment instance uses multiple markers $marker + * is true to show fields for marking (as opposed to grading). */ - public function __construct($userid, $groupid, $assignment) { + public function __construct($userid, $groupid, $assignment, $marker = false) { $this->userid = $userid; $this->groupid = $groupid; $this->assignment = $assignment; $this->participants = $assignment->list_participants_with_filter_status_and_group($groupid); + $this->marker = $marker; if (!$this->userid && count($this->participants)) { $this->userid = reset($this->participants)->id; } @@ -101,6 +110,7 @@ class grading_app implements templatable, renderable { $export->hasmarkingworkflow = count($export->markingworkflowfilters) > 0; $export->markingallocationfilters = $this->assignment->get_marking_allocation_filters(true); $export->hasmarkingallocation = count($export->markingallocationfilters) > 0; + $export->ismarking = (int)$this->marker; $num = 1; foreach ($this->participants as $idx => $record) { diff --git a/public/mod/assign/classes/privacy/provider.php b/public/mod/assign/classes/privacy/provider.php index 8918fe1078b..a989bc30204 100644 --- a/public/mod/assign/classes/privacy/provider.php +++ b/public/mod/assign/classes/privacy/provider.php @@ -108,11 +108,23 @@ class provider implements 'assignment' => 'privacy:metadata:assignmentid', 'userid' => 'privacy:metadata:userid' ]; + $assignmark = [ + 'assignment' => 'privacy:metadata:assignmentid', + 'gradeid' => 'privacy:metadata:grade', + 'marker' => 'privacy:metadata:userid', + ]; + $assignallocatedusers = [ + 'assignment' => 'privacy:metadata:assignmentid', + 'studentid' => 'privacy:metadata:userid', + 'marker' => 'privacy:metadata:userid', + ]; $collection->add_database_table('assign_grades', $assigngrades, 'privacy:metadata:assigngrades'); $collection->add_database_table('assign_overrides', $assignoverrides, 'privacy:metadata:assignoverrides'); $collection->add_database_table('assign_submission', $assignsubmission, 'privacy:metadata:assignsubmissiondetail'); $collection->add_database_table('assign_user_flags', $assignuserflags, 'privacy:metadata:assignuserflags'); $collection->add_database_table('assign_user_mapping', $assignusermapping, 'privacy:metadata:assignusermapping'); + $collection->add_database_table('assign_mark', $assignmark, 'privacy:metadata:assignmark'); + $collection->add_database_table('assign_allocated_marker', $assignallocatedusers, 'privacy:metadata:assignallocatedmarker'); $collection->add_user_preference('assign_perpage', 'privacy:metadata:assignperpage'); $collection->add_user_preference('assign_filter', 'privacy:metadata:assignfilter'); $collection->add_user_preference('assign_markerfilter', 'privacy:metadata:assignmarkerfilter'); @@ -194,6 +206,37 @@ class provider implements $contextlist->add_from_sql($sql, $params); + // Contexts where the given user has assignment marks. + $sql = "SELECT ctx.id + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :modulename + JOIN {assign} a ON cm.instance = a.id + JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + JOIN {assign_grades} ag ON a.id = ag.assignment + JOIN {assign_mark} am ON am.gradeid = ag.id + WHERE ag.userid = :userid"; + $contextlist->add_from_sql($sql, $params); + + // Contexts where the given user has assigned assignment marks. + $sql = "SELECT ctx.id + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :modulename + JOIN {assign} a ON cm.instance = a.id + JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + JOIN {assign_mark} am ON am.assignment = a.id + WHERE am.marker = :userid"; + $contextlist->add_from_sql($sql, $params); + + // Contexts where the given user is either an allocated marker or has an allocated marker. + $sql = "SELECT ctx.id + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :modulename + JOIN {assign} a ON cm.instance = a.id + JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + JOIN {assign_allocated_marker} am ON am.assignment = a.id + WHERE am.marker = :graderid OR am.student = :userid"; + $contextlist->add_from_sql($sql, $params); + manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'get_context_for_userid_within_feedback', [$userid, $contextlist]); manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, @@ -266,6 +309,29 @@ class provider implements WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; $userlist->add_from_sql('userid', $sql, $params); + // Users who are either a student with a mark, or the teacher who has given a mark. + $sql = "SELECT g.userid, am.marker + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {assign} a ON a.id = cm.instance + JOIN {assign_grades} g ON a.id = g.assignment + JOIN {assign_mark} am ON am.gradeid = g.id + WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; + $userlist->add_from_sql('userid', $sql, $params); + $userlist->add_from_sql('marker', $sql, $params); + + // Users who are either a student with an allocated marker, or the allocated marker. + $sql = "SELECT am.student, am.marker + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {assign} a ON a.id = cm.instance + JOIN {assign_allocated_marker} am ON am.assignment = a.id + WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; + $userlist->add_from_sql('student', $sql, $params); + $userlist->add_from_sql('marker', $sql, $params); + manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_USER_INTERFACE, 'get_userids_from_context', [$userlist]); manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_USER_INTERFACE, @@ -302,6 +368,8 @@ class provider implements static::export_overrides($context, $assign, $user); static::export_submission($assign, $user, $context, []); + static::export_marks($assign, $user, $context); + static::export_allocations($assign, $user, $context); // Meta data. self::store_assign_user_flags($context, $assign, $user->id); if ($assign->is_blind_marking()) { @@ -349,6 +417,8 @@ class provider implements $DB->delete_records('assign_submission', ['assignment' => $assign->get_instance()->id]); $DB->delete_records('assign_user_flags', ['assignment' => $assign->get_instance()->id]); $DB->delete_records('assign_user_mapping', ['assignment' => $assign->get_instance()->id]); + static::delete_allocated_markers_for_users($assign); + static::delete_marks_for_users($assign); } } } @@ -394,6 +464,8 @@ class provider implements static::delete_overrides_for_users($assign, [$user->id]); $DB->delete_records('assign_user_flags', ['assignment' => $assignid, 'userid' => $user->id]); $DB->delete_records('assign_user_mapping', ['assignment' => $assignid, 'userid' => $user->id]); + static::delete_allocated_markers_for_users($assign, [$user->id]); + static::delete_marks_for_users($assign, [$user->id]); $DB->delete_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]); $DB->delete_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]); } @@ -440,6 +512,8 @@ class provider implements $params['assignment'] = $assignid; $DB->delete_records_select('assign_user_flags', "assignment = :assignment AND userid $sql", $params); $DB->delete_records_select('assign_user_mapping', "assignment = :assignment AND userid $sql", $params); + static::delete_allocated_markers_for_users($assign, $userids); + static::delete_marks_for_users($assign, $userids); $DB->delete_records_select('assign_grades', "assignment = :assignment AND userid $sql", $params); $DB->delete_records_select('assign_submission', "assignment = :assignment AND userid $sql", $params); } @@ -674,4 +748,127 @@ class provider implements } } } + + /** + * Delete all allocated markers for an array of users, for the given assignment. + * + * @param \assign $assign The assignment object. + * @param array $userids If empty, delete all marks for the assignment. + */ + protected static function delete_allocated_markers_for_users(\assign $assign, array $userids = []): void { + global $DB; + + if ($userids) { + [$insql, $inparams] = $DB->get_in_or_equal($userids); + $params = [$assign->get_instance()->id]; + // We need this twice as we're using the same $insql for student and marker. + $params = array_merge($params, $inparams); + $params = array_merge($params, $inparams); + $sql = " + SELECT id + FROM {assign_allocated_marker} + WHERE assignment = ? + AND (student {$insql} OR marker {$insql}) + "; + $DB->delete_records_subquery('assign_allocated_marker', 'id', 'id', $sql, $params); + } else { + $DB->delete_records('assign_allocated_marker', ['assignment' => $assign->get_instance()->id]); + } + } + + /** + * Delete all assignment marks for an array of users, for the given assignment. + * + * @param \assign $assign The assignment object. + * @param array $userids If empty, delete all marks for the assignment. + */ + protected static function delete_marks_for_users(\assign $assign, array $userids = []): void { + global $DB; + if ($userids) { + [$insql, $inparams] = $DB->get_in_or_equal($userids); + $params = [$assign->get_instance()->id]; + // We need this twice as we're using the same $insql for student and marker. + $params = array_merge($params, $inparams); + $params = array_merge($params, $inparams); + $sql = " + SELECT am.id + FROM {assign_mark} am + LEFT JOIN {assign_grades} ag ON ag.id = am.gradeid + WHERE am.assignment = ? + AND (ag.userid {$insql} OR am.marker {$insql}) + "; + $DB->delete_records_subquery('assign_mark', 'id', 'id', $sql, $params); + } else { + $DB->delete_records('assign_mark', ['assignment' => $assign->get_instance()->id]); + } + } + + /** + * Export the user's marks for the given assignment. + * + * @param \assign $assign The assignment object + * @param \stdClass $user The user object + * @param \context $context The context + */ + protected static function export_marks(\assign $assign, \stdClass $user, \context $context): void { + global $DB; + + $records = $DB->get_records_sql( + " + SELECT am.* + FROM {assign_mark} am + LEFT JOIN {assign_grades} ag on ag.id = am.gradeid + WHERE am.assignment = :assignment + AND (ag.userid = :uid1 OR am.marker = :uid2) + ", + [ + 'assignment' => $assign->get_instance()->id, + 'uid1' => $user->id, + 'uid2' => $user->id, + ] + ); + + $data = []; + foreach ($records as $row) { + $row = (array)$row; + unset($row['id']); + $data[] = $row; + } + + writer::with_context($context)->export_data([get_string('marks', 'mod_assign')], (object) ['data' => $data]); + } + + /** + * Export the user's marker allocations for the given assignment. + * + * @param \assign $assign The assignment object + * @param \stdClass $user The user object + * @param \context $context The context + */ + protected static function export_allocations(\assign $assign, \stdClass $user, \context $context): void { + global $DB; + + $records = $DB->get_records_sql( + " + SELECT * + FROM {assign_allocated_marker} + WHERE assignment = :assignment + AND (student = :uid1 OR marker = :uid2) + ", + [ + 'assignment' => $assign->get_instance()->id, + 'uid1' => $user->id, + 'uid2' => $user->id, + ] + ); + + $data = []; + foreach ($records as $row) { + $row = (array)$row; + unset($row['id']); + $data[] = $row; + } + + writer::with_context($context)->export_data([get_string('markerallocations', 'mod_assign')], (object) ['data' => $data]); + } } diff --git a/public/mod/assign/db/install.xml b/public/mod/assign/db/install.xml index da3b2765b7e..f31608336ad 100644 --- a/public/mod/assign/db/install.xml +++ b/public/mod/assign/db/install.xml @@ -34,6 +34,9 @@ + + + @@ -138,7 +141,6 @@ - @@ -168,5 +170,37 @@ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
diff --git a/public/mod/assign/db/upgrade.php b/public/mod/assign/db/upgrade.php index 6a79a0c5a0c..ab052950ea8 100644 --- a/public/mod/assign/db/upgrade.php +++ b/public/mod/assign/db/upgrade.php @@ -131,5 +131,91 @@ function xmldb_assign_upgrade($oldversion) { upgrade_mod_savepoint(true, 2026022300, 'assign'); } + if ($oldversion < 2026022301) { + // Define field markercount to be added to assign. + $table = new xmldb_table('assign'); + $field = new xmldb_field('markercount', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1', 'markingallocation'); + // Conditionally launch add field markercount. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field multimarkmethod to be added to assign. + $field = new xmldb_field('multimarkmethod', XMLDB_TYPE_CHAR, '10', null, false, false, null, 'markercount'); + // Conditionally launch add field multimarkmethod. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define table assign_mark to be created. + $table = new xmldb_table('assign_mark'); + + // Adding fields to table assign_mark. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('assignment', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('gradeid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('marker', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('mark', XMLDB_TYPE_NUMBER, '10,5', null, false, null, null); + $table->add_field('workflowstate', XMLDB_TYPE_CHAR, '20', null, null, null, null); + + // Adding keys to table assign_grades_mark. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('assignment', XMLDB_KEY_FOREIGN, ['assignment'], 'assign', ['id']); + $table->add_key('gradeid', XMLDB_KEY_FOREIGN, ['gradeid'], 'assign_grades', ['id']); + $table->add_key('marker', XMLDB_KEY_FOREIGN, ['marker'], 'user', ['id']); + + // Conditionally launch create table for assign_mark. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define table assign_allocated_marker to be created. + $table = new xmldb_table('assign_allocated_marker'); + + // Adding fields to table assign_allocated_marker. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('student', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('assignment', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('marker', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table assign_allocated_marker. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('student', XMLDB_KEY_FOREIGN, ['student'], 'user', ['id']); + $table->add_key('assignment', XMLDB_KEY_FOREIGN, ['assignment'], 'assign', ['id']); + $table->add_key('marker', XMLDB_KEY_FOREIGN, ['marker'], 'user', ['id']); + + // Conditionally launch create table for assign_allocated_marker. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define field allocatedmarker to be dropped from assign_user_flags. + $table = new xmldb_table('assign_user_flags'); + $field = new xmldb_field('allocatedmarker'); + + // Populate assign_allocated_marker before the allocatedmarker field is dropped. + if ($dbman->field_exists($table, $field)) { + $DB->execute( + "INSERT INTO {assign_allocated_marker} (assignment, student, marker) + SELECT assignment, userid, allocatedmarker + FROM {assign_user_flags}" + ); + $dbman->drop_field($table, $field); + } + + // Define field multimarkrounding to be added to assign. + $table = new xmldb_table('assign'); + $field = new xmldb_field('multimarkrounding', XMLDB_TYPE_INTEGER, '2', null, false, false, null, 'multimarkmethod'); + // Conditionally launch add field multimarkrounding. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Assign savepoint reached. + upgrade_mod_savepoint(true, 2026022301, 'assign'); + } + return true; } diff --git a/public/mod/assign/externallib.php b/public/mod/assign/externallib.php index 725e600ca52..527e06b956e 100644 --- a/public/mod/assign/externallib.php +++ b/public/mod/assign/externallib.php @@ -358,6 +358,9 @@ class mod_assign_external extends \mod_assign\external\external_api { 'm.maxattempts, ' . 'm.markingworkflow, ' . 'm.markingallocation, ' . + 'm.markercount, ' . + 'm.multimarkmethod, ' . + 'm.multimarkrounding, ' . 'm.markinganonymous, ' . 'm.requiresubmissionstatement, '. 'm.preventsubmissionnotingroup, '. @@ -438,6 +441,9 @@ class mod_assign_external extends \mod_assign\external\external_api { 'maxattempts' => $module->maxattempts, 'markingworkflow' => $module->markingworkflow, 'markingallocation' => $module->markingallocation, + 'markercount' => $module->markercount, + 'multimarkmethod' => $module->multimarkmethod, + 'multimarkrounding' => $module->multimarkrounding, 'markinganonymous' => $module->markinganonymous, 'requiresubmissionstatement' => $module->requiresubmissionstatement, 'preventsubmissionnotingroup' => $module->preventsubmissionnotingroup, @@ -558,6 +564,9 @@ class mod_assign_external extends \mod_assign\external\external_api { 'maxattempts' => new external_value(PARAM_INT, 'maximum number of attempts allowed'), 'markingworkflow' => new external_value(PARAM_INT, 'enable marking workflow'), 'markingallocation' => new external_value(PARAM_INT, 'enable marking allocation'), + 'markercount' => new external_value(PARAM_INT, 'number of markers'), + 'multimarkmethod' => new external_value(PARAM_TEXT, 'method to use when combining marks to a final grade'), + 'multimarkrounding' => new external_value(PARAM_INT, 'rounding direction to use when handling decimal marks'), 'markinganonymous' => new external_value(PARAM_INT, 'enable marking anonymous'), 'requiresubmissionstatement' => new external_value(PARAM_INT, 'student must accept submission statement'), 'preventsubmissionnotingroup' => new external_value(PARAM_INT, 'Prevent submission not in group', VALUE_OPTIONAL), @@ -1127,7 +1136,7 @@ class mod_assign_external extends \mod_assign\external\external_api { list($inorequalsql, $placeholders) = $DB->get_in_or_equal($requestedassignmentids, SQL_PARAMS_NAMED); $sql = "SELECT auf.id,auf.assignment,auf.userid,auf.locked,auf.mailed,". - "auf.extensionduedate,auf.workflowstate,auf.allocatedmarker ". + "auf.extensionduedate,auf.workflowstate " . "FROM {assign_user_flags} auf ". "WHERE auf.assignment ".$inorequalsql. " ORDER BY auf.assignment, auf.id"; @@ -1143,7 +1152,6 @@ class mod_assign_external extends \mod_assign\external\external_api { $userflag['mailed'] = $rd->mailed; $userflag['extensionduedate'] = $rd->extensionduedate; $userflag['workflowstate'] = $rd->workflowstate; - $userflag['allocatedmarker'] = $rd->allocatedmarker; if (is_null($currentassignmentid) || ($rd->assignment != $currentassignmentid )) { if (!is_null($assignment)) { @@ -1197,7 +1205,6 @@ class mod_assign_external extends \mod_assign\external\external_api { 'mailed' => new external_value(PARAM_INT, 'mailed'), 'extensionduedate' => new external_value(PARAM_INT, 'extension due date'), 'workflowstate' => new external_value(PARAM_ALPHA, 'marking workflow state', VALUE_OPTIONAL), - 'allocatedmarker' => new external_value(PARAM_INT, 'allocated marker') ) ) ) @@ -1545,7 +1552,8 @@ class mod_assign_external extends \mod_assign\external\external_api { array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), 'userid' => new external_value(PARAM_INT, 'The user id the submission belongs to'), - 'jsonformdata' => new external_value(PARAM_RAW, 'The data from the grading form, encoded as a json array') + 'jsonformdata' => new external_value(PARAM_RAW, 'The data from the grading form, encoded as a json array'), + 'marker' => new external_value(PARAM_BOOL, 'Flag, false if grading, true if marking', VALUE_DEFAULT, false), ) ); } @@ -1556,12 +1564,12 @@ class mod_assign_external extends \mod_assign\external\external_api { * @param int $assignmentid The id of the assignment * @param int $userid The id of the user the submission belongs to. * @param string $jsonformdata The data from the form, encoded as a json array. + * @param bool $marker Are we marking instead of grading? * @return array of warnings to indicate any errors. * @since Moodle 3.1 */ - public static function submit_grading_form($assignmentid, $userid, $jsonformdata) { + public static function submit_grading_form($assignmentid, $userid, $jsonformdata, $marker = false) { global $CFG, $USER; - require_once($CFG->dirroot . '/mod/assign/locallib.php'); require_once($CFG->dirroot . '/mod/assign/gradeform.php'); @@ -1569,7 +1577,8 @@ class mod_assign_external extends \mod_assign\external\external_api { array( 'assignmentid' => $assignmentid, 'userid' => $userid, - 'jsonformdata' => $jsonformdata + 'marker' => $marker, + 'jsonformdata' => $jsonformdata, )); list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); @@ -1585,7 +1594,8 @@ class mod_assign_external extends \mod_assign\external\external_api { 'userid' => $params['userid'], 'attemptnumber' => $data['attemptnumber'], 'rownum' => 0, - 'gradingpanel' => true + 'gradingpanel' => true, + 'marker' => $marker, ); if (WS_SERVER) { @@ -1600,7 +1610,6 @@ class mod_assign_external extends \mod_assign\external\external_api { // Data is injected into the form by the last param for the constructor. $mform = new mod_assign_grade_form(null, $formparams, 'post', '', null, true, $data); $validateddata = $mform->get_data(); - if ($validateddata) { $assignment->save_grade($params['userid'], $validateddata); } else { @@ -2637,8 +2646,13 @@ class mod_assign_external extends \mod_assign\external\external_api { 'onlyids' => new external_value(PARAM_BOOL, 'Do not return all user fields', VALUE_DEFAULT, false), 'includeenrolments' => new external_value(PARAM_BOOL, 'Do return courses where the user is enrolled', VALUE_DEFAULT, true), - 'tablesort' => new external_value(PARAM_BOOL, 'Apply current user table sorting preferences.', - VALUE_DEFAULT, false) + 'tablesort' => new external_value( + PARAM_BOOL, + 'Apply current user table sorting preferences.', + VALUE_DEFAULT, + false + ), + 'marking' => new external_value(PARAM_BOOL, 'Are we marking instead of grading?', VALUE_DEFAULT, false), ) ); } @@ -2654,12 +2668,22 @@ class mod_assign_external extends \mod_assign\external\external_api { * @param bool $onlyids Only return user ids. * @param bool $includeenrolments Return courses where the user is enrolled. * @param bool $tablesort Apply current user table sorting params from the grading table. + * @param bool $marking Are we marking instead of grading? * @return array of warnings and status result * @since Moodle 3.1 * @throws moodle_exception */ - public static function list_participants($assignid, $groupid, $filter, $skip, - $limit, $onlyids, $includeenrolments, $tablesort) { + public static function list_participants( + $assignid, + $groupid, + $filter, + $skip, + $limit, + $onlyids, + $includeenrolments, + $tablesort, + $marking + ) { global $DB, $CFG; require_once($CFG->dirroot . "/mod/assign/locallib.php"); require_once($CFG->dirroot . "/user/lib.php"); @@ -2674,7 +2698,8 @@ class mod_assign_external extends \mod_assign\external\external_api { 'limit' => $limit, 'onlyids' => $onlyids, 'includeenrolments' => $includeenrolments, - 'tablesort' => $tablesort + 'tablesort' => $tablesort, + 'marking' => $marking, )); $warnings = array(); @@ -2682,6 +2707,7 @@ class mod_assign_external extends \mod_assign\external\external_api { require_capability('mod/assign:view', $context); + $assign->set_is_marking($marking); $assign->require_view_grades(); $participants = array(); diff --git a/public/mod/assign/gradingtable.php b/public/mod/assign/gradingtable.php index da6d0c1c568..6ac23aa3358 100644 --- a/public/mod/assign/gradingtable.php +++ b/public/mod/assign/gradingtable.php @@ -151,8 +151,7 @@ class assign_grading_table extends table_sql implements renderable { $fields .= 'uf.mailed as mailed, '; $fields .= 'uf.locked as locked, '; $fields .= 'uf.extensionduedate as extensionduedate, '; - $fields .= 'uf.workflowstate as workflowstate, '; - $fields .= 'uf.allocatedmarker as allocatedmarker'; + $fields .= 'uf.workflowstate as workflowstate'; $from = '{user} u LEFT JOIN {assign_submission} s @@ -346,10 +345,14 @@ class assign_grading_table extends table_sql implements renderable { // Check to see if marker filter is set. $markerfilter = (int)get_user_preferences('assign_markerfilter', ''); if (!empty($markerfilter)) { + $from .= 'LEFT JOIN {assign_allocated_marker} am + ON u.id = am.student + AND am.assignment = :assignmentid4 '; + $params['assignmentid4'] = (int)$this->assignment->get_instance()->id; if ($markerfilter == ASSIGN_MARKER_FILTER_NO_MARKER) { - $where .= ' AND (uf.allocatedmarker IS NULL OR uf.allocatedmarker = 0)'; + $where .= ' AND am.marker IS NULL'; } else { - $where .= ' AND uf.allocatedmarker = :markerid'; + $where .= " AND am.marker = :markerid"; $params['markerid'] = $markerfilter; } } @@ -453,14 +456,21 @@ class assign_grading_table extends table_sql implements renderable { $columns[] = 'team'; $headers[] = get_string('submissionteam', 'assign'); } - // Allocated marker. + + // Multiple markers. if ($this->assignment->get_instance()->markingworkflow && $this->assignment->get_instance()->markingallocation && - has_capability('mod/assign:manageallocations', $this->assignment->get_context())) { - // Add a column for the allocated marker. - $columns[] = 'allocatedmarker'; - $headers[] = get_string('marker', 'assign'); + property_exists( + $assignment->get_instance(), + 'markercount', + ) + ) { + for ($i = 1; $i <= $assignment->get_instance()->markercount; $i++) { + $columns[] = "marker$i"; + $headers[] = get_string('markernumber', 'assign', $i); + } } + // Grade. $columns[] = 'grade'; $headers[] = get_string('gradenoun'); @@ -665,15 +675,6 @@ class assign_grading_table extends table_sql implements renderable { $o .= html_writer::div($allworkflowstates[$workflowstate]); } else { $o .= html_writer::select($workflowstates, $name, $workflowstate, ['' => $notmarked]); - // Check if this user is a marker that can't manage allocations and doesn't have the marker column added. - if ($this->assignment->get_instance()->markingworkflow && - $this->assignment->get_instance()->markingallocation && - !has_capability('mod/assign:manageallocations', $this->assignment->get_context())) { - - $name = 'quickgrade_' . $row->id . '_allocatedmarker'; - $o .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => $name, - 'value' => $row->allocatedmarker]); - } } } else { $o .= $this->output->container(get_string('markingworkflowstate' . $workflowstate, 'assign'), $workflowstate); @@ -696,56 +697,102 @@ class assign_grading_table extends table_sql implements renderable { return get_string('markingworkflowstate' . $state, 'assign'); } + /** + * Get the user object for the marker of a given student and marker number. + * + * @param int $studentid ID of the student. + * @param int $number Marker index number. + * @return stdClass|false User object or false if not found. + */ + protected function get_marker_number(int $studentid, int $number): stdClass|false { + global $DB; + $multimarkers = $DB->get_fieldset('assign_allocated_marker', 'marker', [ + 'student' => $studentid, 'assignment' => $this->assignment->get_instance()->id, + ]); + if (!empty($multimarkers) && count($multimarkers) >= $number) { + // Then get the name of the one at the column position requested, e.g. marker1, marker2, etc... + $index = $number - 1; + return \core_user::get_user($multimarkers[$index]); + } + return false; + } + /** * list current marker * * @param stdClass $row - The row of data - * @return id the user->id of the marker. + * @param int $markerpos - Marker position number - related to the markercount + * @return string The name of the allocated marker */ - public function col_allocatedmarker(stdClass $row) { + public function col_allocatedmarker(stdClass $row, int $markerpos = 1) { static $markers = null; static $markerlist = array(); - if ($markers === null) { - list($sort, $params) = users_order_by_sql('u'); - // Only enrolled users could be assigned as potential markers. - $markers = get_enrolled_users($this->assignment->get_context(), 'mod/assign:grade', 0, 'u.*', $sort); - $markerlist[0] = get_string('choosemarker', 'assign'); - $viewfullnames = has_capability('moodle/site:viewfullnames', $this->assignment->get_context()); - foreach ($markers as $marker) { - $markerlist[$marker->id] = fullname($marker, $viewfullnames); + + // Get the allocated markers that have been assigned to this student, if we are using multi-marking. + $allocatedmarker = $this->get_marker_number($row->userid, $markerpos); + + if ($this->is_downloading()) { + if ($allocatedmarker) { + return fullname( + $allocatedmarker, + has_capability('moodle/site:viewfullnames', $this->assignment->get_context()) + ); } - } - if (empty($markerlist)) { - // TODO: add some form of notification here that no markers are available. return ''; } - if ($this->is_downloading()) { - if (isset($markers[$row->allocatedmarker])) { - return fullname($markers[$row->allocatedmarker], - has_capability('moodle/site:viewfullnames', $this->assignment->get_context())); - } else { + + if ( + $this->quickgrading && + has_capability('mod/assign:manageallocations', $this->assignment->get_context()) && + ( + empty($row->workflowstate) || + $row->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INMARKING || + $row->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED + ) + ) { + // Get the potential users who could be assigned as an allocated marker. + if ($markers === null) { + [$sort, $params] = users_order_by_sql('u'); + // Only enrolled users could be assigned as potential markers. + $markers = get_enrolled_users($this->assignment->get_context(), 'mod/assign:grade', 0, 'u.*', $sort); + $markerlist[0] = get_string('choosemarker', 'assign'); + $viewfullnames = has_capability('moodle/site:viewfullnames', $this->assignment->get_context()); + foreach ($markers as $marker) { + $markerlist[$marker->id] = fullname($marker, $viewfullnames); + } + } + + if (empty($markerlist)) { return ''; } + + if (count($markers) > 1) { + $label = get_string('allocatedmarker', 'assign') . ' ' . $markerpos; + } else { + $label = get_string('allocatedmarker', 'assign'); + } + + $name = 'quickgrade_' . $row->id . '_allocatedmarker_' . $markerpos; + return html_writer::label( + $label, + 'menu' . $name + ) . html_writer::select($markerlist, $name, ($allocatedmarker) ? $allocatedmarker->id : '', false); } - if ($this->quickgrading && has_capability('mod/assign:manageallocations', $this->assignment->get_context()) && - (empty($row->workflowstate) || - $row->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INMARKING || - $row->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED)) { - - $name = 'quickgrade_' . $row->id . '_allocatedmarker'; - return html_writer::select($markerlist, $name, $row->allocatedmarker, false); - } else if (!empty($row->allocatedmarker)) { + if ($allocatedmarker) { $output = ''; if ($this->quickgrading) { // Add hidden field for quickgrading page. - $name = 'quickgrade_' . $row->id . '_allocatedmarker'; - $attributes = ['type' => 'hidden', 'name' => $name, 'value' => $row->allocatedmarker]; + $name = 'quickgrade_' . $row->id . '_allocatedmarker_' . $markerpos; + $attributes = ['type' => 'hidden', 'name' => $name, 'value' => $allocatedmarker->id]; $output .= html_writer::empty_tag('input', $attributes); } - $output .= $markerlist[$row->allocatedmarker]; + $output .= html_writer::tag('strong', fullname($allocatedmarker)); return $output; } + + return ''; } + /** * For download only - list all the valid options for this custom scale. * @@ -774,9 +821,10 @@ class assign_grading_table extends table_sql implements renderable { * @param int $userid The user id of the user this grade belongs to * @param int $modified Timestamp showing when the grade was last modified * @param float $deductedmark The deducted mark if penalty is applied + * @param int|null $markerid The allocated marker id if we are displaying a mark instead of an overall grade * @return string The formatted grade */ - public function display_grade($grade, $editable, $userid, $modified, float $deductedmark = 0) { + public function display_grade($grade, $editable, $userid, $modified, float $deductedmark = 0, ?int $markerid = null) { if ($this->is_downloading()) { if ($this->assignment->get_instance()->grade >= 0) { if ($grade == -1 || $grade === null) { @@ -793,7 +841,7 @@ class assign_grading_table extends table_sql implements renderable { return $scale; } } - return $this->assignment->display_grade($grade, $editable, $userid, $modified, $deductedmark); + return $this->assignment->display_grade($grade, $editable, $userid, $modified, $deductedmark, $markerid); } /** @@ -1003,10 +1051,29 @@ class assign_grading_table extends table_sql implements renderable { * @return string */ public function col_grade(stdClass $row): string { + global $DB, $USER; + $gradingdisabled = $this->assignment->grading_disabled($row->id, true, $this->gradinginfo); $displaygrade = $this->display_grade($row->grade, $this->quickgrading && !$gradingdisabled, $row->userid, $row->timemarked); - if (!$this->is_downloading() && $this->hasgrade) { + // If assignment uses multiple markers with the manual (aka agreement) + // method then the grader must be an existing marker so only show the + // action menu if they are. + $multimarkcangrade = true; + if ( + $this->assignment->is_using_multiple_marking() && + $this->assignment->get_instance()->multimarkmethod === 'manual' && + !$DB->get_record('assign_mark', ['gradeid' => $row->gradeid, 'marker' => $USER->id]) && + !$DB->get_record('assign_allocated_marker', [ + 'assignment' => $this->assignment->get_instance()->id, + 'marker' => $USER->id, + 'student' => $row->userid, + ]) + ) { + $multimarkcangrade = false; + } + + if (!$this->is_downloading() && $this->hasgrade && $multimarkcangrade) { $urlparams = [ 'id' => $this->assignment->get_course_module()->id, 'rownum' => 0, @@ -1045,6 +1112,128 @@ class assign_grading_table extends table_sql implements renderable { return $displaygrade; } + /** + * Format the marker column's data for display. + * + * @param stdClass $row Row object. + * @param int $col Marker number column. + * @return string + */ + public function col_marker(stdClass $row, int $col): string { + global $USER, $DB; + $allocatedmarker = ""; + $index = $col - 1; + + if ( + $this->assignment->get_instance()->markingworkflow && + $this->assignment->get_instance()->markingallocation + ) { + $allocatedmarker = $this->col_allocatedmarker($row, $col); + } + + $gradingdisabled = $this->assignment->grading_disabled($row->id, true, $this->gradinginfo); + $displaymark = ""; + + if (!$this->is_downloading() && $this->hasgrade) { + if ( + $this->assignment->get_instance()->markingworkflow && + $this->assignment->get_instance()->markingallocation + ) { + // Allocated markers are enabled: get the mark corresponding to + // the marker for this column. + $markers = array_values($DB->get_records('assign_allocated_marker', [ + 'student' => $row->userid, + 'assignment' => $this->assignment->get_instance()->id, + ], 'id')); + if (count($markers) > $index) { + $mark = $DB->get_record('assign_mark', ['gradeid' => $row->gradeid, 'marker' => $markers[$index]->marker]); + // Mark is only editable if we are quick grading, grading is not disabled, and if we are either + // the marker for this column, or we have manageallocations permissions. + $editable = ( + ($this->quickgrading) && + (!$gradingdisabled) && + ($USER->id == $markers[$index]->marker) + ); + $displaymark = $this->display_grade( + $mark->mark ?? null, + $editable, + $row->userid, + $row->timemarked, + 0, + $markers[$index]->marker, + ); + // Display the workflow state for this mark. + if ($markers[$index]->marker > 0) { + $displaymark .= html_writer::div( + get_string('markingworkflowstate' . ($mark->workflowstate ?? 'notmarked'), 'assign'), + 'badge bg-info d-block' + ); + } + } + } + + $urlparams = [ + 'id' => $this->assignment->get_course_module()->id, + 'rownum' => 0, + 'action' => 'marker', + ]; + + if ($this->assignment->is_blind_marking()) { + if (empty($row->recordid)) { + $row->recordid = $this->assignment->get_uniqueid_for_user($row->userid); + } + $urlparams['blindid'] = $row->recordid; + } else { + $urlparams['userid'] = $row->userid; + } + $url = new moodle_url('/mod/assign/view.php', $urlparams); + + // The container with the grade information. + $gradecontainer = $this->output->container($displaymark, 'w-100'); + + // Should this user get the 'Mark' action menu item? + $isallocatedmarker = false; + + if ( + $this->assignment->get_instance()->markingworkflow && + $this->assignment->get_instance()->markingallocation + ) { + // If allocated marking is enabled is this user the marker for this column? + if ( + $markers = $DB->get_fieldset('assign_allocated_marker', 'marker', [ + 'student' => $row->userid, + 'assignment' => $this->assignment->get_instance()->id, + ]) + ) { + $isallocatedmarker = (array_key_exists($index, $markers) && $markers[$index] == $USER->id); + } + } + + if ($isallocatedmarker) { + $menu = new action_menu(); + $menu->set_owner_selector('.gradingtable-actionmenu'); + $menu->set_boundary('window'); + $menu->set_kebab_trigger(get_string('markactions', 'assign')); + $menu->set_additional_classes('ps-2 ms-auto'); + // Prioritise the menu ahead of all other actions. + $menu->prioritise = true; + // Add the 'Mark' action item to the contextual menu. + $menu->add(new action_menu_link_secondary($url, null, get_string('markverb', 'assign'))); + // The contextual menu container. + $contextualmenucontainer = $this->output->container($this->output->render($menu), 'd-flex'); + return $allocatedmarker . + $this->output->container($gradecontainer . $contextualmenucontainer, ['class' => 'd-flex']); + } + } + // The table data is being downloaded, or the user cannot grade; therefore, only the formatted grade for display + // is returned. + if ($this->is_downloading()) { + return $displaymark; + } else { + return $allocatedmarker . $this->output->container($gradecontainer, ['class' => 'd-flex']); + } + } + /** * Format a column of data for display. * @@ -1641,6 +1830,12 @@ class assign_grading_table extends table_sql implements renderable { * @return mixed string or NULL */ public function other_cols($colname, $row) { + // If the column name is formatted as "marker" and a number, e.g. "marker1", this is a Marker column + // and so we need to call col_marker() instead with the marker number provided in the name. In this case "1". + if (str_starts_with($colname, 'marker') && ($col = substr($colname, 6)) && ctype_digit($col)) { + return $this->col_marker($row, $col); + } + // For extra user fields the result is already in $row. if (empty($this->plugincache[$colname])) { return parent::other_cols($colname, $row); diff --git a/public/mod/assign/lang/en/assign.php b/public/mod/assign/lang/en/assign.php index 0a4554a385f..ed10c483bc6 100644 --- a/public/mod/assign/lang/en/assign.php +++ b/public/mod/assign/lang/en/assign.php @@ -374,6 +374,7 @@ $string['introattachments'] = 'Additional files'; $string['introattachments_help'] = 'Additional files for use in the assignment, such as answer templates, may be added.'; $string['invalidgradeforscale'] = 'The grade supplied was not valid for the current scale'; $string['invalidfloatforgrade'] = 'The grade provided could not be understood: {$a}'; +$string['invalidmarkerallocation:notenoughmarkers'] = 'There are not enough markers ({$a->markers}) to meet the required number for this assignment ({$a->requested}).'; $string['invalidoverrideid'] = 'Invalid override id'; $string['lastmodifiedsubmission'] = 'Last modified (submission)'; $string['lastmodifiedgrade'] = 'Last modified (grade)'; @@ -384,9 +385,20 @@ $string['locksubmissionforstudent'] = 'Prevent any more submissions for student: $string['locksubmissions'] = 'Lock submissions'; $string['manageassignfeedbackplugins'] = 'Manage assignment feedback plugins'; $string['manageassignsubmissionplugins'] = 'Manage assignment submission plugins'; +$string['markactions'] = 'Mark actions'; +$string['markerallocations'] = 'Marker allocations'; $string['marker'] = 'Marker'; +$string['markercount'] = 'Markers'; +$string['markercount_help'] = 'Number of markers for the assignment.'; $string['markerfilter'] = 'Marker filter'; $string['markerfilternomarker'] = 'No marker'; +$string['markernumber'] = 'Marker {$a}'; +$string['markgradeaverage'] = 'Average mark'; +$string['markgradeaverage_help'] = 'Grade will be calculated as the average of all marks given.'; +$string['markgrademanual'] = 'Manual'; +$string['markgrademanual_help'] = 'Grade will need to be manually entered, it won\'t be calculated automatically.'; +$string['markgrademaximum'] = 'Maximum mark'; +$string['markgrademaximum_help'] = 'Grade will be calculated as the highest of the marks given.'; $string['markingallocation'] = 'Use marking allocation'; $string['markingallocation_help'] = 'If enabled together with marking workflow, markers can be allocated to particular students.'; $string['markinganonymous'] = 'Allow partial release of grades while marking anonymously'; @@ -409,6 +421,22 @@ $string['markingworkflowstatenotmarked'] = 'Not marked'; $string['markingworkflowstatereadyforreview'] = 'Marking completed'; $string['markingworkflowstatereadyforrelease'] = 'Ready for release'; $string['markingworkflowstatereleased'] = 'Released'; +$string['markoutof'] = 'Mark out of {$a}'; +$string['markoutof_help'] = 'Enter the mark for the student\'s submission here. You may include decimals.'; +$string['marks'] = 'Marks'; +$string['markverb'] = 'Mark'; +$string['multimarkmethod'] = 'Calculate grade'; +$string['multimarkmethod_help'] = 'How the grade will be calculated when there are multiple markers'; +$string['multimarkrounding'] = 'Rounding'; +$string['multimarkrounding_help'] = 'Whether to round up, down, naturally, or not at all when calculating an average mark'; +$string['multimarkrounding:down'] = 'Round down'; +$string['multimarkrounding:down_help'] = 'Average mark will be rounded down to the nearest whole number.'; +$string['multimarkrounding:natural'] = 'Natural rounding'; +$string['multimarkrounding:natural_help'] = 'Average mark will be rounded up or down naturally to the nearest whole number.'; +$string['multimarkrounding:none'] = 'No rounding'; +$string['multimarkrounding:none_help'] = 'Average mark will not be rounded.'; +$string['multimarkrounding:up'] = 'Round up'; +$string['multimarkrounding:up_help'] = 'Average mark will be rounded up to the nearest whole number.'; $string['maxattempts'] = 'Allowed attempts'; $string['maxattempts_help'] = 'The maximum number of submission attempts that can be made by a student. After this number has been reached, the submission can no longer be reopened.'; $string['maxgrade'] = 'Maximum grade'; @@ -500,10 +528,12 @@ $string['previoususer'] = 'Previous user'; $string['privacy:attemptpath'] = 'attempt {$a}'; $string['privacy:blindmarkingidentifier'] = 'The identifier used for anonymous submissions'; $string['privacy:gradepath'] = 'grade'; +$string['privacy:metadata:assignallocatedmarker'] = 'Stores mappings between allocated markers and student assignments'; $string['privacy:metadata:assigndownloadasfolders'] = 'A user preference for whether multiple file submissions should be downloaded into folders'; $string['privacy:metadata:assignfeedbackpluginsummary'] = 'Feedback data for the assignment.'; $string['privacy:metadata:assignfilter'] = 'Filter options such as \'Submitted\', \'Not submitted\', \'Requires grading\', and \'Granted extension\''; $string['privacy:metadata:assigngrades'] = 'Stores user grades for the assignment'; +$string['privacy:metadata:assignmark'] = 'Stores user marks for the assignment'; $string['privacy:metadata:assignmarkerfilter'] = 'Filter the assign summary by the assigned marker.'; $string['privacy:metadata:assignmentid'] = 'Assignment ID'; $string['privacy:metadata:assignmessageexplanation'] = 'Messages are sent to students through the messaging system.'; @@ -709,6 +739,7 @@ $string['usersnone'] = 'There are no users enrolled in this course or no users c $string['usersubmissioncannotberemoved'] = 'The submission of {$a} cannot be removed.'; $string['userswhoneedtosubmit'] = 'Users who need to submit: {$a}'; $string['usergrade'] = 'User grade'; +$string['usermark'] = 'User mark'; $string['validmarkingworkflowstates'] = 'Valid marking workflow states'; $string['viewadifferentattempt'] = 'View a different attempt'; $string['viewbatchsetmarkingworkflowstate'] = 'View batch set marking workflow state page.'; @@ -727,6 +758,9 @@ $string['viewfull'] = 'View full'; $string['viewsummary'] = 'View summary'; $string['viewsubmissiongradingtable'] = 'View submission grading table.'; $string['viewrevealidentitiesconfirm'] = 'View reveal student identities confirmation page.'; +$string['workflowcontext'] = 'Workflow context'; +$string['workflowcontext_help_grade'] = 'Apply the workflow state to the overall grade for the submission.'; +$string['workflowcontext_help_mark'] = 'Apply the workflow state to your mark, where you are an allocated marker.'; $string['workflowfilter'] = 'Workflow filter'; $string['xofy'] = '{$a->x} of {$a->y}'; diff --git a/public/mod/assign/lib.php b/public/mod/assign/lib.php index 582a7da3f11..9d7e1e9959d 100644 --- a/public/mod/assign/lib.php +++ b/public/mod/assign/lib.php @@ -1461,7 +1461,8 @@ function mod_assign_output_fragment_gradingpanel($args) { $viewargs = array( 'userid' => $userid, 'attemptnumber' => $attemptnumber, - 'formdata' => $formdata + 'formdata' => $formdata, + 'marker' => $args['marker'], ); return $assign->view('gradingpanel', $viewargs); diff --git a/public/mod/assign/locallib.php b/public/mod/assign/locallib.php index 6ab0d9aa921..87abc10b28c 100644 --- a/public/mod/assign/locallib.php +++ b/public/mod/assign/locallib.php @@ -84,6 +84,19 @@ define('ASSIGN_EVENT_TYPE_OPEN', 'open'); define('ASSIGN_EVENT_TYPE_CLOSE', 'close'); define('ASSIGN_EVENT_TYPE_EXTENSION', 'extension'); +define('ASSIGN_MULTIMARKING_METHOD_MANUAL', 'manual'); +define('ASSIGN_MULTIMARKING_METHOD_MAX', 'maximum'); +define('ASSIGN_MULTIMARKING_METHOD_AVERAGE', 'average'); +define('ASSIGN_MULTIMARKING_METHOD_FIRST', 'first'); + +define('ASSIGN_MULTIMARKING_AVERAGE_ROUND_NONE', 0); +define('ASSIGN_MULTIMARKING_AVERAGE_ROUND_NATURAL', 1); +define('ASSIGN_MULTIMARKING_AVERAGE_ROUND_DOWN', 2); +define('ASSIGN_MULTIMARKING_AVERAGE_ROUND_UP', 3); + +define('ASSIGN_MULTIMARKING_MAX_MARKERS', 10); +define('ASSIGN_MULTIMARKING_DEFAULT_MARKERS', 2); + require_once($CFG->libdir . '/accesslib.php'); require_once($CFG->libdir . '/formslib.php'); require_once($CFG->dirroot . '/repository/lib.php'); @@ -206,6 +219,11 @@ class assign { /** @var array $usersearch The content that the current user is looking for. */ protected array $usersearch = []; + /** + * @var bool $ismarking Are we marking, instead of grading? + */ + protected bool $ismarking = false; + /** * Constructor for the base assign class. * @@ -237,6 +255,15 @@ class assign { $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM); } + /** + * Set if we are marking instead of grading. + * + * @param bool $value True/False - are we marking? + */ + public function set_is_marking(bool $value): void { + $this->ismarking = $value; + } + /** * Set the action and parameters that can be used to return to the current page. * @@ -684,6 +711,8 @@ class assign { $o .= $this->view_edit_submission_page($mform, $notices); } else if ($action == 'grader') { $o .= $this->view_grader(); + } else if ($action == 'marker') { + $o .= $this->view_grader(true); } else if ($action == 'grading') { $o .= $this->view_grading_page(); } else if ($action == 'downloadall') { @@ -798,6 +827,19 @@ class assign { // Grade penalties. $update->gradepenalty = $formdata->gradepenalty ?? 0; + // If we are using simple grading and we specify a markercount, update the multi marking values. + if (property_exists($formdata, 'markercount') && property_exists($formdata, 'multimarkmethod')) { + $update->markercount = $formdata->markercount; + if ($formdata->markercount > 1) { + $update->multimarkmethod = $formdata->multimarkmethod; + if (property_exists($formdata, 'multimarkrounding')) { + $update->multimarkrounding = $formdata->multimarkrounding; + } + } + } else { + $update->markercount = 1; + } + $returnid = $DB->insert_record('assign', $update); $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST); // Cache the course record. @@ -895,6 +937,8 @@ class assign { $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id)); $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id)); $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id)); + $DB->delete_records('assign_allocated_marker', ['assignment' => $this->get_instance()->id]); + $DB->delete_records('assign_mark', ['assignment' => $this->get_instance()->id]); // Delete items from the gradebook. if (! $this->delete_grades()) { @@ -1258,6 +1302,8 @@ class assign { if (!empty($data->reset_gradebook_grades)) { $DB->delete_records_select('assign_grades', "assignment $sql", $params); + $DB->delete_records_select('assign_mark', "assignment $sql", $params); + $DB->delete_records_select('assign_allocated_marker', "assignment $sql", $params); // Remove all grades from gradebook. require_once($CFG->dirroot . '/mod/assign/lib.php'); assign_reset_gradebook($data->courseid); @@ -1591,6 +1637,24 @@ class assign { // Grade penalties. $update->gradepenalty = $formdata->gradepenalty ?? 0; + // If we are using simple grading and we specify a markercount, update the multi marking values. + if ( + property_exists($formdata, 'markercount') + && property_exists($formdata, 'advancedgradingmethod_submissions') + && $formdata->advancedgradingmethod_submissions === '' + ) { + $update->markercount = $formdata->markercount; + if ($formdata->markercount > 1) { + $update->multimarkmethod = $formdata->multimarkmethod; + $update->multimarkrounding = $formdata->multimarkrounding ?? null; + } + } else { + // If we don't specify a markercount, or we switched the grading type, return to defaults. + $update->markercount = 1; + $update->multimarkmethod = null; + $update->multimarkrounding = null; + } + $result = $DB->update_record('assign', $update); $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST); @@ -2035,14 +2099,19 @@ class assign { * @param int $userid The user id the grade belongs to * @param int $modified Timestamp from when the grade was last modified * @param float $deductedmark The deducted mark if penalty is applied + * @param int|null $markerid The allocated marker id if we are displaying a mark instead of an overall grade * @return string User-friendly representation of grade */ - public function display_grade($grade, $editing, $userid = 0, $modified = 0, float $deductedmark = 0) { + public function display_grade($grade, $editing, $userid = 0, $modified = 0, float $deductedmark = 0, ?int $markerid = null) { global $DB, $PAGE; static $scalegrades = array(); $o = ''; + $fieldname = ($markerid) ? 'quickmark_' . $userid . '_' . $markerid : 'quickgrade_' . $userid; + $fieldtitle = ($markerid) ? + get_string('usermark', 'assign') : + get_string('usergrade', 'assign'); if ($this->get_instance()->grade >= 0) { // Normal number. @@ -2052,12 +2121,10 @@ class assign { } else { $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals()); } - $o .= ''; + $o .= ''; $o .= '' . - get_string('usergrade', 'assign') . - ''; - $o .= ''; $o .= ''; foreach ($this->cache['scale'] as $optionid => $option) { $selected = ''; @@ -2372,18 +2436,20 @@ class assign { } } - if ($instance->markingworkflow && + if ( + $this->ismarking || + ( + $instance->markingworkflow && $instance->markingallocation && !has_capability('mod/assign:manageallocations', $this->get_context()) && - has_capability('mod/assign:grade', $this->get_context())) { - - $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf - ON u.id = uf.userid - AND uf.assignment = :assignmentid3'; - - $params['assignmentid3'] = (int) $instance->id; - - $additionalfilters .= ' AND uf.allocatedmarker = :markerid'; + has_capability('mod/assign:grade', $this->get_context()) + ) + ) { + $additionaljoins .= ' LEFT JOIN {assign_allocated_marker} am + ON u.id = am.student + AND am.assignment = :assignmentid3'; + $additionalfilters .= " AND am.marker = :markerid"; + $params['assignmentid3'] = $instance->id; $params['markerid'] = $USER->id; } @@ -3182,6 +3248,147 @@ class assign { return true; } + /** + * Validate that the given mark is valid for the point grading of the assignment. + * This basically checks that the mark given is not greater than the max grade, or less than zero. + * + * @param float $mark The mark to validate. + * @param int $maxgradevalue The assignment's max grade value to compare against. + * @return bool + */ + protected function validate_point_mark(float $mark, int $maxgradevalue): bool { + if ($mark > $maxgradevalue) { + return false; + } else if ($mark < 0) { + return false; + } + return true; + } + + /** + * Validate that a given mark is valid for the assignment's grading scale. + * + * @param float $mark The mark to validate. + * @param stdClass $scale The scale object used by the assignment. + * @return bool + */ + protected function validate_scale_mark(float $mark, stdClass $scale): bool { + $scaleoptions = make_menu_from_list($scale->scale); + if (!array_key_exists((int) $mark, $scaleoptions)) { + return false; + } + return true; + } + + /** + * Add or update an assign_mark record. + * + * @param stdClass $grade a grade record. + * @param mixed $mark The mark awarded by this marker, for example, 55.2. + * @param string|null $workflowstate The workflow state. + * @return bool + */ + public function update_mark(stdClass $grade, mixed $mark, ?string $workflowstate = null): bool { + global $DB; + + if ($workflowstate === '') { + $workflowstate = null; + } + + // Validate the mark, using the same logic as from update_grade(). + $gradevalue = $this->get_instance()->grade; + + // This uses a negative value here because when an assignment uses a grading scale instead of a point max + // it stores this as the negative version of the scale's ID, in the assignment's `grade` column. + // So, for example, if the assignment is using grading scale of ID 3, the assignment's `grade` column + // would be -3. So to look up the scale here, we reverse it again to the positive. + $scale = $DB->get_record('scale', ['id' => -$gradevalue]); + if ($mark) { + if ($gradevalue > 0 && !$this->validate_point_mark($mark, $gradevalue)) { + return false; + } else if ($scale && !$this->validate_scale_mark($mark, $scale)) { + return false; + } + } + + if ($record = $this->get_mark($grade->id, $grade->grader)) { + $updatedmark = ($record->mark != $mark); + $record->mark = $mark; + $record->workflowstate = $workflowstate; + $record->timemodified = time(); + $DB->update_record('assign_mark', $record); + } else { + $updatedmark = true; + $record = new stdClass(); + $record->assignment = $grade->assignment; + $record->gradeid = $grade->id; + $record->timecreated = $record->timemodified = time(); + $record->marker = $grade->grader; + $record->mark = $mark; + $record->workflowstate = $workflowstate; + $DB->insert_record('assign_mark', $record); + } + + if (!$updatedmark) { + return false; + } + + $marks = $this->get_marks($grade->id, $grade->userid); + + // If not all markers have left a mark, we can't calculate the grade yet. + if (count($marks) < $this->get_instance()->markercount) { + return true; + } + + // Calculate the grade based on the marks. + switch ($this->get_instance()->multimarkmethod) { + case 'maximum': + return $this->calculate_and_update_grade_from_maximum_mark($grade, $marks); + case 'average': + return $this->calculate_and_update_grade_from_average_mark($grade, $marks); + } + + // The manual method requires a manual intervention to set the grade, so nothing to do here. + + return true; + } + + /** + * Calculate and update the assignment grade to be the average of the marks received, taking into account rounding. + * + * @param stdClass $grade Grade object used by the assignment. + * @param array $marks Array of marker marks to average. + * @return bool + */ + protected function calculate_and_update_grade_from_average_mark(stdClass $grade, array $marks): bool { + $value = array_sum($marks) / count($marks); + // Do we need to round? + if (is_float($value)) { + if ($this->get_instance()->multimarkrounding == ASSIGN_MULTIMARKING_AVERAGE_ROUND_UP) { + $value = ceil($value); + } else if ($this->get_instance()->multimarkrounding == ASSIGN_MULTIMARKING_AVERAGE_ROUND_DOWN) { + $value = floor($value); + } else if ($this->get_instance()->multimarkrounding == ASSIGN_MULTIMARKING_AVERAGE_ROUND_NATURAL) { + $value = round($value); + } + // If rounding is not one of those options - default to no rounding. So $value unchanged. + } + $grade->grade = $value; + return $this->update_grade($grade); + } + + /** + * Calculate and update the assignment grade to be the maximum mark received. + * + * @param stdClass $grade Grade object used by the assignment. + * @param array $marks Array of marker marks. + * @return bool + */ + protected function calculate_and_update_grade_from_maximum_mark(stdClass $grade, array $marks): bool { + $grade->grade = grade_floatval(max($marks)); + return $this->update_grade($grade); + } + /** * View the grant extension date page. * @@ -4051,6 +4258,18 @@ class assign { return false; } + /** + * Get the mark object (if it exists) that corresponds to the specified marker and grade. + * + * @param int $gradeid The assignment grade ID + * @param int $markerid The marker's user ID + * @return stdClass|false The assign_mark object or false if it doesn't exist + */ + public function get_mark(int $gradeid, int $markerid): stdClass|false { + global $DB; + return $DB->get_record('assign_mark', ['gradeid' => $gradeid, 'marker' => $markerid]); + } + /** * This will retrieve a grade object from the db. * @@ -4071,7 +4290,7 @@ class assign { * @return string */ protected function view_single_grading_panel($args) { - global $DB, $CFG; + global $DB, $CFG, $USER; $o = ''; @@ -4162,19 +4381,28 @@ class assign { if ($grade) { $data = new stdClass(); + + if (!empty($flags->workflowstate)) { + $data->workflowstate = $flags->workflowstate; + } + if ($grade->grade !== null && $grade->grade >= 0) { $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals()); } + + if ($record = $this->get_mark($grade->id, $USER->id)) { + $data->mark = format_float($record->mark, $this->get_grade_item()->get_decimals()); + } + + // If we are viewing the marker page, overwrite the value for the workflowstate select menu + // to be from the mark, not the overall grade. + if (!empty($args['marker'])) { + $data->workflowstate = ($record) ? $record->workflowstate : null; + } } else { $data = new stdClass(); $data->grade = ''; - } - - if (!empty($flags->workflowstate)) { - $data->workflowstate = $flags->workflowstate; - } - if (!empty($flags->allocatedmarker)) { - $data->allocatedmarker = $flags->allocatedmarker; + $data->mark = ''; } // Warning if required. @@ -4192,7 +4420,8 @@ class assign { 'last' => $last, 'userid' => $userid, 'attemptnumber' => $attemptnumber, - 'gradingpanel' => true); + 'gradingpanel' => true, + 'marker' => $args['marker']); if (!empty($args['formdata'])) { $data = (array) $data; @@ -4750,9 +4979,11 @@ class assign { /** * View entire grader app. * + * @param bool $marker True if we're adding a mark (when muliple markers + * are enabled), false if we're adding a grade. * @return string */ - protected function view_grader() { + protected function view_grader($marker = false) { global $USER, $PAGE; $o = ''; @@ -4786,7 +5017,7 @@ class assign { $gradingtable->setup(); $currentgroup = groups_get_activity_group($this->get_course_module(), true); - $framegrader = new grading_app($userid, $currentgroup, $this); + $framegrader = new grading_app($userid, $currentgroup, $this, $marker); $this->update_effective_access($userid); @@ -5256,7 +5487,8 @@ class assign { $formparams = array( 'userscount' => count($userlist), 'usershtml' => $usershtml, - 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user() + 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user(), + 'assignment' => $this, ); $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams); @@ -5331,6 +5563,7 @@ class assign { } $formparams['markers'] = $markerlist; + $formparams['markercount'] = $this->get_instance()->markercount; $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams); $mform->set_data($formdata); // Initialises the hidden elements. @@ -6082,7 +6315,10 @@ class assign { } if ($this->can_view_grades()) { - $actionbuttons = new \mod_assign\output\actionmenu($this->get_course_module()->id); + $markingworkflow = + property_exists($this->get_instance(), 'markingworkflow') && + $this->get_instance()->markingworkflow == '1'; + $actionbuttons = new \mod_assign\output\actionmenu($this->get_course_module()->id, $markingworkflow); $o .= $this->get_renderer()->submission_actionmenu($actionbuttons); $summary = $this->get_assign_grading_summary_renderable(); @@ -7314,9 +7550,29 @@ class assign { $record = new stdClass(); $record->userid = $userid; if ($modified >= 0) { + // Get the current markers for this student assignment. + $record->currentallocatedmarkerids = array_column($this->get_allocated_markers($record->userid), 'marker'); + sort($record->currentallocatedmarkerids); $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT)); $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_ALPHA); - $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT); + + // Loop through current makers and check if there is a mark submitted for any of them. + $record->marks = []; + foreach ($record->currentallocatedmarkerids as $markerid) { + $record->marks[$markerid] = unformat_float( + optional_param('quickmark_' . $userid . '_' . $markerid, null, PARAM_TEXT) + ); + } + + // Then check if we changed the allocated marker for this student. + $record->allocatedmarkerids = []; + for ($i = 1; $i <= $this->get_instance()->markercount; $i++) { + $allocatedmarker = optional_param('quickgrade_' . $userid . '_allocatedmarker_' . $i, null, PARAM_INT); + if ($allocatedmarker) { + $record->allocatedmarkerids[] = $allocatedmarker; + } + } + sort($record->allocatedmarkerids); } else { // This user was not in the grading table. continue; @@ -7347,7 +7603,7 @@ class assign { WHERE s.assignment = :assignid1 AND s.latest = 1'; $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified, - uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber + uf.workflowstate, gmx.maxattempt AS attemptnumber FROM {user} u LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid LEFT JOIN {assign_grades} g ON @@ -7408,15 +7664,38 @@ class assign { if ($current->grade !== null) { $current->grade = floatval($current->grade); } + + // Get current marks. + $currentmarks = []; + if ($grade) { + $currentmarks = $DB->get_records('assign_mark', [ + 'gradeid' => $grade->id, + ], '', 'marker, mark'); + } + + foreach ($currentmarks as $currentmark) { + $current->marks[$currentmark->marker] = unformat_float($currentmark->mark); + } $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade); $markingallocationchanged = $this->get_instance()->markingworkflow && $this->get_instance()->markingallocation && - ($modified->allocatedmarker !== false) && - ($current->allocatedmarker != $modified->allocatedmarker); + ($modified->currentallocatedmarkerids != $modified->allocatedmarkerids); + $workflowstatechanged = $this->get_instance()->markingworkflow && ($modified->workflowstate !== false) && ($current->workflowstate != $modified->workflowstate); - if ($gradechanged || $markingallocationchanged || $workflowstatechanged) { + + // Have any of the marks changed? + $markschanged = array_filter( + $modified->marks, + fn($v, $k) => (!isset($current->marks[$k]) || $current->marks[$k] !== $v) && !is_null($modified->marks[$k]), + ARRAY_FILTER_USE_BOTH + ); + $markschanged = $this->get_instance()->markingworkflow && + $this->get_instance()->markingallocation && + $markschanged; + + if ($gradechanged || $markingallocationchanged || $workflowstatechanged || $markschanged) { // Grade changed. if ($this->grading_disabled($modified->userid)) { continue; @@ -7441,6 +7720,11 @@ class assign { // Ok - ready to process the updates. foreach ($modifiedusers as $userid => $modified) { + + $markingallocationchanged = $this->get_instance()->markingworkflow && + $this->get_instance()->markingallocation && + ($modified->currentallocatedmarkerids != $modified->allocatedmarkerids); + $grade = $this->get_user_grade($userid, true); $flags = $this->get_user_flags($userid, true); $grade->grade= grade_floatval(unformat_float($modified->grade)); @@ -7465,23 +7749,28 @@ class assign { $workflowstatemodified = ($modified->workflowstate !== false) && ($flags->workflowstate != $modified->workflowstate); - $allocatedmarkermodified = ($modified->allocatedmarker !== false) && - ($flags->allocatedmarker != $modified->allocatedmarker); - if ($workflowstatemodified) { $flags->workflowstate = $modified->workflowstate; - } - if ($allocatedmarkermodified) { - $flags->allocatedmarker = $modified->allocatedmarker; - } - if ($workflowstatemodified || $allocatedmarkermodified) { if ($this->update_user_flags($flags) && $workflowstatemodified) { $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger(); } } + $this->update_grade($grade); + // Update allocated markers. + if ($markingallocationchanged) { + $this->update_allocated_markers($modified->userid, $modified->allocatedmarkerids); + } + + // Update marks from allocated markers. + foreach ($modified->marks as $marker => $mark) { + if (!is_null($mark) && $marker == $grade->grader) { + $this->update_mark($grade, $mark); + } + } + // Allow teachers to skip sending notifications. if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) { $this->notify_grade_modified($grade, true); @@ -7515,6 +7804,44 @@ class assign { return get_string('quickgradingchangessaved', 'assign'); } + /** + * Update the markers allocated to a student's assignment + * @param int $studentid ID of the student user record + * @param array $markerids Array of user IDs for the markers + * @return void + */ + public function update_allocated_markers(int $studentid, array $markerids): void { + global $DB; + + // First, remove all markers allocated to this student and assignment. + // This leaves the marks in place orphaned, so they can be brought back if the marker is re-allocated. + $DB->delete_records('assign_allocated_marker', ['student' => $studentid, 'assignment' => $this->get_instance()->id]); + + // Store array of markers to make sure we don't try to add the same marker twice. + $markers = []; + + // Filter out empty markers. + $markerids = array_filter($markerids); + + // Then loop through the requested markers and assign them to the student/assignment. + foreach ($markerids as $markerid) { + if (in_array($markerid, $markers)) { + continue; + } + + $markers[] = $markerid; + $record = new stdClass(); + $record->student = $studentid; + $record->assignment = $this->get_instance()->id; + $record->marker = $markerid; + $DB->insert_record('assign_allocated_marker', $record); + + $student = $DB->get_record('user', ['id' => $studentid], '*', MUST_EXIST); + $marker = $DB->get_record('user', ['id' => $markerid], '*', MUST_EXIST); + \mod_assign\event\marker_updated::create_from_marker($this, $student, $marker)->trigger(); + } + } + /** * Reveal student identities to markers (and the gradebook). * @@ -7992,6 +8319,7 @@ class assign { $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0; $gradingpanel = !empty($params['gradingpanel']); $bothids = ($userid && $useridlistid); + $marker = (isset($params['marker']) && $params['marker']); if (!$userid || $bothids) { $useridlist = $this->get_grading_userid_list(true, $useridlistid); @@ -8032,12 +8360,20 @@ class assign { } else { // Use simple direct grading. if ($this->get_instance()->grade > 0) { - $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade); if (!$gradingdisabled) { - $gradingelement = $mform->addElement('text', 'grade', $name); - $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign'); - $mform->setType('grade', PARAM_RAW); + if (array_key_exists('marker', $params) && $params['marker']) { + $name = get_string('markoutof', 'assign', $this->get_instance()->grade); + $mform->addElement('text', 'mark', $name); + $mform->addHelpButton('mark', 'markoutof', 'assign'); + $mform->setType('mark', PARAM_RAW); + } else { + $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade); + $mform->addElement('text', 'grade', $name); + $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign'); + $mform->setType('grade', PARAM_RAW); + } } else { + $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade); $strgradelocked = get_string('gradelocked', 'assign'); $mform->addElement('static', 'gradedisabled', $name, $strgradelocked); $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign'); @@ -8045,13 +8381,23 @@ class assign { } else { $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade); if (count($grademenu) > 1) { - $gradingelement = $mform->addElement('select', 'grade', get_string('gradenoun') . ':', $grademenu); + if (array_key_exists('marker', $params) && $params['marker']) { + $name = get_string('markverb', 'assign'); + $gradingelement = $mform->addElement('select', 'mark', $name . ':', $grademenu); + // The mark is already formatted with format_float so it needs to be converted back to an integer. + if (!empty($data->mark)) { + $data->mark = (int)unformat_float($data->mark); + } + $mform->setType('mark', PARAM_INT); + } else { + $gradingelement = $mform->addElement('select', 'grade', get_string('gradenoun') . ':', $grademenu); - // The grade is already formatted with format_float so it needs to be converted back to an integer. - if (!empty($data->grade)) { - $data->grade = (int)unformat_float($data->grade); + // The grade is already formatted with format_float so it needs to be converted back to an integer. + if (!empty($data->grade)) { + $data->grade = (int)unformat_float($data->grade); + } + $mform->setType('grade', PARAM_INT); } - $mform->setType('grade', PARAM_INT); if ($gradingdisabled) { $gradingelement->freeze(); } @@ -8115,15 +8461,21 @@ class assign { } if ($this->get_instance()->markingworkflow) { - $states = $this->get_marking_workflow_states_for_current_user(); + if ($marker) { + $mark = $this->get_mark($grade->id, $USER->id); + $currentstate = ($mark) ? $mark->workflowstate : null; + } else { + $currentstate = $data->workflowstate ?? null; + } + $states = $this->get_marking_workflow_states_for_current_user((int)$marker); $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states; $select = $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options); $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign'); - if (!empty($data->workflowstate) && !array_key_exists($data->workflowstate, $states)) { + if (!empty($currentstate) && !array_key_exists($currentstate, $states)) { // In a workflow state that user should not be able to change, so freeze workflow selector. // Have to add the state so it shows in the frozen selector. $allworkflowstates = $this->get_all_marking_workflow_states(); - $select->addOption($allworkflowstates[$data->workflowstate], $data->workflowstate); + $select->addOption($allworkflowstates[$currentstate], $currentstate); $mform->freeze('workflowstate'); } $gradingstatus = $this->get_grading_status($userid); @@ -8153,12 +8505,49 @@ class assign { foreach ($markers as $marker) { $markerlist[$marker->id] = fullname($marker, $viewfullnames); } - $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist); - $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign'); - $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW); - $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW); - $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE); - $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED); + + // How many allocated markers are in use on this assignment? + $markercount = $this->get_instance()->markercount; + + // Who are the current markers for this student/assignment? + $markers = array_values($this->get_allocated_markers($userid)); + + for ($i = 0; $i < $markercount; $i++) { + $mform->addElement( + 'select', + 'allocatedmarker[' . $i . ']', + get_string('markernumber', 'assign', $i + 1), + $markerlist + ); + if (array_key_exists($i, $markers)) { + $mform->setDefault('allocatedmarker[' . $i . ']', $markers[$i]->marker); + } + $mform->addHelpButton('allocatedmarker[' . $i . ']', 'allocatedmarker', 'assign'); + $mform->disabledIf( + 'allocatedmarker[' . $i . ']', + 'workflowstate', + 'eq', + ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW + ); + $mform->disabledIf( + 'allocatedmarker[' . $i . ']', + 'workflowstate', + 'eq', + ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW + ); + $mform->disabledIf( + 'allocatedmarker[' . $i . ']', + 'workflowstate', + 'eq', + ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE + ); + $mform->disabledIf( + 'allocatedmarker[' . $i . ']', + 'workflowstate', + 'eq', + ASSIGN_MARKING_WORKFLOW_STATE_RELEASED + ); + } } $gradestring = '' . $gradestring . ''; @@ -8577,15 +8966,15 @@ class assign { * @return void */ protected function process_set_batch_marking_workflow_state() { - global $CFG, $DB; - + global $CFG, $DB, $USER; // Include batch marking workflow form. require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php'); $formparams = array( 'userscount' => 0, // This form is never re-displayed, so we don't need to 'usershtml' => '', // initialise these parameters with real information. - 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user() + 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user(), + 'assignment' => $this, ); $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams); @@ -8599,26 +8988,53 @@ class assign { $state = $formdata->markingworkflowstate; foreach ($useridlist as $userid) { + + $gradingdisabled = $this->grading_disabled($userid); + if ($gradingdisabled) { + continue; + } + + $updatingmark = false; + if (isset($formdata->workflowcontext) && $formdata->workflowcontext === 'mark') { + $updatingmark = true; + } + + if ($updatingmark && !$this->is_user_allocated_marker($USER->id, $userid)) { + continue; + } + + // We need the flags, regardless of context, as we might need to update notification flag. $flags = $this->get_user_flags($userid, true); - $flags->workflowstate = $state; + // If we are doing an update of workflow state for the overall submission grade. + if (!$updatingmark) { + $flags->workflowstate = $state; + } // Clear the mailed flag if notification is requested, the student hasn't been // notified previously, the student can access the assignment, and the state // is "Released". $modinfo = get_fast_modinfo($this->course, $userid); $cm = $modinfo->get_cm($this->get_course_module()->id); - if ($formdata->sendstudentnotifications && $cm->uservisible && - $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) { + if ( + $formdata->sendstudentnotifications && $cm->uservisible && + $state === ASSIGN_MARKING_WORKFLOW_STATE_RELEASED + ) { $flags->mailed = 0; } - $gradingdisabled = $this->grading_disabled($userid); + $grade = $this->get_user_grade($userid, true); - // Will not apply update if user does not have permission to assign this workflow state. - if (!$gradingdisabled && $this->update_user_flags($flags)) { - // Update Gradebook. - $grade = $this->get_user_grade($userid, true); + // Then if we are doing an update of workflow state for allocated marks, process that. + if ($updatingmark) { + $mark = $this->get_mark($grade->id, $USER->id); + $grade->grader = $USER->id; + $this->update_mark($grade, $mark ? $mark->mark : null, $state); + $this->calculate_and_save_overall_workflow_state($grade, $flags, $flags->workflowstate); + } + + // Then we still want to process the flags for notifications and write the event. + if ($this->update_user_flags($flags)) { // Fetch any feedback for this student. $gradebookplugin = $this->get_admin_config()->feedback_plugin_for_gradebook; $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin); @@ -8648,8 +9064,9 @@ class assign { require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php'); $formparams = array( - 'userscount' => 0, // This form is never re-displayed, so we don't need to - 'usershtml' => '' // initialise these parameters with real information. + 'userscount' => 0, + 'usershtml' => '', + 'markercount' => $this->get_instance()->markercount, ); list($sort, $params) = users_order_by_sql('u'); @@ -8670,7 +9087,14 @@ class assign { if ($formdata = $mform->get_data()) { $useridlist = explode(',', $formdata->selectedusers); - $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST); + $markers = []; + $markerindex = 1; + while (property_exists($formdata, 'allocatedmarker' . $markerindex)) { + if (!empty($formdata->{'allocatedmarker' . $markerindex})) { + $markers[] = $formdata->{'allocatedmarker' . $markerindex}; + } + $markerindex++; + } foreach ($useridlist as $userid) { $flags = $this->get_user_flags($userid, true); @@ -8682,12 +9106,7 @@ class assign { continue; // Allocated marker can only be changed in certain workflow states. } - $flags->allocatedmarker = $marker->id; - - if ($this->update_user_flags($flags)) { - $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); - \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger(); - } + $this->update_allocated_markers($userid, $markers); } } } @@ -8780,6 +9199,110 @@ class assign { return $this->unlock_submission($userid); } + /** + * Get all marks for a grade (a student submission record). + * @param int $gradeid + * @param int $studentid + * @return array + */ + public function get_marks(int $gradeid, int $studentid): array { + global $DB; + + // Old marks can still be stored in the database if markers have changed. So we specifically want to + // only get marks from currently allocated markers. + $marks = []; + $markers = $this->get_allocated_markers($studentid); + foreach ($markers as $marker) { + $value = $DB->get_field( + 'assign_mark', + 'mark', + [ + 'gradeid' => $gradeid, + 'marker' => $marker->marker, + ] + ); + if ($value !== false) { + $marks[$marker->marker] = $value; + } + } + + return $marks; + } + + /** + * Get all marks records for a grade (a student submission record). + * @param int $gradeid + * @param int $studentid + * @return array + */ + public function get_mark_records(int $gradeid, int $studentid): array { + global $DB; + + // Old marks can still be stored in the database if markers have changed. So we specifically want to + // only get marks from currently allocated markers. + $marks = []; + $markers = $this->get_allocated_markers($studentid); + foreach ($markers as $marker) { + $value = $DB->get_record('assign_mark', ['gradeid' => $gradeid, 'marker' => $marker->marker]); + if ($value !== false) { + $marks[$marker->marker] = $value; + } + } + + return $marks; + } + + /** + * Calculate what the overall marking workflow should be based on the allocated marker states, and save it. + * @param stdClass $grade + * @param stdClass $flags + * @param string|null $oldworkflowstate + * @return void + */ + public function calculate_and_save_overall_workflow_state(stdClass $grade, stdClass $flags, ?string $oldworkflowstate): void { + global $DB; + + $expected = $this->get_instance()->markercount; + $marks = $this->get_mark_records($grade->id, $grade->userid); + + $states = [ + ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED => 0, + ASSIGN_MARKING_WORKFLOW_STATE_INMARKING => 0, + ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW => 0, + ]; + + foreach ($marks as $mark) { + if ($mark->workflowstate == '') { + $mark->workflowstate = ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED; + } + $states[$mark->workflowstate]++; + } + + // If every marker has set theirs to Marking Complete, we can set the overall to "Marking Complete" as well. + if ($states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] === (int)$expected) { + $overall = ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW; + } else if ( + $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] > 0 || + $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] > 0 + ) { + // If any marker has progressed from "Not marked" we can set the overall to "In Marking". + $overall = ASSIGN_MARKING_WORKFLOW_STATE_INMARKING; + } else { + // Otherwise, every marker must still be in Not marked. So that's what we set. + $overall = ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED; + } + + $flags->workflowstate = $overall; + + if ( + $this->update_user_flags($flags) && + $overall !== $oldworkflowstate + ) { + $user = $DB->get_record('user', ['id' => $grade->userid], '*', MUST_EXIST); + \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $overall)->trigger(); + } + } + /** * Apply a grade from a grading form to a user (may be called multiple times for a group submission). * @@ -8790,6 +9313,14 @@ class assign { */ protected function apply_grade_to_user($formdata, $userid, $attemptnumber) { global $USER, $CFG, $DB; + // If we are using marker allocation, are we allocated to this student? If not, we should not be able to update + // their marks, even if they are in the same group as a student we are allocated to. + if (isset($formdata->mark) && $this->get_instance()->markingworkflow && $this->get_instance()->markingallocation) { + $markerids = array_column($this->get_allocated_markers($userid), 'marker'); + if (!in_array($USER->id, $markerids)) { + return; + } + } $grade = $this->get_user_grade($userid, true, $attemptnumber); $originalgrade = $grade->grade; @@ -8805,18 +9336,29 @@ class assign { $grade->grade = grade_floatval(unformat_float($formdata->grade)); } } - if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) { + if (isset($formdata->workflowstate)) { $flags = $this->get_user_flags($userid, true); $oldworkflowstate = $flags->workflowstate; $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate; - $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker; - if ($this->update_user_flags($flags) && - isset($formdata->workflowstate) && - $formdata->workflowstate !== $oldworkflowstate) { + // If we are saving a mark, set the overall workflow state to null and we will calculate it again shortly. + if (property_exists($formdata, 'mark')) { + $flags->workflowstate = null; + } + if ( + $this->update_user_flags($flags) && + isset($formdata->workflowstate) && + $formdata->workflowstate !== $oldworkflowstate + ) { $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger(); } } + + // Update allocated markers, but only if this is not a group submission. + // Otherwise we can end up overriding allocated markers by trying to apply a mark/workflow. + if (isset($formdata->allocatedmarker) && !$this->get_instance()->teamsubmission) { + $this->update_allocated_markers($userid, $formdata->allocatedmarker); + } } $grade->grader= $USER->id; @@ -8847,10 +9389,19 @@ class assign { } // We do not want to update the timemodified if no grade was added. - if (!empty($formdata->addattempt) || - ($originalgrade !== null && $originalgrade != -1) || - ($grade->grade !== null && $grade->grade != -1) || - $feedbackmodified) { + if (empty($formdata->addattempt) && property_exists($formdata, 'mark')) { + if (isset($formdata->workflowstate)) { + $this->update_mark($grade, $formdata->mark, $formdata->workflowstate); + $this->calculate_and_save_overall_workflow_state($grade, $flags, $oldworkflowstate); + } else { + $this->update_mark($grade, $formdata->mark); + } + } else if ( + !empty($formdata->addattempt) || + ($originalgrade !== null && $originalgrade != -1) || + ($grade->grade !== null && $grade->grade != -1) || + $feedbackmodified + ) { $this->update_grade($grade, !empty($formdata->addattempt)); } @@ -9451,28 +10002,46 @@ class assign { /** * Get the list of marking_workflow states the current user has permission to transition a grade to. * + * @param int $marker Are we on the Marker panel instead of the Grader panel? Integer representation of a bool + * as it's used as an array key to return values if already retrieved. * @return array of state => description */ - public function get_marking_workflow_states_for_current_user() { - if (!empty($this->markingworkflowstates)) { - return $this->markingworkflowstates; + public function get_marking_workflow_states_for_current_user(int $marker = 0) { + if (!empty($this->markingworkflowstates[$marker])) { + return $this->markingworkflowstates[$marker]; } $states = array(); if (has_capability('mod/assign:grade', $this->context)) { $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign'); $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign'); } - if (has_any_capability(array('mod/assign:reviewgrades', - 'mod/assign:managegrades'), $this->context)) { + if ( + has_any_capability( + [ + 'mod/assign:reviewgrades', + 'mod/assign:managegrades', + ], + $this->context + ) && + !$marker + ) { $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign'); $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign'); } - if (has_any_capability(array('mod/assign:releasegrades', - 'mod/assign:managegrades'), $this->context)) { + if ( + has_any_capability( + [ + 'mod/assign:releasegrades', + 'mod/assign:managegrades', + ], + $this->context + ) && + !$marker + ) { $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign'); } - $this->markingworkflowstates = $states; - return $this->markingworkflowstates; + $this->markingworkflowstates[$marker] = $states; + return $this->markingworkflowstates[$marker]; } /** @@ -9982,6 +10551,51 @@ class assign { public function is_marking_anonymous(): bool { return isset($this->get_instance()->markinganonymous) && $this->get_instance()->markinganonymous; } + + /** + * Is the assignment using multiple marker allocation? + * + * @return bool + */ + public function is_using_multiple_marking(): bool { + return ($this->get_instance()->markingworkflow + && $this->get_instance()->markingallocation + && $this->get_instance()->markercount > 1 + ); + } + + /** + * Check if a given user is allocated as a marker for a given student on this assignment. + * + * @param int $userid The ID of the user we are checking to see if they are a marker. + * @param int $studentid The ID of the student. + * @return bool + */ + public function is_user_allocated_marker(int $userid, int $studentid): bool { + global $DB; + + $record = $DB->count_records('assign_allocated_marker', [ + 'assignment' => $this->get_instance()->id, + 'student' => $studentid, + 'marker' => $userid, + ]); + + return ($record > 0); + } + + /** + * Get the markers allocated to the specified student on this assignment. + * + * @param int $studentid ID of the student. + * @return array Array of allocated_marker records for this student. + */ + public function get_allocated_markers(int $studentid): array { + global $DB; + return $DB->get_records('assign_allocated_marker', [ + 'student' => $studentid, + 'assignment' => $this->get_instance()->id, + ], 'id'); + } } /** diff --git a/public/mod/assign/mod_form.php b/public/mod/assign/mod_form.php index f4ea6d45ee2..ecc81dbe149 100644 --- a/public/mod/assign/mod_form.php +++ b/public/mod/assign/mod_form.php @@ -243,6 +243,82 @@ class mod_assign_mod_form extends moodleform_mod { $mform->addHelpButton('markingallocation', 'markingallocation', 'assign'); $mform->hideIf('markingallocation', 'markingworkflow', 'eq', 0); + $name = get_string('markercount', 'assign'); + $markercount = range(1, ASSIGN_MULTIMARKING_MAX_MARKERS); + $mform->addElement('select', 'markercount', $name, array_combine($markercount, $markercount)); + $mform->addHelpButton('markercount', 'markercount', 'assign'); + $mform->disabledIf('markercount', 'advancedgradingmethod_submissions', 'neq', ''); + $mform->hideIf('markercount', 'advancedgradingmethod_submissions', 'neq', ''); + $mform->hideIf('markercount', 'markingallocation', 'neq', '1'); + $mform->hideIf('markercount', 'markingworkflow', 'neq', '1'); + + $name = get_string('multimarkmethod', 'assign'); + $options = new core\output\choicelist(); + $options->add_option( + ASSIGN_MULTIMARKING_METHOD_MANUAL, + get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MANUAL, 'assign'), + [ + 'description' => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MANUAL . '_help', 'assign'), + ] + ); + $options->add_option( + ASSIGN_MULTIMARKING_METHOD_MAX, + get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MAX, 'assign'), + [ + 'description' => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MAX . '_help', 'assign'), + ] + ); + $options->add_option( + ASSIGN_MULTIMARKING_METHOD_AVERAGE, + get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_AVERAGE, 'assign'), + [ + 'description' => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_AVERAGE . '_help', 'assign'), + ] + ); + $mform->addElement('choicedropdown', 'multimarkmethod', $name, $options); + $mform->hideIf('multimarkmethod', 'markingallocation', 'eq', '0'); + $mform->hideIf('multimarkmethod', 'markercount', 'eq', '1'); + $mform->disabledIf('multimarkmethod', 'advancedgradingmethod_submissions', 'neq', ''); + $mform->hideIf('multimarkmethod', 'advancedgradingmethod_submissions', 'neq', ''); + + $name = get_string('multimarkrounding', 'assign'); + $options = new core\output\choicelist(); + $options->add_option( + ASSIGN_MULTIMARKING_AVERAGE_ROUND_NONE, + get_string('multimarkrounding:none', 'assign'), + [ + 'description' => get_string('multimarkrounding:none_help', 'assign'), + ] + ); + $options->add_option( + ASSIGN_MULTIMARKING_AVERAGE_ROUND_NATURAL, + get_string('multimarkrounding:natural', 'assign'), + [ + 'description' => get_string('multimarkrounding:natural_help', 'assign'), + ] + ); + $options->add_option( + ASSIGN_MULTIMARKING_AVERAGE_ROUND_DOWN, + get_string('multimarkrounding:down', 'assign'), + [ + 'description' => get_string('multimarkrounding:down_help', 'assign'), + ] + ); + $options->add_option( + ASSIGN_MULTIMARKING_AVERAGE_ROUND_UP, + get_string('multimarkrounding:up', 'assign'), + [ + 'description' => get_string('multimarkrounding:up_help', 'assign'), + ] + ); + $mform->addElement('choicedropdown', 'multimarkrounding', $name, $options); + $mform->hideIf('multimarkrounding', 'markingallocation', 'eq', '0'); + $mform->hideIf('multimarkrounding', 'markercount', 'eq', '1'); + $mform->hideIf('multimarkrounding', 'multimarkmethod', 'neq', 'average'); + $mform->disabledIf('multimarkrounding', 'multimarkmethod', 'neq', 'average'); + $mform->disabledIf('multimarkrounding', 'advancedgradingmethod_submissions', 'neq', ''); + $mform->hideIf('multimarkrounding', 'advancedgradingmethod_submissions', 'neq', ''); + $name = get_string('markinganonymous', 'assign'); $mform->addElement('selectyesno', 'markinganonymous', $name); $mform->addHelpButton('markinganonymous', 'markinganonymous', 'assign'); diff --git a/public/mod/assign/settings.php b/public/mod/assign/settings.php index 312a008ce7a..b9739f5659e 100644 --- a/public/mod/assign/settings.php +++ b/public/mod/assign/settings.php @@ -360,6 +360,62 @@ if ($ADMIN->fulltree) { $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); $settings->add($setting); + // Default setting for number of markers. + $name = new lang_string('markercount', 'mod_assign'); + $description = new lang_string('markercount_help', 'mod_assign'); + $setting = new admin_setting_configselect( + 'assign/markercount', + $name, + $description, + ASSIGN_MULTIMARKING_DEFAULT_MARKERS, + array_combine( + range(1, ASSIGN_MULTIMARKING_MAX_MARKERS), + range(1, ASSIGN_MULTIMARKING_MAX_MARKERS) + ), + ); + $setting->set_advanced_flag_options(admin_setting_flag::ENABLED, false); + $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + + // Default setting for multi-marking method (e.g. manual, max, etc...). + $name = new lang_string('multimarkmethod', 'mod_assign'); + $description = new lang_string('multimarkmethod_help', 'mod_assign'); + $options = [ + ASSIGN_MULTIMARKING_METHOD_MANUAL => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MANUAL, 'assign'), + ASSIGN_MULTIMARKING_METHOD_MAX => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MAX, 'assign'), + ASSIGN_MULTIMARKING_METHOD_AVERAGE => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_AVERAGE, 'assign'), + ]; + $setting = new admin_setting_configselect( + 'assign/multimarkmethod', + $name, + $description, + ASSIGN_MULTIMARKING_METHOD_MANUAL, + $options, + ); + $setting->set_advanced_flag_options(admin_setting_flag::ENABLED, false); + $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + + // Default setting for multi-marking rounding (e.g. up, down, none). + $name = new lang_string('multimarkrounding', 'mod_assign'); + $description = new lang_string('multimarkrounding_help', 'mod_assign'); + $options = [ + ASSIGN_MULTIMARKING_AVERAGE_ROUND_NONE => get_string('multimarkrounding:none', 'assign'), + ASSIGN_MULTIMARKING_AVERAGE_ROUND_NATURAL => get_string('multimarkrounding:natural', 'assign'), + ASSIGN_MULTIMARKING_AVERAGE_ROUND_DOWN => get_string('multimarkrounding:down', 'assign'), + ASSIGN_MULTIMARKING_AVERAGE_ROUND_UP => get_string('multimarkrounding:up', 'assign'), + ]; + $setting = new admin_setting_configselect( + 'assign/multimarkrounding', + $name, + $description, + ASSIGN_MULTIMARKING_AVERAGE_ROUND_NONE, + $options, + ); + $setting->set_advanced_flag_options(admin_setting_flag::ENABLED, false); + $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + $name = new lang_string('markinganonymous', 'mod_assign'); $description = new lang_string('markinganonymous_help', 'mod_assign'); $setting = new admin_setting_configcheckbox('assign/markinganonymous', diff --git a/public/mod/assign/templates/grading_navigation_user_selector.mustache b/public/mod/assign/templates/grading_navigation_user_selector.mustache index 0fe2dc97665..c2f07dd9b23 100644 --- a/public/mod/assign/templates/grading_navigation_user_selector.mustache +++ b/public/mod/assign/templates/grading_navigation_user_selector.mustache @@ -36,7 +36,7 @@ {{{larrow}}} - + {{{rarrow}}} diff --git a/public/mod/assign/templates/submission_actionmenu.mustache b/public/mod/assign/templates/submission_actionmenu.mustache index 82631acf22d..f77fcd98478 100644 --- a/public/mod/assign/templates/submission_actionmenu.mustache +++ b/public/mod/assign/templates/submission_actionmenu.mustache @@ -27,7 +27,8 @@ Example context (json): { - "gradelink": "https://moodle.org" + "gradelink": "https://moodle.org", + "marklink": "https://moodle.org" } }} @@ -38,5 +39,10 @@ {{#str}}gradeverb, core{{/str}} {{/gradelink}} + {{#marklink}} + + {{/marklink}} diff --git a/public/mod/assign/tests/backup/restore_date_test.php b/public/mod/assign/tests/backup/restore_date_test.php index c02ae086453..61275f474c0 100644 --- a/public/mod/assign/tests/backup/restore_date_test.php +++ b/public/mod/assign/tests/backup/restore_date_test.php @@ -127,6 +127,9 @@ final class restore_date_test extends \restore_date_testcase { 'timelimit' => DAYSECS, 'submissionattachments' => 1, 'gradepenalty' => 1, + 'markercount' => 5, + 'multimarkmethod' => 'average', + 'multimarkrounding' => 0, ]; $assign = $this->getDataGenerator()->create_module('assign', $record); diff --git a/public/mod/assign/tests/behat/allocate_markers.feature b/public/mod/assign/tests/behat/allocate_markers.feature new file mode 100644 index 00000000000..2048376ffef --- /dev/null +++ b/public/mod/assign/tests/behat/allocate_markers.feature @@ -0,0 +1,95 @@ +@mod @mod_assign @javascript +Feature: Allocate markers to student submissions + In order to use the marking workflow and record provisional marks before calculating a final grade + As a teacher + I need to allocate markers to student submissions + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | One | student1@example.com | + | student2 | Student | Two | student2@example.com | + | teacher1 | Teacher | One | teacher1@example.com | + | teacher2 | Teacher | Two | teacher2@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student2 | C1 | student | + | teacher1 | C1 | editingteacher | + | teacher2 | C1 | editingteacher | + And the following "activity" exists: + | activity | assign | + | course | C1 | + | idnumber | A1 | + | name | Assignment 1 | + | section | 1 | + | completion | 1 | + | markingworkflow | 1 | + | markingallocation | 1 | + | markercount | 2 | + | grade[modgrade_type] | point | + | grade[modgrade_point] | 100 | + + Scenario: Allocate markers to students via the Allocate Markers page + Given I am on the "A1" "assign activity" page logged in as teacher1 + And I navigate to "Submissions" in current page administration + And I set the field "selectall" to "1" + And I click on "Allocate marker" "button" in the "sticky-footer" "region" + And I click on "Allocate marker" "button" in the ".modal-footer" "css_element" + When I select "Teacher One" from the "Allocated marker 1" singleselect + And I select "Teacher Two" from the "Allocated marker 2" singleselect + And I press "Save changes" + Then the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | + | Student One | Teacher One | Teacher Two | + | Student Two | Teacher One | Teacher Two | + + Scenario: Allocate markers to students via the Quick Grading page + Given I am on the "A1" "assign activity" page logged in as teacher1 + And I navigate to "Submissions" in current page administration + And I click on "Quick grading" "checkbox" + When I set the field "Allocated marker 1" in the "Student One" "table_row" to "Teacher One" + And I set the field "Allocated marker 1" in the "Student Two" "table_row" to "Teacher One" + And I set the field "Allocated marker 2" in the "Student One" "table_row" to "Teacher Two" + And I set the field "Allocated marker 2" in the "Student Two" "table_row" to "Teacher Two" + And I click on "Save" "button" in the "sticky-footer" "region" + And I press "Continue" + And I click on "Quick grading" "checkbox" + Then the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | + | Student One | Teacher One | Teacher Two | + | Student Two | Teacher One | Teacher Two | + + Scenario: Allocate markers to students via the Grader page + Given I am on the "A1" "assign activity" page logged in as teacher1 + And I go to "Student One" "Assignment 1" activity advanced grading page + When I set the field "Marker 1" to "Teacher One" + And I set the field "Marker 2" to "Teacher Two" + And I press "Save changes" + And I am on the "A1" "assign activity" page + And I navigate to "Submissions" in current page administration + Then the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | + | Student One | Teacher One | Teacher Two | + | Student Two | | | + + Scenario: Allocate a duplicate marker to a student + Given I am on the "A1" "assign activity" page logged in as teacher1 + And I navigate to "Submissions" in current page administration + And I set the field "selectall" to "1" + And I click on "Allocate marker" "button" in the "sticky-footer" "region" + And I click on "Allocate marker" "button" in the ".modal-footer" "css_element" + When I select "Teacher One" from the "Allocated marker 1" singleselect + And I select "Teacher One" from the "Allocated marker 2" singleselect + And I press "Save changes" + Then the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | + | Student One | Teacher One | | + | Student Two | Teacher One | | + And the following should not exist in the "submissions" table: + | Marker 2 | + | Teacher One | + | Teacher One | diff --git a/public/mod/assign/tests/behat/allocate_marks.feature b/public/mod/assign/tests/behat/allocate_marks.feature new file mode 100644 index 00000000000..ee966948ef1 --- /dev/null +++ b/public/mod/assign/tests/behat/allocate_marks.feature @@ -0,0 +1,168 @@ +@mod @mod_assign @javascript +Feature: Allocate marks to student submissions + In order to assess a submission with multiple markers + As a teacher + I need to allocate marks to student submissions + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | One | student1@example.com | + | student2 | Student | Two | student2@example.com | + | teacher1 | Teacher | One | teacher1@example.com | + | teacher2 | Teacher | Two | teacher2@example.com | + | teacher3 | Teacher | Three | teacher3@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student2 | C1 | student | + | teacher1 | C1 | editingteacher | + | teacher2 | C1 | editingteacher | + | teacher3 | C1 | editingteacher | + And the following "activity" exists: + | activity | assign | + | course | C1 | + | idnumber | A1 | + | name | Assignment 1 | + | section | 1 | + | completion | 1 | + | markingworkflow | 1 | + | markingallocation | 1 | + | markercount | 2 | + | multimarkmethod | maximum | + | grade[modgrade_type] | point | + | grade[modgrade_point] | 100 | + | assignfeedback_comments_enabled | 1 | + And the following "mod_assign > marker_allocations" exist: + | assign | user | marker | + | Assignment 1 | student1 | teacher1 | + | Assignment 1 | student1 | teacher2 | + | Assignment 1 | student2 | teacher1 | + | Assignment 1 | student2 | teacher2 | + + Scenario: Allocate marks to students via the Quick Grading page + Given I am on the "A1" "assign activity" page logged in as teacher1 + And I navigate to "Submissions" in current page administration + And I click on "Quick grading" "checkbox" + When I set the field "User mark" in the "Student One" "table_row" to "99" + And I click on "Save" "button" in the "sticky-footer" "region" + And I press "Continue" + And I click on "Quick grading" "checkbox" + Then the following should exist in the "submissions" table: + | First name | Marker 1 | + | Student One | 99 | + + Scenario: Allocate marks to students via the Advanced Marker window + Given I am on the "A1" "assign activity" page logged in as teacher1 + And I go to "Student One" "Assignment 1" activity advanced marking page + When I set the field "Mark out of 100" to "50" + And I press "Save changes" + And I am on the "A1" "assign activity" page + And I navigate to "Submissions" in current page administration + Then the following should exist in the "submissions" table: + | First name | Marker 1 | + | Student One | 50 | + + Scenario: Set workflow state for an allocated mark via Advanced Marker window + Given I am on the "A1" "assign activity" page logged in as teacher1 + And I go to "Student One" "Assignment 1" activity advanced marking page + When I set the field "Mark out of 100" to "42" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I am on the "A1" "assign activity" page + And I navigate to "Submissions" in current page administration + Then the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | + | Student One | Marking completed | Not marked | In marking | + | Student Two | | | Not marked | + + Scenario: Bulk set workflow state as allocated markers + Given I am on the "A1" "assign activity" page logged in as teacher1 + And I navigate to "Submissions" in current page administration + And I set the field "selectall" to "1" + And I click on "Change marking state" "button" in the "sticky-footer" "region" + And I click on "Change marking state" "button" in the ".modal-footer" "css_element" + And I select "Mark" from the "Workflow context" singleselect + When I select "Marking completed" from the "Marking workflow state" singleselect + And I press "Save changes" + Then the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | + | Student One | Marking completed | Not marked | In marking | + | Student Two | Marking completed | Not marked | In marking | + # Log in as the other teacher and change their workflow status too. + And I am on the "A1" "assign activity" page logged in as teacher2 + And I navigate to "Submissions" in current page administration + And I set the field "selectall" to "1" + And I click on "Change marking state" "button" in the "sticky-footer" "region" + And I click on "Change marking state" "button" in the ".modal-footer" "css_element" + And I select "Mark" from the "Workflow context" singleselect + And I select "Marking completed" from the "Marking workflow state" singleselect + And I press "Save changes" + And the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | + | Student One | Marking completed | Marking completed | Marking completed | + | Student Two | Marking completed | Marking completed | Marking completed | + + Scenario: Grades are only calculated after all marks are given + Given I am on the "A1" "assign activity" page logged in as teacher1 + And I go to "Student One" "Assignment 1" activity advanced marking page + When I set the field "Mark out of 100" to "42" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I am on the "A1" "assign activity" page + And I navigate to "Submissions" in current page administration + Then the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | + | Student One | Marking completed | Not marked | In marking | + And the following should not exist in the "submissions" table: + | Grade | + | 42 | + # Now, provide marks as teacher2. + And I am on the "A1" "assign activity" page logged in as teacher2 + And I go to "Student One" "Assignment 1" activity advanced marking page + And I set the field "Mark out of 100" to "55" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I am on the "A1" "assign activity" page + And I navigate to "Submissions" in current page administration + And the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | Grade | + | Student One | Marking completed | Marking completed | Marking completed | 55 | + + Scenario: Grade updates when new allocated marker provides mark + Given I am on the "A1" "assign activity" page logged in as teacher1 + And I change window size to "large" + And I go to "Student One" "Assignment 1" activity advanced marking page + When I set the field "Mark out of 100" to "50" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + # Now, provide marks as teacher2. + And I am on the "A1" "assign activity" page logged in as teacher2 + And I go to "Student One" "Assignment 1" activity advanced marking page + And I set the field "Mark out of 100" to "51" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I am on the "A1" "assign activity" page + And I navigate to "Submissions" in current page administration + Then the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | Grade | + | Student One | Marking completed | Marking completed | Marking completed | 51 | + # Swap teacher2 for teacher3 and provide a new mark. + And I am on the "A1" "assign activity" page logged in as teacher3 + And I go to "Student One" "Assignment 1" activity advanced grading page + And I set the field "Marking workflow state" to "In marking" + And I set the field "Marker 2" to "Teacher Three" + And I press "Save changes" + And I am on the "A1" "assign activity" page + And I go to "Student One" "Assignment 1" activity advanced marking page + And I set the field "Mark out of 100" to "52" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I am on the "A1" "assign activity" page + And I navigate to "Submissions" in current page administration + And the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | Grade | + | Student One | Marking completed | Marking completed | Marking completed | 52 | diff --git a/public/mod/assign/tests/behat/behat_mod_assign.php b/public/mod/assign/tests/behat/behat_mod_assign.php index e5c4d2b6955..53875ec8d75 100644 --- a/public/mod/assign/tests/behat/behat_mod_assign.php +++ b/public/mod/assign/tests/behat/behat_mod_assign.php @@ -86,4 +86,31 @@ class behat_mod_assign extends behat_base { $rule = ['contextid' => 1, 'overdueby' => DAYSECS, 'penalty' => 10, 'sortorder' => 0]; $DB->insert_record('gradepenalty_duedate_rule', (object) $rule); } + + /** + * Goes to the student's advanced marking page. + * + * @Given /^I go to "(?P(?:[^"]|\\")*)" "(?P(?:[^"]|\\")*)" activity advanced marking page$/ + * @param string $userfullname The user's full name including firstname and lastname. + * @param string $activityname The activity name + */ + public function i_go_to_activity_advanced_marking_page(string $userfullname, string $activityname): void { + + // Step to access the user grade page from the grading page. + $this->execute('behat_navigation::go_to_breadcrumb_location', $this->escape($activityname)); + + $this->execute('behat_general::click_link', get_string('gradeitem:submissions', 'mod_assign')); + + $this->execute( + 'behat_general::i_click_on_in_the', + [ + $this->escape(get_string('markactions', 'assign')), + 'actionmenu', + $this->escape($userfullname), + 'table_row', + ] + ); + + $this->execute('behat_action_menu::i_choose_in_the_open_action_menu', get_string('markverb', 'mod_assign')); + } } diff --git a/public/mod/assign/tests/behat/filter_by_marker.feature b/public/mod/assign/tests/behat/filter_by_marker.feature index 8022cbd11b3..cb4cd6ad219 100644 --- a/public/mod/assign/tests/behat/filter_by_marker.feature +++ b/public/mod/assign/tests/behat/filter_by_marker.feature @@ -57,7 +57,7 @@ Feature: In an assignment, teachers can filter displayed submissions by assigned And I press "Save and display" And I am on the "Test assignment name" "assign activity" page And I go to "Student 1" "Test assignment name" activity advanced grading page - And I set the field "allocatedmarker" to "Marker 1" + And I set the field "Marker 1" to "Marker 1" And I set the field "Notify student" to "0" And I press "Save changes" And I am on the "Test assignment name" Activity page @@ -130,7 +130,7 @@ Feature: In an assignment, teachers can filter displayed submissions by assigned And I change window size to "large" And I go to "Student 1" "Test assignment name" activity advanced grading page # Allocate Marker 1 as the marker of Student 1. - And I set the field "allocatedmarker" to "Marker 1" + And I set the field "Marker 1" to "Marker 1" And I set the field "Notify student" to "0" And I press "Save changes" And I follow "View all submissions" diff --git a/public/mod/assign/tests/behat/grading_app_filters.feature b/public/mod/assign/tests/behat/grading_app_filters.feature index bc7e273354e..8f68d467987 100644 --- a/public/mod/assign/tests/behat/grading_app_filters.feature +++ b/public/mod/assign/tests/behat/grading_app_filters.feature @@ -32,6 +32,7 @@ Feature: In an assignment, teachers can change filters in the grading app | assignsubmission_file_enabled | 0 | | markingworkflow | 1 | | markingallocation | 1 | + | markercount | 1 | @javascript Scenario: Set filters in the grading table and see them in the grading app @@ -40,7 +41,7 @@ Feature: In an assignment, teachers can change filters in the grading app And I should not see "Course 1 &" And the "title" attribute of "a[title='Course: Course 1 &']" "css_element" should not contain "&" And I should not see "Test assignment name &" - And I set the field "allocatedmarker" to "Marker 1" + And I set the field "Marker 1" to "Marker 1" And I set the field "workflowstate" to "In marking" And I set the field "Notify student" to "0" And I press "Save changes" @@ -64,7 +65,7 @@ Feature: In an assignment, teachers can change filters in the grading app Scenario: Set filters in the grading app and see them in the grading table Given I am on the "Test assignment name &" Activity page logged in as teacher1 And I go to "Student 1" "Test assignment name &" activity advanced grading page - And I set the field "allocatedmarker" to "Marker 1" + And I set the field "Marker 1" to "Marker 1" And I set the field "workflowstate" to "In marking" And I set the field "Notify student" to "0" And I press "Save changes" diff --git a/public/mod/assign/tests/behat/multimarking_workflow.feature b/public/mod/assign/tests/behat/multimarking_workflow.feature new file mode 100644 index 00000000000..3c33e4d4a0d --- /dev/null +++ b/public/mod/assign/tests/behat/multimarking_workflow.feature @@ -0,0 +1,241 @@ +@mod @mod_assign @javascript +Feature: Complete multi-marking workflow + In order to conduct an assignment with multi-marking + As a teacher + I need to: + - Allocate markers to the student(s) + - Allocate marks to the student(s) submission(s) + - Calculate a final grade based on the configured multi-marking agreement method + - Release the final grade to the student(s) + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | One | student1@example.com | + | student2 | Student | Two | student2@example.com | + | student3 | Student | Three | student3@example.com | + | student4 | Student | Four | student4@example.com | + | teacher1 | Teacher | One | teacher1@example.com | + | teacher2 | Teacher | Two | teacher2@example.com | + | teacher3 | Teacher | Three | teacher3@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + | teacher1 | C1 | editingteacher | + | teacher2 | C1 | editingteacher | + | teacher3 | C1 | editingteacher | + And the following "groups" exist: + | name | course | idnumber | participation | + | Group 1 | C1 | G1 | 1 | + | Group 2 | C1 | G2 | 1 | + And the following "group members" exist: + | user | group | + | student1 | G1 | + | student2 | G1 | + | student3 | G2 | + | student4 | G2 | + And the following "activity" exists: + | activity | assign | + | course | C1 | + | idnumber | A1 | + | name | Assignment 1 | + | section | 1 | + | completion | 1 | + | markingworkflow | 1 | + | markingallocation | 1 | + | markercount | 2 | + | multimarkmethod | average | + | multimarkrounding | 1 | + | grade[modgrade_type] | point | + | grade[modgrade_point] | 100 | + And the following "activity" exists: + | activity | assign | + | course | C1 | + | idnumber | A2 | + | name | Assignment 2 | + | section | 1 | + | completion | 1 | + | markingworkflow | 1 | + | markingallocation | 1 | + | markercount | 2 | + | multimarkmethod | maximum | + | multimarkrounding | 1 | + | teamsubmission | 1 | + | grade[modgrade_type] | point | + | grade[modgrade_point] | 100 | + + Scenario: Complete workflow of multi-marking + # Firstly, allocate the markers to the students. + Given I am on the "A1" "assign activity" page logged in as teacher1 + And I change window size to "large" + And I navigate to "Submissions" in current page administration + And I set the field "selectall" to "1" + When I click on "Allocate marker" "button" in the "sticky-footer" "region" + And I click on "Allocate marker" "button" in the ".modal-footer" "css_element" + And I select "Teacher One" from the "Allocated marker 1" singleselect + And I select "Teacher Two" from the "Allocated marker 2" singleselect + And I press "Save changes" + Then the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | + | Student One | Teacher One | Teacher Two | Not marked | + | Student Two | Teacher One | Teacher Two | Not marked | + | Student Three | | | Not marked | + | Student Four | | | Not marked | + # Then allocate marks to the student submissions as teacher1. + And I go to "Student One" "Assignment 1" activity advanced marking page + And I set the field "Mark out of 100" to "99" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I go to "Student Two" "Assignment 1" activity advanced marking page + And I set the field "Mark out of 100" to "11" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I am on the "A1" "assign activity" page + And I navigate to "Submissions" in current page administration + And the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | + | Student One | 99 | | In marking | + | Student Two | 11 | | In marking | + | Student Three | | | Not marked | + | Student Four | | | Not marked | + # Then allocate marks to the student submissions as teacher2. + And I am on the "A1" "assign activity" page logged in as teacher2 + And I go to "Student One" "Assignment 1" activity advanced marking page + And I set the field "Mark out of 100" to "88" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I go to "Student Two" "Assignment 1" activity advanced marking page + And I set the field "Mark out of 100" to "22" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I am on the "A1" "assign activity" page + And I navigate to "Submissions" in current page administration + And the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | Grade | + | Student One | 99 | 88 | Marking completed | 94 | + | Student Two | 11 | 22 | Marking completed | 17 | + | Student Three | | | Not marked | | + | Student Four | | | Not marked | | + # Then we check the calculated final grade and release them to the students. + And I am on the "A1" "assign activity" page logged in as teacher1 + And I navigate to "Submissions" in current page administration + And I set the field "selectall" to "1" + And I click on "Change marking state" "button" in the "sticky-footer" "region" + And I click on "Change marking state" "button" in the "Set marking workflow state" "dialogue" + And I set the field "Workflow context" to "Grade" + And I set the field "Marking workflow state" to "Released" + And I press "Save changes" + And the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | Final grade | + | Student One | 99 | 88 | Released | 94 | + | Student Two | 11 | 22 | Released | 17 | + | Student Three | | | Released | | + | Student Four | | | Released | | + + Scenario: Complete workflow of multi-marking with group submissions + # Firstly, allocate the markers to the students (to test thoroughly, one group will have the same marker + # for both students. The other group won't). + Given I am on the "A2" "assign activity" page logged in as teacher1 + And I change window size to "large" + And I navigate to "Submissions" in current page administration + And I click on "Quick grading" "checkbox" + When I set the field "Allocated marker 1" in the "Student One" "table_row" to "Teacher One" + And I set the field "Allocated marker 1" in the "Student Two" "table_row" to "Teacher One" + And I set the field "Allocated marker 1" in the "Student Three" "table_row" to "Teacher One" + And I set the field "Allocated marker 1" in the "Student Four" "table_row" to "Teacher One" + And I set the field "Allocated marker 2" in the "Student One" "table_row" to "Teacher Two" + And I set the field "Allocated marker 2" in the "Student Two" "table_row" to "Teacher Two" + And I set the field "Allocated marker 2" in the "Student Three" "table_row" to "Teacher Two" + And I set the field "Allocated marker 2" in the "Student Four" "table_row" to "Teacher Three" + And I click on "Save" "button" in the "sticky-footer" "region" + And I press "Continue" + And I click on "Quick grading" "checkbox" + Then the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | + | Student One | Teacher One | Teacher Two | | + | Student Two | Teacher One | Teacher Two | | + | Student Three | Teacher One | Teacher Two | | + | Student Four | Teacher One | Teacher Three | | + # Next we test adding a mark as Marker 1 (teacher1) to a student in Group 1. This should populate to the other + # student in Group 1, but not the student with this same marker, who is not in Group 1. + And I go to "Student One" "Assignment 2" activity advanced marking page + And I set the field "Mark out of 100" to "50" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I go to "Student Four" "Assignment 2" activity advanced marking page + And I set the field "Mark out of 100" to "60" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I am on the "A2" "assign activity" page + And I navigate to "Submissions" in current page administration + And the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | + | Student One | 50 | | In marking | + | Student Two | 50 | | In marking | + | Student Three | 60 | | In marking | + | Student Four | 60 | | In marking | + # Next we add a mark as teacher2 to a student in both groups. The mark given to the student in Group 1 should + # populate to the other student in Group 1. The mark given to the student in Group 2 should not, as they are not + # an allocated marker for that final student. + And I am on the "A2" "assign activity" page logged in as teacher2 + And I go to "Student One" "Assignment 2" activity advanced marking page + And I set the field "Mark out of 100" to "30" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I am on the "A2" "assign activity" page + And I go to "Student Three" "Assignment 2" activity advanced marking page + And I set the field "Mark out of 100" to "15" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I am on the "A2" "assign activity" page + And I navigate to "Submissions" in current page administration + And the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | + | Student One | 50 | 30 | Marking completed | + | Student Two | 50 | 30 | Marking completed | + | Student Three | 60 | 15 | Marking completed | + | Student Four | 60 | | In marking | + # Then we login as teacher3 and add that last mark to the final student. + And I am on the "A2" "assign activity" page logged in as teacher3 + And I go to "Student Four" "Assignment 2" activity advanced marking page + And I set the field "Mark out of 100" to "99" + And I set the field "Marking workflow state" to "Marking completed" + And I press "Save changes" + And I am on the "A2" "assign activity" page + And I navigate to "Submissions" in current page administration + And the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | + | Student One | 50 | 30 | Marking completed | + | Student Two | 50 | 30 | Marking completed | + | Student Three | 60 | 15 | Marking completed | + | Student Four | 60 | 99 | Marking completed | + # Then we check that the grades have been calculated correctly. + And I am on the "A2" "assign activity" page logged in as teacher1 + And I navigate to "Submissions" in current page administration + And the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | Grade | + | Student One | 50 | 30 | Marking completed | 50 | + | Student Two | 50 | 30 | Marking completed | 50 | + | Student Three | 60 | 15 | Marking completed | 60 | + | Student Four | 60 | 99 | Marking completed | 99 | + # Then we release them all and check the final grade column. + And I am on the "A2" "assign activity" page logged in as teacher1 + And I navigate to "Submissions" in current page administration + And I set the field "selectall" to "1" + And I click on "Change marking state" "button" in the "sticky-footer" "region" + And I click on "Change marking state" "button" in the "Set marking workflow state" "dialogue" + And I set the field "Workflow context" to "Grade" + And I set the field "Marking workflow state" to "Released" + And I press "Save changes" + And the following should exist in the "submissions" table: + | First name | Marker 1 | Marker 2 | Status | Final grade | + | Student One | 50 | 30 | Released | 50 | + | Student Two | 50 | 30 | Released | 50 | + | Student Three | 60 | 15 | Released | 60 | + | Student Four | 60 | 100 | Released | 99 | diff --git a/public/mod/assign/tests/event/events_test.php b/public/mod/assign/tests/event/events_test.php index db20f6e4ff0..32311fc819c 100644 --- a/public/mod/assign/tests/event/events_test.php +++ b/public/mod/assign/tests/event/events_test.php @@ -1076,7 +1076,8 @@ final class events_test extends \advanced_testcase { $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); - $assign = $this->create_instance($course); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $assign = $this->create_instance($course, ['markercount' => 1]); // Trigger and capture the event. $sink = $this->redirectEvents(); diff --git a/public/mod/assign/tests/externallib_test.php b/public/mod/assign/tests/externallib_test.php index 2defcd410b9..e552652e29c 100644 --- a/public/mod/assign/tests/externallib_test.php +++ b/public/mod/assign/tests/externallib_test.php @@ -735,7 +735,6 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase { $userflag->mailed = 0; $userflag->extensionduedate = 0; $userflag->workflowstate = 'inmarking'; - $userflag->allocatedmarker = $USER->id; $DB->insert_record('assign_user_flags', $userflag); @@ -757,7 +756,6 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase { $this->assertEquals(0, $userflag['mailed']); $this->assertEquals(0, $userflag['extensionduedate']); $this->assertEquals('inmarking', $userflag['workflowstate']); - $this->assertEquals($USER->id, $userflag['allocatedmarker']); } /** @@ -1742,7 +1740,6 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase { $userflags = array(); $userflag['userid'] = $student->id; $userflag['workflowstate'] = 'inmarking'; - $userflag['allocatedmarker'] = $USER->id; $userflags = array($userflag); $createduserflags = mod_assign_external::set_user_flags($assign->id, $userflags); @@ -1759,7 +1756,6 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase { $this->assertEquals(2, $createduserflag->mailed); $this->assertEquals(0, $createduserflag->extensionduedate); $this->assertEquals('inmarking', $createduserflag->workflowstate); - $this->assertEquals($USER->id, $createduserflag->allocatedmarker); // Create update data. $userflags = array(); @@ -1781,7 +1777,6 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase { $this->assertEquals(2, $updateduserflag->mailed); $this->assertEquals(0, $updateduserflag->extensionduedate); $this->assertEquals('readyforreview', $updateduserflag->workflowstate); - $this->assertEquals($USER->id, $updateduserflag->allocatedmarker); } /** @@ -2824,7 +2819,7 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase { $DB->update_record('user', $student); $this->setUser($teacher); - $participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, true, true); + $participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, true, true, false); $participants = external_api::clean_returnvalue(mod_assign_external::list_participants_returns(), $participants); $this->assertCount(1, $participants); @@ -2843,7 +2838,7 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase { $this->assertEquals('', $participant['submissionstatus']); $this->assertArrayHasKey('enrolledcourses', $participant); - $participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, false, true); + $participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, false, true, false); $participants = external_api::clean_returnvalue(mod_assign_external::list_participants_returns(), $participants); // Check that the list of courses the participant is enrolled is not returned. $participant = $participants[0]; @@ -2919,7 +2914,7 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase { $this->setUser($teacher); // Test mod_assign_external::list_participants. - $participants = mod_assign_external::list_participants($assignmodule->id, $group->id, '', 0, 0, false, true, true); + $participants = mod_assign_external::list_participants($assignmodule->id, $group->id, '', 0, 0, false, true, true, false); $participants = external_api::clean_returnvalue(mod_assign_external::list_participants_returns(), $participants); $this->assertEquals($group->id, $participants[0]['groupid']); $this->assertEquals(format_string($gname, true), $participants[0]['groupname']); diff --git a/public/mod/assign/tests/fixtures/testable_assign.php b/public/mod/assign/tests/fixtures/testable_assign.php index 214b4f13d4a..9b3808b9609 100644 --- a/public/mod/assign/tests/fixtures/testable_assign.php +++ b/public/mod/assign/tests/fixtures/testable_assign.php @@ -75,7 +75,7 @@ class mod_assign_testable_assign extends assign { $data = array(); $data['id'] = $this->get_course_module()->id; $data['selectedusers'] = $selectedusers; - $data['allocatedmarker'] = $markerid; + $data['allocatedmarker1'] = $markerid; $data['action'] = 'setbatchmarkingallocation'; mod_assign_batch_set_allocatedmarker_form::mock_submit($data); diff --git a/public/mod/assign/tests/generator/behat_mod_assign_generator.php b/public/mod/assign/tests/generator/behat_mod_assign_generator.php index 8e3fd587e70..d7e0441716e 100644 --- a/public/mod/assign/tests/generator/behat_mod_assign_generator.php +++ b/public/mod/assign/tests/generator/behat_mod_assign_generator.php @@ -43,6 +43,12 @@ class behat_mod_assign_generator extends behat_generator_base { 'required' => ['assign', 'user', 'extensionduedate'], 'switchids' => ['assign' => 'cmid', 'user' => 'userid'], ], + 'marker_allocations' => [ + 'singular' => 'marker_allocation', + 'datagenerator' => 'marker_allocation', + 'required' => ['assign', 'user', 'marker'], + 'switchids' => ['assign' => 'cmid', 'user' => 'userid', 'marker' => 'markerid'], + ], ]; } @@ -55,4 +61,15 @@ class behat_mod_assign_generator extends behat_generator_base { protected function get_assign_id(string $identifier): int { return $this->get_cm_by_activity_name('assign', $identifier)->id; } + + /** + * Get the user id for the marker based on their username. + * + * @param string $username The user's username + * @return int The user's ID + */ + protected function get_marker_id(string $username): int { + return $this->get_user_id($username); + } + } diff --git a/public/mod/assign/tests/generator/lib.php b/public/mod/assign/tests/generator/lib.php index ee47c32a4c1..95a6e9ab317 100644 --- a/public/mod/assign/tests/generator/lib.php +++ b/public/mod/assign/tests/generator/lib.php @@ -59,6 +59,9 @@ class mod_assign_generator extends testing_module_generator { 'maxattempts' => 1, 'markingworkflow' => 0, 'markingallocation' => 0, + 'markercount' => 0, + 'multimarkmethod' => null, + 'multimarkrounding' => null, 'markinganonymous' => 0, 'activityformat' => 0, 'timelimit' => 0, @@ -203,4 +206,36 @@ class mod_assign_generator extends testing_module_generator { $DB->insert_record('assign_overrides', (object) $data); } + + /** + * Create a marker allocation record. + * + * @param array $data Array containing: ['assignid', 'userid', 'markerid'] + */ + public function create_marker_allocation(array $data): void { + global $DB; + + if (!isset($data['cmid'])) { + throw new coding_exception('Must specify assign when creating a marker allocation.'); + } + + if (!isset($data['userid'])) { + throw new coding_exception('Must specify user when creating a marker allocation.'); + } + + if (!isset($data['markerid'])) { + throw new coding_exception('Must specify marker when creating a marker allocation.'); + } + + [$course, $cm] = get_course_and_cm_from_cmid($data['cmid'], 'assign'); + $context = context_module::instance($cm->id); + $assign = new assign($context, $cm, $course); + + $DB->insert_record('assign_allocated_marker', [ + 'student' => $data['userid'], + 'assignment' => $assign->get_instance()->id, + 'marker' => $data['markerid'], + ]); + } + } diff --git a/public/mod/assign/tests/locallib_test.php b/public/mod/assign/tests/locallib_test.php index 731f68d6b96..dcb0adfd88a 100644 --- a/public/mod/assign/tests/locallib_test.php +++ b/public/mod/assign/tests/locallib_test.php @@ -3349,46 +3349,6 @@ You can see it appended to your users['students'] as $studentnumber => $student) { + if ($studentnumber <= 3) { + $assignment->update_allocated_markers($student->id, [$this->users['teachers'][1]->id]); + } + } + + // Allocate a mark to the first student in the group. + // This should spread out to the other students in the group as well. + $this->setUser($this->users['teachers'][1]); + + // Before we save it, we need to create the submission record, which won't happen from just saving it. + // We are passing -1 as userid because it's a required argument, but if the groupid is present, then + // the `get_group_submission` function ignores it, so it just needs any value really. + $assignment->get_group_submission(-1, $this->groups['A']->id, true); + + // Then save it. + $assignment->save_grade($this->users['students'][1]->id, (object)[ + 'mark' => 50, + 'applytoall' => 1, + 'attemptnumber' => -1, + ]); + + // All 3 students in the group should now have the same mark from this allocated marker. + foreach ($this->users['students'] as $studentnumber => $student) { + if ($studentnumber <= 3) { + $gradeobject = $assignment->get_user_grade($student->id, true); + $mark = $assignment->get_mark($gradeobject->id, $this->users['teachers'][1]->id); + $this->assertEquals(50, $mark->mark); + } + } + + // Now allocate teacher2 to 2 out of 3 students in group B. + foreach ($this->users['students'] as $studentnumber => $student) { + if ($studentnumber > 3 && $studentnumber < 6) { + $assignment->update_allocated_markers($student->id, [$this->users['teachers'][2]->id]); + } + } + + // Allocate a mark to the first student in the group. + $this->setUser($this->users['teachers'][2]); + $assignment->get_group_submission(-1, $this->groups['B']->id, true); + $assignment->save_grade($this->users['students'][4]->id, (object)[ + 'mark' => 99, + 'applytoall' => 1, + 'attemptnumber' => -1, + ]); + + // Only 2 out of 3 students should have the grade applied. + foreach ($this->users['students'] as $studentnumber => $student) { + if ($studentnumber > 3) { + $gradeobject = $assignment->get_user_grade($student->id, true); + $mark = $assignment->get_mark($gradeobject->id, $this->users['teachers'][2]->id); + if ($studentnumber < 6) { + $this->assertEquals(99, $mark->mark); + } else { + $this->assertFalse($mark); + } + } + } + } + + /** + * Create all the needed elements to test the difference between both functions. + * + * @coversNothing + */ + public function test_markerusers(): void { + $this->setup_data(); + + $oldusers = [$this->users[0], $this->users[1], $this->users[3]]; + $newusers = [$this->users[0], $this->users[1]]; list($sort, $params) = users_order_by_sql('u'); @@ -140,6 +291,329 @@ final class markerallocation_test extends \advanced_testcase { } $this->assertEquals(count($oldmarkers), 0); + } + /** + * Test functionality around having multiple allocated markers. + * + * @covers ::update_allocated_markers, ::update_mark + */ + public function test_multiple_marker_allocation(): void { + + $this->setup_data(); + $assignment = $this->create_assignment(); + + // To start with, confirm that no markers are allocated to the student submission. + $markers = $assignment->get_allocated_markers($this->users[2]->id); + $this->assertCount(0, $markers); + + // Allocate both teachers to the student assignment. + $assignment->update_allocated_markers($this->users[2]->id, [ + $this->users[0]->id, + $this->users[1]->id, + ]); + $markers = $assignment->get_allocated_markers($this->users[2]->id); + $this->assertCount(2, $markers); + + // Now test that we can add a mark to the submission. + // Firstly, there should be no mark currently for either marker. + $gradeobject = $assignment->get_user_grade($this->users[2]->id, true); + $mark = $assignment->get_mark($gradeobject->id, $this->users[0]->id); + $this->assertFalse($mark); + + // Assign a mark as teacher1. + $gradeobject->grader = $this->users[0]->id; + $assignment->update_mark($gradeobject, 99); + + // Now check that we can find the mark. + $mark = $assignment->get_mark($gradeobject->id, $this->users[0]->id); + $this->assertEquals("99.00000", $mark->mark); + + // Assign a mark as teacher2. + $gradeobject->grader = $this->users[1]->id; + $assignment->update_mark($gradeobject, 11); + + // Now check that we can find the mark. + $mark = $assignment->get_mark($gradeobject->id, $this->users[1]->id); + $this->assertEquals("11.00000", $mark->mark); + } + + /** + * Test manual calculation of final grade. + * + * @covers ::update_mark + */ + public function test_calculated_marker_grade_manual(): void { + $this->setup_data(); + $assignment = $this->create_assignment(); + + // Allocate both teachers to the student assignment. + $assignment->update_allocated_markers($this->users[2]->id, [ + $this->users[0]->id, + $this->users[1]->id, + ]); + + $gradeobject = $assignment->get_user_grade($this->users[2]->id, true); + + // Assign a mark as teacher1. + $gradeobject->grader = $this->users[0]->id; + $assignment->update_mark($gradeobject, 99); + + // Assign a mark as teacher2. + $gradeobject->grader = $this->users[1]->id; + $assignment->update_mark($gradeobject, 11); + + // With manual calculation, there should be no grade set yet. + $gradeobject = $assignment->get_user_grade($this->users[2]->id, false); + $this->assertEquals(-1, $gradeobject->grade); + } + + /** + * Test "maximum" calculation of final grade when using scale grading. + * + * @covers ::update_mark + */ + public function test_calculated_marker_grade_maximum(): void { + $this->setup_data(); + $assignment = $this->create_assignment([ + 'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_MAX, + ]); + + // Allocate both teachers to the student assignment. + $assignment->update_allocated_markers($this->users[2]->id, [ + $this->users[0]->id, + $this->users[1]->id, + ]); + + $gradeobject = $assignment->get_user_grade($this->users[2]->id, true); + + // Assign a mark as teacher1. + $gradeobject->grader = $this->users[0]->id; + $assignment->update_mark($gradeobject, 11); + + // Assign a mark as teacher2. + $gradeobject->grader = $this->users[1]->id; + $assignment->update_mark($gradeobject, 99); + + // With max calculation, the grade should be the highest one. + $gradeobject = $assignment->get_user_grade($this->users[2]->id, false); + $this->assertEquals(99, $gradeobject->grade); + } + + /** + * Test "average" calculation of final grade when using rounding of "none". + * + * @covers ::update_mark + */ + public function test_calculated_marker_grade_average_round_none(): void { + $this->setup_data(); + $assignment = $this->create_assignment([ + 'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_AVERAGE, + 'multimarkrounding' => ASSIGN_MULTIMARKING_AVERAGE_ROUND_NONE, + ]); + + // Allocate both teachers to the student assignment. + $assignment->update_allocated_markers($this->users[2]->id, [ + $this->users[0]->id, + $this->users[1]->id, + ]); + + $gradeobject = $assignment->get_user_grade($this->users[2]->id, true); + + // Assign a mark as teacher1. + $gradeobject->grader = $this->users[0]->id; + $assignment->update_mark($gradeobject, 90); + + // Assign a mark as teacher2. + $gradeobject->grader = $this->users[1]->id; + $assignment->update_mark($gradeobject, 25); + + // With avg calculation and no rounding, the grade should be 57.5. + $gradeobject = $assignment->get_user_grade($this->users[2]->id, false); + $this->assertEquals(57.5, $gradeobject->grade); + } + + /** + * Test "average" calculation of final grade when using rounding of "down". + * + * @covers ::update_mark + */ + public function test_calculated_marker_grade_average_rounding_down(): void { + $this->setup_data(); + $assignment = $this->create_assignment([ + 'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_AVERAGE, + 'multimarkrounding' => ASSIGN_MULTIMARKING_AVERAGE_ROUND_DOWN, + ]); + + // Allocate both teachers to the student assignment. + $assignment->update_allocated_markers($this->users[2]->id, [ + $this->users[0]->id, + $this->users[1]->id, + ]); + + $gradeobject = $assignment->get_user_grade($this->users[2]->id, true); + + // Assign a mark as teacher1. + $gradeobject->grader = $this->users[0]->id; + $assignment->update_mark($gradeobject, 90); + + // Assign a mark as teacher2. + $gradeobject->grader = $this->users[1]->id; + $assignment->update_mark($gradeobject, 25); + + // With avg calculation and down rounding, the grade should be 57. + $gradeobject = $assignment->get_user_grade($this->users[2]->id, false); + $this->assertEquals(57, $gradeobject->grade); + } + + /** + * Test that the grade calculation from marks using method "average" with up rounding, sets the correct grade. + * + * @covers ::update_mark + */ + public function test_calculated_marker_grade_average_round_up(): void { + $this->setup_data(); + $assignment = $this->create_assignment([ + 'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_AVERAGE, + 'multimarkrounding' => ASSIGN_MULTIMARKING_AVERAGE_ROUND_UP, + ]); + + // Allocate both teachers to the student assignment. + $assignment->update_allocated_markers($this->users[2]->id, [ + $this->users[0]->id, + $this->users[1]->id, + ]); + + $gradeobject = $assignment->get_user_grade($this->users[2]->id, true); + + // Assign a mark as teacher1. + $gradeobject->grader = $this->users[0]->id; + $assignment->update_mark($gradeobject, 90); + + // Assign a mark as teacher2. + $gradeobject->grader = $this->users[1]->id; + $assignment->update_mark($gradeobject, 25); + + // With avg calculation and up rounding, the grade should be 58. + $gradeobject = $assignment->get_user_grade($this->users[2]->id, false); + $this->assertEquals(58, $gradeobject->grade); + } + + /** + * Test that the grade calculation from marks using method "average" with natural rounding, sets the correct grade. + * + * @covers ::update_mark + */ + public function test_calculated_marker_grade_average_round_natural(): void { + $this->setup_data(); + $assignment = $this->create_assignment([ + 'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_AVERAGE, + 'multimarkrounding' => ASSIGN_MULTIMARKING_AVERAGE_ROUND_NATURAL, + ]); + + // Allocate both teachers to the student assignment. + $assignment->update_allocated_markers($this->users[2]->id, [ + $this->users[0]->id, + $this->users[1]->id, + ]); + + $gradeobject = $assignment->get_user_grade($this->users[2]->id, true); + + // Assign a mark as teacher1. + $gradeobject->grader = $this->users[0]->id; + $assignment->update_mark($gradeobject, 90); + + // Assign a mark as teacher2. + $gradeobject->grader = $this->users[1]->id; + $assignment->update_mark($gradeobject, 25); + + // With avg calculation and natural rounding, the grade should be 58. + $gradeobject = $assignment->get_user_grade($this->users[2]->id, false); + $this->assertEquals(58, $gradeobject->grade); + } + + /** + * Test that the workflow state changes on the overall grade based on marker states. + * + * @covers ::update_mark, ::calculate_and_save_overall_workflow_state + */ + public function test_calculated_marker_workflow(): void { + $this->setup_data(); + $assignment = $this->create_assignment(); + + // Allocate both teachers to the student assignment. + $assignment->update_allocated_markers($this->users[2]->id, [ + $this->users[0]->id, + $this->users[1]->id, + ]); + + // First confirm that the overall grade workflow state is not set. + $flags = $assignment->get_user_flags($this->users[2]->id, true); + $this->assertEmpty($flags->workflowstate); + + // One marker then sets their mark to be in the state "In Marking". + $gradeobject = $assignment->get_user_grade($this->users[2]->id, true); + $gradeobject->grader = $this->users[0]->id; + $assignment->update_mark($gradeobject, null, ASSIGN_MARKING_WORKFLOW_STATE_INMARKING); + $assignment->calculate_and_save_overall_workflow_state($gradeobject, $flags, $flags->workflowstate); + + // Re-check the overall workflow. This should now be "In Marking" as well. + $flags = $assignment->get_user_flags($this->users[2]->id, true); + $this->assertEquals(ASSIGN_MARKING_WORKFLOW_STATE_INMARKING, $flags->workflowstate); + + // Now this teacher marks theirs as "Marking Complete". + $gradeobject->grader = $this->users[0]->id; + $assignment->update_mark($gradeobject, 90, ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW); + $assignment->calculate_and_save_overall_workflow_state($gradeobject, $flags, $flags->workflowstate); + + // Nothing should change on the overall state, that should still be In Marking. + $flags = $assignment->get_user_flags($this->users[2]->id, true); + $this->assertEquals(ASSIGN_MARKING_WORKFLOW_STATE_INMARKING, $flags->workflowstate); + + // Now the second marker sets theirs as "Marking Complete". + $gradeobject->grader = $this->users[1]->id; + $assignment->update_mark($gradeobject, 70, ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW); + $assignment->calculate_and_save_overall_workflow_state($gradeobject, $flags, $flags->workflowstate); + + // Now that both are complete, the overall state should be the same. + $flags = $assignment->get_user_flags($this->users[2]->id, true); + $this->assertEquals(ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW, $flags->workflowstate); + } + + /** + * Test that when we remove a marker their marks are not counted towards anything. + * + * @covers ::update_mark + */ + public function test_unallocated_marker_not_included_in_mark_calculations(): void { + $this->setup_data(); + $assignment = $this->create_assignment([ + 'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_AVERAGE, + 'multimarkrounding' => ASSIGN_MULTIMARKING_AVERAGE_ROUND_NATURAL, + ]); + + // Allocate both teachers to the student assignment. + $assignment->update_allocated_markers($this->users[2]->id, [ + $this->users[0]->id, + $this->users[1]->id, + ]); + + $gradeobject = $assignment->get_user_grade($this->users[2]->id, true); + + // Assign a mark as teacher1. + $gradeobject->grader = $this->users[0]->id; + $assignment->update_mark($gradeobject, 90); + + // Now we remove teacher1 and add manager instead. So we have manager and teacher2 as the markers. + $assignment->update_allocated_markers($this->users[2]->id, [$this->users[3]->id, $this->users[1]->id]); + + // Now add a marker from teacher2. + $gradeobject->grader = $this->users[1]->id; + $assignment->update_mark($gradeobject, 10); + + // At this point, though we've had 2 marks, only 1 of the allocated markers has marked. + // So the grade should not be set. + $gradeobject = $assignment->get_user_grade($this->users[2]->id, false); + $this->assertEquals(-1, $gradeobject->grade); } } diff --git a/public/mod/assign/tests/privacy/provider_test.php b/public/mod/assign/tests/privacy/provider_test.php index 201e5bcbd9a..f2343c822e0 100644 --- a/public/mod/assign/tests/privacy/provider_test.php +++ b/public/mod/assign/tests/privacy/provider_test.php @@ -210,7 +210,10 @@ final class provider_test extends provider_testcase { 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL, 'maxattempts' => 3, 'assignsubmission_onlinetext_enabled' => true, - 'assignfeedback_comments_enabled' => true + 'assignfeedback_comments_enabled' => true, + 'markingworkflow' => 1, + 'markingallocation' => 1, + 'markercount' => 2, ]); $context = $assign->get_context(); @@ -253,6 +256,14 @@ final class provider_test extends provider_testcase { // Give the submission a grade. $assign->save_grade($user->id, $data); + // Allocate the teacher as a marker on the assignment. + $assign->update_allocated_markers($user->id, [$teacher->id]); + + // Give the submission a mark. + $gradeobject = $assign->get_user_grade($user->id, true); + $gradeobject->grader = $teacher->id; + $assign->update_mark($gradeobject, 99.9); + /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertFalse($writer->has_any_data()); @@ -284,6 +295,12 @@ final class provider_test extends provider_testcase { $overrideexport->cutoffdate); $this->assertEquals(\core_privacy\local\request\transform::datetime($overridedata->allowsubmissionsfromdate), $overrideexport->allowsubmissionsfromdate); + + // Check allocated markers and marks are exported. + $this->assertEquals($teacher->id, $writer->get_data(['Marker allocations'])->data[0]['marker']); + $this->assertEquals($user->id, $writer->get_data(['Marker allocations'])->data[0]['student']); + $this->assertEquals($teacher->id, $writer->get_data(['Marks'])->data[0]['marker']); + $this->assertEquals(99.9, $writer->get_data(['Marks'])->data[0]['mark']); } /** @@ -306,7 +323,10 @@ final class provider_test extends provider_testcase { 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL, 'maxattempts' => 3, 'assignsubmission_onlinetext_enabled' => true, - 'assignfeedback_comments_enabled' => true + 'assignfeedback_comments_enabled' => true, + 'markingworkflow' => 1, + 'markingallocation' => 1, + 'markercount' => 2, ]); $context = $assign->get_context(); @@ -366,6 +386,18 @@ final class provider_test extends provider_testcase { $flagdata->extensionduedate = $duedate; $assign->update_user_flags($flagdata); + // Allocate the teacher as a marker on the assignment for both users. + $assign->update_allocated_markers($user1->id, [$teacher->id]); + $assign->update_allocated_markers($user2->id, [$teacher->id]); + + // Give the submission a mark for both users. + $gradeobject = $assign->get_user_grade($user1->id, true); + $gradeobject->grader = $teacher->id; + $assign->update_mark($gradeobject, 11.1); + $gradeobject = $assign->get_user_grade($user2->id, true); + $gradeobject->grader = $teacher->id; + $assign->update_mark($gradeobject, 88.8); + /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertFalse($writer->has_any_data()); @@ -393,6 +425,14 @@ final class provider_test extends provider_testcase { 'Feedback comments'])->commenttext); $this->assertStringContainsString($teachercommenttext3, $writer->get_data(['studentsubmissions', $user2->id, 'attempt 2', 'Feedback comments'])->commenttext); + + // Check for marker allocations as this teacher. + $this->assertEquals($teacher->id, $writer->get_data(['Marker allocations'])->data[0]['marker']); + $this->assertEquals($teacher->id, $writer->get_data(['Marker allocations'])->data[1]['marker']); + + // And check for marks they have given. + $this->assertEquals($teacher->id, $writer->get_data(['Marks'])->data[0]['marker']); + $this->assertEquals($teacher->id, $writer->get_data(['Marks'])->data[1]['marker']); } /** @@ -415,7 +455,10 @@ final class provider_test extends provider_testcase { 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL, 'maxattempts' => 3, 'assignsubmission_onlinetext_enabled' => true, - 'assignfeedback_comments_enabled' => true + 'assignfeedback_comments_enabled' => true, + 'markingworkflow' => 1, + 'markingallocation' => 1, + 'markercount' => 2, ]); $context = $assign->get_context(); @@ -478,6 +521,18 @@ final class provider_test extends provider_testcase { // Give the submission a grade. $assign->save_grade($user2->id, $data); + // Allocate the teacher as a marker on the assignment for both users. + $assign->update_allocated_markers($user1->id, [$teacher->id]); + $assign->update_allocated_markers($user2->id, [$teacher->id]); + + // Give the submission a mark for both users. + $gradeobject = $assign->get_user_grade($user1->id, true); + $gradeobject->grader = $teacher->id; + $assign->update_mark($gradeobject, 11.1); + $gradeobject = $assign->get_user_grade($user2->id, true); + $gradeobject->grader = $teacher->id; + $assign->update_mark($gradeobject, 88.8); + // Delete all user data for this assignment. provider::delete_data_for_all_users_in_context($context); @@ -490,6 +545,10 @@ final class provider_test extends provider_testcase { $this->assertEmpty($records); $records = $DB->get_records('assignfeedback_comments'); $this->assertEmpty($records); + $records = $DB->get_records('assign_mark'); + $this->assertEmpty($records); + $records = $DB->get_records('assign_allocated_marker'); + $this->assertEmpty($records); // Check that overrides and the calendar events are deleted. $records = $DB->get_records('event'); @@ -520,7 +579,10 @@ final class provider_test extends provider_testcase { 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL, 'maxattempts' => 3, 'assignsubmission_onlinetext_enabled' => true, - 'assignfeedback_comments_enabled' => true + 'assignfeedback_comments_enabled' => true, + 'markingworkflow' => 1, + 'markingallocation' => 1, + 'markercount' => 2, ]); $context = $assign->get_context(); @@ -583,6 +645,18 @@ final class provider_test extends provider_testcase { // Give the submission a grade. $assign->save_grade($user2->id, $data); + // Allocate the teacher as a marker on the assignment for both users. + $assign->update_allocated_markers($user1->id, [$teacher->id]); + $assign->update_allocated_markers($user2->id, [$teacher->id]); + + // Give the submission a mark for both users. + $gradeobject = $assign->get_user_grade($user1->id, true); + $gradeobject->grader = $teacher->id; + $assign->update_mark($gradeobject, 11.1); + $gradeobject = $assign->get_user_grade($user2->id, true); + $gradeobject->grader = $teacher->id; + $assign->update_mark($gradeobject, 88.8); + // Delete user 2's data. $approvedlist = new approved_contextlist($user2, 'mod_assign', [$context->id, $coursecontext->id]); provider::delete_data_for_user($approvedlist); @@ -621,6 +695,12 @@ final class provider_test extends provider_testcase { $record = array_shift($records); // The remaining event should be for user 1. $this->assertEquals($user1->id, $record->userid); + + // Check marks and allocated markers only exist for user1 now. + $records = $DB->get_records('assign_allocated_marker'); + $this->assertCount(1, $records); + $records = $DB->get_records('assign_mark'); + $this->assertCount(1, $records); } /** @@ -639,7 +719,7 @@ final class provider_test extends provider_testcase { $user2 = $this->getDataGenerator()->create_user(); // User 3 made a submission. $user3 = $this->getDataGenerator()->create_user(); - // User 4 makes a submission and it is marked by the teacher. + // User 4 makes a submission and it is graded by the teacher. $user4 = $this->getDataGenerator()->create_user(); // Grading and providing feedback as a teacher. $user5 = $this->getDataGenerator()->create_user(); @@ -718,6 +798,25 @@ final class provider_test extends provider_testcase { // Give the submission a grade. $assign2->save_grade($user6->id, $data); + // Allocate the teacher as a marker on both assignments for some of the users. + $assign1->update_allocated_markers($user1->id, [$user5->id]); + $assign1->update_allocated_markers($user2->id, [$user5->id]); + $assign1->update_allocated_markers($user6->id, [$user5->id]); + $assign2->update_allocated_markers($user1->id, [$user5->id]); + $assign2->update_allocated_markers($user2->id, [$user5->id]); + $assign2->update_allocated_markers($user6->id, [$user5->id]); + + // Give the submission a mark for some users on both assignments. + $gradeobject = $assign1->get_user_grade($user3->id, true); + $gradeobject->grader = $user5->id; + $assign1->update_mark($gradeobject, 10); + $gradeobject = $assign1->get_user_grade($user4->id, true); + $gradeobject->grader = $user5->id; + $assign1->update_mark($gradeobject, 100); + $gradeobject = $assign2->get_user_grade($user6->id, true); + $gradeobject->grader = $user5->id; + $assign2->update_mark($gradeobject, 59); + // Check data is in place. $data = $DB->get_records('assign_submission'); // We should have one entry for user 3 and two entries each for user 4 and 6. @@ -735,8 +834,8 @@ final class provider_test extends provider_testcase { $this->assertEquals(2, $usercounts[$user6->id]); $data = $DB->get_records('assign_grades'); - // Two entries in assign_grades, one for each grade given. - $this->assertCount(2, $data); + // Three entries in assign_grades, for grades given and grade items created for setting marks. + $this->assertCount(3, $data); $data = $DB->get_records('assign_overrides'); $this->assertCount(1, $data); @@ -744,6 +843,12 @@ final class provider_test extends provider_testcase { $data = $DB->get_records('comments'); $this->assertCount(1, $data); + $data = $DB->get_records('assign_allocated_marker'); + $this->assertCount(6, $data); + + $data = $DB->get_records('assign_mark'); + $this->assertCount(3, $data); + $userlist = new \core_privacy\local\request\approved_userlist($context, 'assign', [$user1->id, $user2->id]); provider::delete_data_for_users($userlist); @@ -757,6 +862,14 @@ final class provider_test extends provider_testcase { // No change here. $this->assertCount(5, $data); + // Should be 4 records left - user6 on assign1, and everyone on assign2. + $data = $DB->get_records('assign_allocated_marker'); + $this->assertCount(4, $data); + + // Should be unchanged. + $data = $DB->get_records('assign_mark'); + $this->assertCount(3, $data); + $userlist = new \core_privacy\local\request\approved_userlist($context, 'assign', [$user3->id, $user5->id]); provider::delete_data_for_users($userlist); @@ -767,5 +880,13 @@ final class provider_test extends provider_testcase { $data = $DB->get_records('assign_grades'); // Grades should be unchanged. $this->assertCount(2, $data); + + // Should be 3 left, all for assign2. + $data = $DB->get_records('assign_allocated_marker'); + $this->assertCount(3, $data); + + // Should be 1 left, for assign2. + $data = $DB->get_records('assign_mark'); + $this->assertCount(1, $data); } } diff --git a/public/mod/assign/version.php b/public/mod/assign/version.php index 0dc6b54f3ca..f3667519558 100644 --- a/public/mod/assign/version.php +++ b/public/mod/assign/version.php @@ -25,5 +25,5 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics). -$plugin->version = 2026022300; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2026022301; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2025092600; // Requires this Moodle version.