MDL-80984 mod_assign: penalty implementation
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
define("mod_assign/override_delete_modal",["exports","core/custom_interaction_events","core/modal"],(function(_exports,CustomEvents,_modal){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,CustomEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CustomEvents),_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};const SELECTORS_RECACULATION_CHECKBOX="#recalculatepenalties";class OverrideDeleteModal extends _modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!1,modalConfig.removeOnClose=!0,super.configure(modalConfig)}constructor(root){super(root),this.recalculationCheckbox=this.getModal().find(SELECTORS_RECACULATION_CHECKBOX),this.setOverrideId(null),this.setSessionKey(null)}setOverrideId(id){this.overrideId=id}getOverrideId(){return this.overrideId}setSessionKey(key){this.sessionKey=key}getSessionKey(){return this.sessionKey}registerEventListeners(){super.registerEventListeners(this),this.registerCloseOnCancel(),this.getModal().on(CustomEvents.events.activate,this.getActionSelector("delete"),(()=>{this.deleteOverride()}))}deleteOverride(){const recalculate=this.recalculationCheckbox.prop("checked");window.location.href=M.cfg.wwwroot+"/mod/assign/overridedelete.php?id="+this.getOverrideId()+"&sesskey="+this.getSessionKey()+"&confirm=1"+(recalculate?"&recalculate=1":"")}}return _exports.default=OverrideDeleteModal,_defineProperty(OverrideDeleteModal,"TYPE","mod_assign/override_delete_modal"),_defineProperty(OverrideDeleteModal,"TEMPLATE","mod_assign/override_delete_modal"),OverrideDeleteModal.registerModalType(),_exports.default}));
|
||||
|
||||
//# sourceMappingURL=override_delete_modal.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"override_delete_modal.min.js","sources":["../src/override_delete_modal.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Modal for deleting an override with the option to recalculate penalties.\n *\n * @module mod_assign/override_delete_modal\n * @copyright 2025 Catalyst IT Australia Pty Ltd\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as CustomEvents from 'core/custom_interaction_events';\nimport Modal from 'core/modal';\n\nconst SELECTORS = {\n DELETE_BUTTONS: '.delete-override',\n RECACULATION_CHECKBOX: '#recalculatepenalties',\n};\n\n/**\n * Custom Modal\n */\nexport default class OverrideDeleteModal extends Modal {\n static TYPE = \"mod_assign/override_delete_modal\";\n static TEMPLATE = \"mod_assign/override_delete_modal\";\n\n /**\n * Configure the modal.\n *\n * @param {Object} modalConfig\n */\n configure(modalConfig) {\n // Add question modals are always large.\n modalConfig.large = true;\n\n // Always show on creation.\n modalConfig.show = false;\n modalConfig.removeOnClose = true;\n\n // Apply standard configuration.\n super.configure(modalConfig);\n }\n\n /**\n * Constructor.\n * Set required data to null.\n *\n * @param {HTMLElement} root\n */\n constructor(root) {\n super(root);\n\n // Recalculate penalties checkbox.\n this.recalculationCheckbox = this.getModal().find(SELECTORS.RECACULATION_CHECKBOX);\n\n // Data.\n this.setOverrideId(null);\n this.setSessionKey(null);\n }\n\n /**\n * Set the override id.\n *\n * @param {number} id The override id.\n */\n setOverrideId(id) {\n this.overrideId = id;\n }\n\n /**\n * Get the override id.\n *\n * @returns {*}\n */\n getOverrideId() {\n return this.overrideId;\n }\n\n /**\n * Set the session key.\n *\n * @param {string} key\n */\n setSessionKey(key) {\n this.sessionKey = key;\n }\n\n /**\n * Get the session key.\n *\n * @returns {*}\n */\n getSessionKey() {\n return this.sessionKey;\n }\n\n /**\n * Register events.\n *\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n // Register to close on cancel.\n this.registerCloseOnCancel();\n\n // Register the delete action.\n this.getModal().on(CustomEvents.events.activate, this.getActionSelector('delete'), () => {\n this.deleteOverride();\n });\n }\n\n /**\n * Delete a override.\n *\n */\n deleteOverride() {\n // Check if the recalculation checkbox is checked.\n const recalculate = this.recalculationCheckbox.prop('checked');\n\n // Redirect to the delete URL.\n window.location.href = M.cfg.wwwroot + '/mod/assign/overridedelete.php?id=' + this.getOverrideId() +\n '&sesskey=' + this.getSessionKey() + '&confirm=1'\n + (recalculate ? '&recalculate=1' : '');\n }\n}\n\nOverrideDeleteModal.registerModalType();\n"],"names":["SELECTORS","OverrideDeleteModal","Modal","configure","modalConfig","large","show","removeOnClose","constructor","root","recalculationCheckbox","this","getModal","find","setOverrideId","setSessionKey","id","overrideId","getOverrideId","key","sessionKey","getSessionKey","registerEventListeners","registerCloseOnCancel","on","CustomEvents","events","activate","getActionSelector","deleteOverride","recalculate","prop","window","location","href","M","cfg","wwwroot","registerModalType"],"mappings":"+3CA0BMA,gCAEqB,8BAMNC,4BAA4BC,eAS7CC,UAAUC,aAENA,YAAYC,OAAQ,EAGpBD,YAAYE,MAAO,EACnBF,YAAYG,eAAgB,QAGtBJ,UAAUC,aASpBI,YAAYC,YACFA,WAGDC,sBAAwBC,KAAKC,WAAWC,KAAKb,sCAG7Cc,cAAc,WACdC,cAAc,MAQvBD,cAAcE,SACLC,WAAaD,GAQtBE,uBACWP,KAAKM,WAQhBF,cAAcI,UACLC,WAAaD,IAQtBE,uBACWV,KAAKS,WAOhBE,+BAEUA,uBAAuBX,WAGxBY,6BAGAX,WAAWY,GAAGC,aAAaC,OAAOC,SAAUhB,KAAKiB,kBAAkB,WAAW,UAC1EC,oBAQbA,uBAEUC,YAAcnB,KAAKD,sBAAsBqB,KAAK,WAGpDC,OAAOC,SAASC,KAAOC,EAAEC,IAAIC,QAAU,qCAAuC1B,KAAKO,gBAC/E,YAAcP,KAAKU,gBAAkB,cAClCS,YAAc,iBAAmB,iEAtG3B7B,2BACH,oDADGA,+BAEC,oCAwGtBA,oBAAoBqC"}
|
||||
@@ -0,0 +1,10 @@
|
||||
define("mod_assign/override_modal_manager",["exports","core/str","mod_assign/override_delete_modal"],(function(_exports,_str,_override_delete_modal){var obj;
|
||||
/**
|
||||
* Modal manager for the override delete modal.
|
||||
*
|
||||
* @module mod_assign/override_modal_manager
|
||||
* @copyright 2025 Catalyst IT Australia Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_override_delete_modal=(obj=_override_delete_modal)&&obj.__esModule?obj:{default:obj};const SELECTORS_DELETE_BUTTONS=".delete-override",SELECTORS_PARENT_CONTAINER="#assignoverrides",SELECTORS_USER_GROUP_NAME=".usergroupname";_exports.init=(mode,showRecalculationCheckBox)=>{document.querySelector(SELECTORS_PARENT_CONTAINER).addEventListener("click",(async event=>{const button=event.target.closest(SELECTORS_DELETE_BUTTONS);if(!button)return;event.preventDefault();const name=event.target.closest("tr").querySelector(SELECTORS_USER_GROUP_NAME).innerText,confirmMessage=await getConfirmMessage(mode,name),modal=await _override_delete_modal.default.create({templateContext:{confirmmessage:confirmMessage,showpenaltyrecalculation:showRecalculationCheckBox}});modal.setOverrideId(button.getAttribute("data-overrideid")),modal.setSessionKey(button.getAttribute("data-sesskey")),modal.show()}))};const getConfirmMessage=async(mode,name)=>{switch(mode){case"group":return await(0,_str.get_string)("overridedeletegroupsure","assign",name);case"user":return await(0,_str.get_string)("overridedeleteusersure","assign",name);default:return""}}}));
|
||||
|
||||
//# sourceMappingURL=override_modal_manager.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"override_modal_manager.min.js","sources":["../src/override_modal_manager.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Modal manager for the override delete modal.\n *\n * @module mod_assign/override_modal_manager\n * @copyright 2025 Catalyst IT Australia Pty Ltd\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {get_string as getString} from 'core/str';\nimport OverrideDeleteModal from 'mod_assign/override_delete_modal';\n\nconst SELECTORS = {\n DELETE_BUTTONS: '.delete-override',\n PARENT_CONTAINER: '#assignoverrides',\n USER_GROUP_NAME: '.usergroupname',\n};\n\n/**\n * Initialise the modal manager.\n *\n * @param {string} mode The override mode.\n * @param {boolean} showRecalculationCheckBox Whether to show the recalculation checkbox.\n */\nexport const init = (mode, showRecalculationCheckBox) => {\n document.querySelector(SELECTORS.PARENT_CONTAINER).addEventListener('click', async(event) => {\n const button = event.target.closest(SELECTORS.DELETE_BUTTONS);\n\n if (!button) {\n return;\n }\n\n event.preventDefault();\n\n // Get the name of the user or group from the first column of the row.\n const name = event.target.closest('tr').querySelector(SELECTORS.USER_GROUP_NAME).innerText;\n\n // Get the confirm message for the modal.\n const confirmMessage = await getConfirmMessage(mode, name);\n\n // Create and show the modal.\n const modal = await OverrideDeleteModal.create({\n templateContext: {\n confirmmessage: confirmMessage,\n showpenaltyrecalculation: showRecalculationCheckBox,\n },\n });\n modal.setOverrideId(button.getAttribute('data-overrideid'));\n modal.setSessionKey(button.getAttribute('data-sesskey'));\n modal.show();\n });\n};\n\n/**\n * Get the confirm message for the modal.\n *\n * @param {string} mode The override mode.\n * @param {boolean} name The name of the user or group.\n * @returns {Promise<string>} The confirm message.\n */\nconst getConfirmMessage = async(mode, name) => {\n switch (mode) {\n case \"group\":\n return await getString('overridedeletegroupsure', 'assign', name);\n case \"user\":\n return await getString('overridedeleteusersure', 'assign', name);\n default:\n return \"\";\n }\n};\n"],"names":["SELECTORS","mode","showRecalculationCheckBox","document","querySelector","addEventListener","async","button","event","target","closest","preventDefault","name","innerText","confirmMessage","getConfirmMessage","modal","OverrideDeleteModal","create","templateContext","confirmmessage","showpenaltyrecalculation","setOverrideId","getAttribute","setSessionKey","show"],"mappings":";;;;;;;8KA0BMA,yBACc,mBADdA,2BAEgB,mBAFhBA,0BAGe,+BASD,CAACC,KAAMC,6BACvBC,SAASC,cAAcJ,4BAA4BK,iBAAiB,SAASC,MAAAA,cACnEC,OAASC,MAAMC,OAAOC,QAAQV,8BAE/BO,cAILC,MAAMG,uBAGAC,KAAOJ,MAAMC,OAAOC,QAAQ,MAAMN,cAAcJ,2BAA2Ba,UAG3EC,qBAAuBC,kBAAkBd,KAAMW,MAG/CI,YAAcC,+BAAoBC,OAAO,CAC3CC,gBAAiB,CACbC,eAAgBN,eAChBO,yBAA0BnB,6BAGlCc,MAAMM,cAAcf,OAAOgB,aAAa,oBACxCP,MAAMQ,cAAcjB,OAAOgB,aAAa,iBACxCP,MAAMS,iBAWRV,kBAAoBT,MAAML,KAAMW,eAC1BX,UACC,qBACY,mBAAU,0BAA2B,SAAUW,UAC3D,oBACY,mBAAU,yBAA0B,SAAUA,oBAEpD"}
|
||||
@@ -0,0 +1,141 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Modal for deleting an override with the option to recalculate penalties.
|
||||
*
|
||||
* @module mod_assign/override_delete_modal
|
||||
* @copyright 2025 Catalyst IT Australia Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import * as CustomEvents from 'core/custom_interaction_events';
|
||||
import Modal from 'core/modal';
|
||||
|
||||
const SELECTORS = {
|
||||
DELETE_BUTTONS: '.delete-override',
|
||||
RECACULATION_CHECKBOX: '#recalculatepenalties',
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom Modal
|
||||
*/
|
||||
export default class OverrideDeleteModal extends Modal {
|
||||
static TYPE = "mod_assign/override_delete_modal";
|
||||
static TEMPLATE = "mod_assign/override_delete_modal";
|
||||
|
||||
/**
|
||||
* Configure the modal.
|
||||
*
|
||||
* @param {Object} modalConfig
|
||||
*/
|
||||
configure(modalConfig) {
|
||||
// Add question modals are always large.
|
||||
modalConfig.large = true;
|
||||
|
||||
// Always show on creation.
|
||||
modalConfig.show = false;
|
||||
modalConfig.removeOnClose = true;
|
||||
|
||||
// Apply standard configuration.
|
||||
super.configure(modalConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Set required data to null.
|
||||
*
|
||||
* @param {HTMLElement} root
|
||||
*/
|
||||
constructor(root) {
|
||||
super(root);
|
||||
|
||||
// Recalculate penalties checkbox.
|
||||
this.recalculationCheckbox = this.getModal().find(SELECTORS.RECACULATION_CHECKBOX);
|
||||
|
||||
// Data.
|
||||
this.setOverrideId(null);
|
||||
this.setSessionKey(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the override id.
|
||||
*
|
||||
* @param {number} id The override id.
|
||||
*/
|
||||
setOverrideId(id) {
|
||||
this.overrideId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the override id.
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getOverrideId() {
|
||||
return this.overrideId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the session key.
|
||||
*
|
||||
* @param {string} key
|
||||
*/
|
||||
setSessionKey(key) {
|
||||
this.sessionKey = key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session key.
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getSessionKey() {
|
||||
return this.sessionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register events.
|
||||
*
|
||||
*/
|
||||
registerEventListeners() {
|
||||
// Apply parent event listeners.
|
||||
super.registerEventListeners(this);
|
||||
|
||||
// Register to close on cancel.
|
||||
this.registerCloseOnCancel();
|
||||
|
||||
// Register the delete action.
|
||||
this.getModal().on(CustomEvents.events.activate, this.getActionSelector('delete'), () => {
|
||||
this.deleteOverride();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a override.
|
||||
*
|
||||
*/
|
||||
deleteOverride() {
|
||||
// Check if the recalculation checkbox is checked.
|
||||
const recalculate = this.recalculationCheckbox.prop('checked');
|
||||
|
||||
// Redirect to the delete URL.
|
||||
window.location.href = M.cfg.wwwroot + '/mod/assign/overridedelete.php?id=' + this.getOverrideId() +
|
||||
'&sesskey=' + this.getSessionKey() + '&confirm=1'
|
||||
+ (recalculate ? '&recalculate=1' : '');
|
||||
}
|
||||
}
|
||||
|
||||
OverrideDeleteModal.registerModalType();
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Modal manager for the override delete modal.
|
||||
*
|
||||
* @module mod_assign/override_modal_manager
|
||||
* @copyright 2025 Catalyst IT Australia Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import {get_string as getString} from 'core/str';
|
||||
import OverrideDeleteModal from 'mod_assign/override_delete_modal';
|
||||
|
||||
const SELECTORS = {
|
||||
DELETE_BUTTONS: '.delete-override',
|
||||
PARENT_CONTAINER: '#assignoverrides',
|
||||
USER_GROUP_NAME: '.usergroupname',
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise the modal manager.
|
||||
*
|
||||
* @param {string} mode The override mode.
|
||||
* @param {boolean} showRecalculationCheckBox Whether to show the recalculation checkbox.
|
||||
*/
|
||||
export const init = (mode, showRecalculationCheckBox) => {
|
||||
document.querySelector(SELECTORS.PARENT_CONTAINER).addEventListener('click', async(event) => {
|
||||
const button = event.target.closest(SELECTORS.DELETE_BUTTONS);
|
||||
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// Get the name of the user or group from the first column of the row.
|
||||
const name = event.target.closest('tr').querySelector(SELECTORS.USER_GROUP_NAME).innerText;
|
||||
|
||||
// Get the confirm message for the modal.
|
||||
const confirmMessage = await getConfirmMessage(mode, name);
|
||||
|
||||
// Create and show the modal.
|
||||
const modal = await OverrideDeleteModal.create({
|
||||
templateContext: {
|
||||
confirmmessage: confirmMessage,
|
||||
showpenaltyrecalculation: showRecalculationCheckBox,
|
||||
},
|
||||
});
|
||||
modal.setOverrideId(button.getAttribute('data-overrideid'));
|
||||
modal.setSessionKey(button.getAttribute('data-sesskey'));
|
||||
modal.show();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the confirm message for the modal.
|
||||
*
|
||||
* @param {string} mode The override mode.
|
||||
* @param {boolean} name The name of the user or group.
|
||||
* @returns {Promise<string>} The confirm message.
|
||||
*/
|
||||
const getConfirmMessage = async(mode, name) => {
|
||||
switch (mode) {
|
||||
case "group":
|
||||
return await getString('overridedeletegroupsure', 'assign', name);
|
||||
case "user":
|
||||
return await getString('overridedeleteusersure', 'assign', name);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
@@ -96,7 +96,8 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
|
||||
'activity',
|
||||
'activityformat',
|
||||
'timelimit',
|
||||
'submissionattachments'));
|
||||
'submissionattachments',
|
||||
'gradepenalty'));
|
||||
|
||||
$userflags = new backup_nested_element('userflags');
|
||||
|
||||
@@ -129,6 +130,7 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
|
||||
'timemodified',
|
||||
'grader',
|
||||
'grade',
|
||||
'penalty',
|
||||
'attemptnumber'));
|
||||
|
||||
$pluginconfigs = new backup_nested_element('plugin_configs');
|
||||
|
||||
@@ -142,6 +142,11 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
|
||||
$data->grade = -($this->get_mappingid('scale', abs($data->grade)));
|
||||
}
|
||||
|
||||
// Grade penalty.
|
||||
if (!isset($data->gradepenalty)) {
|
||||
$data->gradepenalty = 0;
|
||||
}
|
||||
|
||||
$newitemid = $DB->insert_record('assign', $data);
|
||||
|
||||
$this->apply_activity_instance($newitemid);
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace mod_assign\penalty;
|
||||
|
||||
use assign;
|
||||
use context_module;
|
||||
use core_grades\penalty_manager;
|
||||
use grade_item;
|
||||
|
||||
/**
|
||||
* Helper class for penalty in assignment module.
|
||||
*
|
||||
* @package mod_assign
|
||||
* @copyright 2024 Catalyst IT Australia Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class helper {
|
||||
/**
|
||||
* Check if penalty is enabled for an assignment.
|
||||
*
|
||||
* @param int $assignid The assignment id.
|
||||
*/
|
||||
public static function is_penalty_enabled(int $assignid): bool {
|
||||
// Get the assignment course module.
|
||||
$cm = get_coursemodule_from_instance('assign', $assignid);
|
||||
$context = context_module::instance($cm->id);
|
||||
|
||||
// Get the assignment instance.
|
||||
$assign = new assign($context, $cm, $cm->course);
|
||||
|
||||
// Check if due date is set.
|
||||
if (!$assign->get_instance()->duedate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the grade type is set to GRADE_TYPE_VALUE (grade 1 to 100).
|
||||
if ($assign->get_instance()->grade < GRADE_TYPE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the assignment is set to use penalty.
|
||||
if (!$assign->get_instance()->gradepenalty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply penalty to a user.
|
||||
*
|
||||
* @param int $assignid The assignment id.
|
||||
* @param int $userid The user id.
|
||||
*/
|
||||
public static function apply_penalty_to_user(int $assignid, int $userid): void {
|
||||
global $CFG, $DB;
|
||||
|
||||
require_once($CFG->dirroot . '/mod/assign/locallib.php');
|
||||
|
||||
// Check if penalty is enabled for this assignment.
|
||||
if (!self::is_penalty_enabled($assignid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the assignment course module.
|
||||
$cm = get_coursemodule_from_instance('assign', $assignid);
|
||||
$context = context_module::instance($cm->id);
|
||||
|
||||
// Get the assignment instance.
|
||||
$assign = new assign($context, $cm, $cm->course);
|
||||
|
||||
// Find the graded attempt.
|
||||
$sql = "SELECT MAX(attemptnumber) as attemptnumber
|
||||
FROM {assign_grades}
|
||||
WHERE assignment = :assignid
|
||||
AND userid = :userid
|
||||
AND grade >= 0";
|
||||
$assigngrade = $DB->get_record_sql($sql, ['assignid' => $assignid, 'userid' => $userid]);
|
||||
|
||||
// Get the submission.
|
||||
if ($assign->get_instance()->teamsubmission) {
|
||||
$submission = $assign->get_group_submission($userid, 0, false, $assigngrade->attemptnumber);
|
||||
} else {
|
||||
$submission = $assign->get_user_submission($userid, false, $assigngrade->attemptnumber);
|
||||
}
|
||||
|
||||
// Check if the submission is null.
|
||||
if ($submission === null) {
|
||||
debugging('Submission not found for user ' . $userid . ' in assignment ' . $assignid
|
||||
. ' attempt ' . $assigngrade->attemptnumber);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get submission date.
|
||||
$submissiondate = $submission->timemodified;
|
||||
|
||||
// Check if we have valid submission date.
|
||||
if (empty($submissiondate)) {
|
||||
debugging('Invalid submission date for user ' . $userid . ' in assignment ' . $assignid
|
||||
. ' attempt ' . $assigngrade->attemptnumber);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the due date from the override if it exists. Otherwise, retrieve the date from the assignment settings.
|
||||
$duedate = $assign->override_exists($userid)->duedate ?? $assign->get_instance()->duedate;
|
||||
|
||||
// Get extension.
|
||||
$userflags = $assign->get_user_flags($userid, false);
|
||||
if (!empty($userflags)) {
|
||||
$duedate = max($userflags->extensionduedate, $duedate);
|
||||
}
|
||||
|
||||
// Get grade item.
|
||||
$gradeitem = grade_item::fetch([
|
||||
'courseid' => $assign->get_course()->id,
|
||||
'itemtype' => 'mod',
|
||||
'itemmodule' => 'assign',
|
||||
'iteminstance' => $assign->get_instance()->id,
|
||||
'itemnumber' => 0,
|
||||
]);
|
||||
|
||||
// Apply penalty.
|
||||
$container = penalty_manager::apply_grade_penalty_to_user($userid, $gradeitem, $submissiondate, $duedate);
|
||||
$deductedpercentage = $container->get_penalty() / $container->get_grade_before_penalties() * 100;
|
||||
|
||||
// Store the assign grade penalty.
|
||||
$DB->set_field_select(
|
||||
'assign_grades',
|
||||
'penalty',
|
||||
$deductedpercentage,
|
||||
'assignment = :assignid AND userid = :userid AND attemptnumber = :attemptnumber',
|
||||
['assignid' => $assignid, 'userid' => $userid, 'attemptnumber' => $assigngrade->attemptnumber]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace mod_assign;
|
||||
|
||||
use core\context;
|
||||
use mod_assign\task\recalculate_penalties;
|
||||
|
||||
/**
|
||||
* Recalculate penalties for the assignment.
|
||||
*
|
||||
* @package mod_assign
|
||||
* @copyright 2025 Catalyst IT Australia Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class penalty_recalculator extends \core_grades\penalty_recalculator {
|
||||
|
||||
/**
|
||||
* Recalculate penalties for the assignment.
|
||||
*
|
||||
* @param context $context the context where the penalty is being recalculated.
|
||||
* @param int $usermodified the user who triggered the recalculation.
|
||||
* @return void
|
||||
*/
|
||||
public static function recalculate_penalty(context $context, int $usermodified): void {
|
||||
global $CFG, $DB;
|
||||
|
||||
require_once($CFG->dirroot . '/mod/assign/locallib.php');
|
||||
|
||||
switch ($context->contextlevel) {
|
||||
case CONTEXT_MODULE:
|
||||
$cmid = $context->instanceid;
|
||||
$cm = get_coursemodule_from_id('assign', $cmid, 0, false, MUST_EXIST);
|
||||
recalculate_penalties::queue($cm->instance, $usermodified);
|
||||
break;
|
||||
case CONTEXT_COURSE:
|
||||
$courseid = $context->instanceid;
|
||||
$assigns = $DB->get_records('assign', ['course' => $courseid]);
|
||||
foreach ($assigns as $assign) {
|
||||
recalculate_penalties::queue($assign->id, $usermodified);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace mod_assign\task;
|
||||
|
||||
use core\exception\moodle_exception;
|
||||
use core\task\adhoc_task;
|
||||
|
||||
/**
|
||||
* Ad-hoc task to recalculate penalties for users in an assignment.
|
||||
*
|
||||
* @package mod_assign
|
||||
* @copyright 2024 David Woloszyn <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class recalculate_penalties extends adhoc_task {
|
||||
|
||||
/**
|
||||
* Execute the task.
|
||||
*/
|
||||
public function execute(): void {
|
||||
global $CFG, $DB;
|
||||
|
||||
require_once($CFG->dirroot . '/mod/assign/lib.php');
|
||||
require_once($CFG->dirroot . '/course/lib.php');
|
||||
|
||||
$assignid = $this->get_custom_data()->assignid;
|
||||
$assign = $DB->get_record('assign', ['id' => $assignid], '*', MUST_EXIST);
|
||||
$cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST);
|
||||
$assign->cmidnumber = $cm->idnumber;
|
||||
assign_update_grades($assign);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue the task.
|
||||
*
|
||||
* @param int $assignid assignment id
|
||||
* @param int $usermodified user who triggered the recalculation
|
||||
*/
|
||||
public static function queue(int $assignid, int $usermodified): void {
|
||||
$task = new self();
|
||||
$task->set_custom_data((object) [
|
||||
'assignid' => $assignid,
|
||||
'usermodified' => $usermodified,
|
||||
]);
|
||||
\core\task\manager::queue_adhoc_task($task);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="mod/assign/db" VERSION="20240327" COMMENT="XMLDB file for Moodle mod/assign"
|
||||
<XMLDB PATH="mod/assign/db" VERSION="20240809" COMMENT="XMLDB file for Moodle mod/assign"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
|
||||
>
|
||||
@@ -41,6 +41,7 @@
|
||||
<FIELD NAME="activityformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="timelimit" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="submissionattachments" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="gradepenalty" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If enabled, penalties will be applied."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="The unique id for this assignment instance."/>
|
||||
@@ -48,6 +49,7 @@
|
||||
<INDEXES>
|
||||
<INDEX NAME="course" UNIQUE="false" FIELDS="course" COMMENT="The course this assignment instance belongs to."/>
|
||||
<INDEX NAME="teamsubmissiongroupingid" UNIQUE="false" FIELDS="teamsubmissiongroupingid" COMMENT="The grouping id for team submissions"/>
|
||||
<INDEX NAME="gradepenalty" UNIQUE="false" FIELDS="gradepenalty"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
<TABLE NAME="assign_submission" COMMENT="This table keeps information about student interactions with the mod/assign. This is limited to metadata about a student submission but does not include the submission itself which is stored by plugins.">
|
||||
@@ -83,6 +85,7 @@
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The most recent modification time for the assignment submission by a grader."/>
|
||||
<FIELD NAME="grader" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="grade" TYPE="number" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" DECIMALS="5" COMMENT="The numerical grade for this assignment submission. Can be determined by scales/advancedgradingforms etc but will always be converted back to a floating point number."/>
|
||||
<FIELD NAME="penalty" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" COMMENT="The percentage should be deducted from final grade"/>
|
||||
<FIELD NAME="attemptnumber" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The attempt number that this grade relates to"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
|
||||
@@ -109,5 +109,37 @@ function xmldb_assign_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v4.5.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2024121801) {
|
||||
|
||||
// Define field gradepenalty to be added to assign.
|
||||
$table = new xmldb_table('assign');
|
||||
$field = new xmldb_field('gradepenalty', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'submissionattachments');
|
||||
|
||||
// Conditionally launch add field gradepenalty.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Define index gradepenalty (not unique) to be added to assign.
|
||||
$index = new xmldb_index('gradepenalty', XMLDB_INDEX_NOTUNIQUE, ['gradepenalty']);
|
||||
|
||||
// Conditionally launch add index gradepenalty.
|
||||
if (!$dbman->index_exists($table, $index)) {
|
||||
$dbman->add_index($table, $index);
|
||||
}
|
||||
|
||||
// Define field penalty to be added to assign_grades.
|
||||
$table = new xmldb_table('assign_grades');
|
||||
$field = new xmldb_field('penalty', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'grade');
|
||||
|
||||
// Conditionally launch add field penalty.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Assign savepoint reached.
|
||||
upgrade_mod_savepoint(true, 2024121801, 'assign');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -343,6 +343,7 @@ class mod_assign_external extends \mod_assign\external\external_api {
|
||||
'm.duedate, ' .
|
||||
'm.allowsubmissionsfromdate, '.
|
||||
'm.grade, ' .
|
||||
'm.gradepenalty, ' .
|
||||
'm.timemodified, '.
|
||||
'm.completionsubmit, ' .
|
||||
'm.cutoffdate, ' .
|
||||
@@ -422,6 +423,7 @@ class mod_assign_external extends \mod_assign\external\external_api {
|
||||
'duedate' => $assign->get_instance()->duedate,
|
||||
'allowsubmissionsfromdate' => $assign->get_instance()->allowsubmissionsfromdate,
|
||||
'grade' => $module->grade,
|
||||
'gradepenalty' => $module->gradepenalty,
|
||||
'timemodified' => $module->timemodified,
|
||||
'completionsubmit' => $module->completionsubmit,
|
||||
'cutoffdate' => $assign->get_instance()->cutoffdate,
|
||||
@@ -541,6 +543,7 @@ class mod_assign_external extends \mod_assign\external\external_api {
|
||||
'duedate' => new external_value(PARAM_INT, 'assignment due date'),
|
||||
'allowsubmissionsfromdate' => new external_value(PARAM_INT, 'allow submissions from date'),
|
||||
'grade' => new external_value(PARAM_INT, 'grade type'),
|
||||
'gradepenalty' => new external_value(PARAM_INT, 'if enabled, penalty will be applied to late submissions'),
|
||||
'timemodified' => new external_value(PARAM_INT, 'last time assignment was modified'),
|
||||
'completionsubmit' => new external_value(PARAM_INT, 'if enabled, set activity as complete following submission'),
|
||||
'cutoffdate' => new external_value(PARAM_INT, 'date after which submission is not accepted without an extension'),
|
||||
|
||||
@@ -753,9 +753,10 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
* @param boolean $editable
|
||||
* @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
|
||||
* @return string The formatted grade
|
||||
*/
|
||||
public function display_grade($grade, $editable, $userid, $modified) {
|
||||
public function display_grade($grade, $editable, $userid, $modified, float $deductedmark = 0) {
|
||||
if ($this->is_downloading()) {
|
||||
if ($this->assignment->get_instance()->grade >= 0) {
|
||||
if ($grade == -1 || $grade === null) {
|
||||
@@ -772,7 +773,7 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
return $scale;
|
||||
}
|
||||
}
|
||||
return $this->assignment->display_grade($grade, $editable, $userid, $modified);
|
||||
return $this->assignment->display_grade($grade, $editable, $userid, $modified, $deductedmark);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1035,7 +1036,7 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
|
||||
$grade = $this->get_gradebook_data_for_user($row->userid);
|
||||
if ($grade) {
|
||||
$o = $this->display_grade($grade->grade, false, $row->userid, $row->timemarked);
|
||||
$o = $this->display_grade($grade->grade, false, $row->userid, $row->timemarked, $grade->deductedmark);
|
||||
}
|
||||
|
||||
return $o;
|
||||
|
||||
@@ -326,6 +326,8 @@ $string['gradelocked'] = 'This grade is locked or overridden in the gradebook.';
|
||||
$string['gradeoutof'] = 'Grade out of {$a}';
|
||||
$string['gradeoutofhelp'] = 'Grade';
|
||||
$string['gradeoutofhelp_help'] = 'Enter the grade for the student\'s submission here. You may include decimals.';
|
||||
$string['gradepenalty'] = 'Grade penalties';
|
||||
$string['gradepenalty_help'] = 'If enabled, penalties will be applied to submissions';
|
||||
$string['gradestudent'] = 'Grade student: (id={$a->id}, fullname={$a->fullname}). ';
|
||||
$string['grading'] = 'Grading';
|
||||
$string['gradingchangessaved'] = 'The grade changes were saved';
|
||||
@@ -462,6 +464,7 @@ $string['overridedeletegroupsure'] = 'Are you sure you want to delete the overri
|
||||
$string['overridedeleteusersure'] = 'Are you sure you want to delete the override for user {$a}?';
|
||||
$string['overridegroup'] = 'Override group';
|
||||
$string['overridegroupeventname'] = '{$a->assign} - {$a->group}';
|
||||
$string['overriderecalculatepenalty'] = 'Recalculate penalty for user(s) in the override';
|
||||
$string['overrides'] = 'Overrides';
|
||||
$string['overrideuser'] = 'Override user';
|
||||
$string['overrideusereventname'] = '{$a->assign} - Override';
|
||||
@@ -470,6 +473,7 @@ $string['page-mod-assign-x'] = 'Any assignment module page';
|
||||
$string['page-mod-assign-view'] = 'Assignment module main and submission page';
|
||||
$string['paramtimeremaining'] = '{$a} remaining';
|
||||
$string['participant'] = 'Participant';
|
||||
$string['penaltyduedatechangemessage'] = 'Some grades have already been awarded. In order to change the due date, disable/enable penalty, you must first choose whether or not to recalculate existing grades.';
|
||||
$string['pluginadministration'] = 'Assignment administration';
|
||||
$string['pluginname'] = 'Assignment';
|
||||
$string['preventsubmissionnotingroup'] = 'Require group to make submission';
|
||||
|
||||
+34
-1
@@ -384,6 +384,8 @@ function assign_supports($feature) {
|
||||
return true;
|
||||
case FEATURE_GRADE_HAS_GRADE:
|
||||
return true;
|
||||
case FEATURE_GRADE_HAS_PENALTY:
|
||||
return true;
|
||||
case FEATURE_GRADE_OUTCOMES:
|
||||
return true;
|
||||
case FEATURE_BACKUP_MOODLE2:
|
||||
@@ -471,6 +473,12 @@ function assign_extend_settings_navigation(settings_navigation $settings, naviga
|
||||
key: 'mod_assign_submissions'
|
||||
);
|
||||
}
|
||||
|
||||
// Allow changing grade penalty settings at course module level, on assignment module.
|
||||
// Other modules can choose to allow this change or not.
|
||||
if (\mod_assign\penalty\helper::is_penalty_enabled($cm->instance)) {
|
||||
\core_grades\penalty_manager::extend_navigation_module($settings, $navref);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1063,7 +1071,7 @@ function assign_grade_item_update($assign, $grades=null) {
|
||||
$grades = null;
|
||||
}
|
||||
|
||||
return grade_update('mod/assign',
|
||||
$result = grade_update('mod/assign',
|
||||
$assign->courseid,
|
||||
'mod',
|
||||
'assign',
|
||||
@@ -1071,6 +1079,31 @@ function assign_grade_item_update($assign, $grades=null) {
|
||||
0,
|
||||
$grades,
|
||||
$params);
|
||||
|
||||
// Get lists of users whose grades are updated.
|
||||
$userids = [];
|
||||
if (is_array($grades)) {
|
||||
// The $grades is array/object of grade(s).
|
||||
// We are checking if it is single user (array with simple values such as userid and rawgrade).
|
||||
// Or it is array of grade objects, for multiple users.
|
||||
if (isset($grades['userid']) && isset($grades['rawgrade'])) {
|
||||
// Single user grade update.
|
||||
$userids = [$grades['userid']];
|
||||
} else {
|
||||
// Multiple user grade update.
|
||||
foreach ($grades as $grade) {
|
||||
if (is_object($grade) && isset($grade->userid) && isset($grade->rawgrade)) {
|
||||
$userids[] = $grade->userid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Apply penalty to each user.
|
||||
foreach ($userids as $userid) {
|
||||
\mod_assign\penalty\helper::apply_penalty_to_user($assign->id, $userid);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+73
-8
@@ -790,6 +790,10 @@ class assign {
|
||||
$update->markinganonymous = $formdata->markinganonymous;
|
||||
}
|
||||
}
|
||||
|
||||
// Grade penalties.
|
||||
$update->gradepenalty = $formdata->gradepenalty ?? 0;
|
||||
|
||||
$returnid = $DB->insert_record('assign', $update);
|
||||
$this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
|
||||
// Cache the course record.
|
||||
@@ -1579,6 +1583,9 @@ class assign {
|
||||
$update->markinganonymous = 0;
|
||||
}
|
||||
|
||||
// Grade penalties.
|
||||
$update->gradepenalty = $formdata->gradepenalty ?? 0;
|
||||
|
||||
$result = $DB->update_record('assign', $update);
|
||||
$this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
|
||||
|
||||
@@ -1611,6 +1618,13 @@ class assign {
|
||||
$update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
|
||||
$DB->update_record('assign', $update);
|
||||
|
||||
// Check if we need to recalculate penalty for existing grades.
|
||||
if (!empty($formdata->recalculatepenalty) && $formdata->recalculatepenalty === 'yes') {
|
||||
$assign = clone $this->get_instance();
|
||||
$assign->cmidnumber = $this->get_course_module()->idnumber;
|
||||
assign_update_grades($assign);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -2015,10 +2029,11 @@ class assign {
|
||||
* @param boolean $editing Are we allowing changes to this grade?
|
||||
* @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
|
||||
* @return string User-friendly representation of grade
|
||||
*/
|
||||
public function display_grade($grade, $editing, $userid=0, $modified=0) {
|
||||
global $DB;
|
||||
public function display_grade($grade, $editing, $userid = 0, $modified = 0, float $deductedmark = 0) {
|
||||
global $DB, $PAGE;
|
||||
|
||||
static $scalegrades = array();
|
||||
|
||||
@@ -2043,7 +2058,6 @@ class assign {
|
||||
maxlength="10"
|
||||
class="quickgrade"/>';
|
||||
$o .= ' / ' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals());
|
||||
return $o;
|
||||
} else {
|
||||
if ($grade == -1 || $grade === null) {
|
||||
$o .= '-';
|
||||
@@ -2055,9 +2069,24 @@ class assign {
|
||||
$o .= ' / ' . format_float($this->get_instance()->grade, $item->get_decimals());
|
||||
}
|
||||
}
|
||||
return $o;
|
||||
}
|
||||
|
||||
// Add penalty indicator, icon only.
|
||||
$penaltyindicator = '';
|
||||
if ($deductedmark > 0) {
|
||||
$gradeitem = $this->get_grade_item();
|
||||
$ispenaltyapplied = $gradeitem && $gradeitem->get_grade($userid)->is_penalty_applied_to_final_grade();
|
||||
// If the user is set, we need to check if the penalty is applied to overridden grade.
|
||||
if ($userid == 0 || $ispenaltyapplied) {
|
||||
$usergrade = new \grade_grade();
|
||||
$usergrade->deductedmark = $deductedmark;
|
||||
$indicator = new \core_grades\output\penalty_indicator(2, $usergrade);
|
||||
$renderer = $PAGE->get_renderer('core_grades');
|
||||
$penaltyindicator = $renderer->render_penalty_indicator($indicator);
|
||||
}
|
||||
}
|
||||
|
||||
return $penaltyindicator . $o;
|
||||
} else {
|
||||
// Scale.
|
||||
if (empty($this->cache['scale'])) {
|
||||
@@ -3912,6 +3941,7 @@ class assign {
|
||||
if ($attemptnumber >= 0) {
|
||||
$grade->attemptnumber = $attemptnumber;
|
||||
}
|
||||
$grade->penalty = 0.0;
|
||||
|
||||
$gid = $DB->insert_record('assign_grades', $grade);
|
||||
$grade->id = $gid;
|
||||
@@ -5399,7 +5429,7 @@ class assign {
|
||||
);
|
||||
$gradefordisplay = $gradebookgrade->str_long_grade;
|
||||
} else {
|
||||
$gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
|
||||
$gradefordisplay = $this->display_grade($gradebookgrade->grade, false, 0, 0, $gradebookgrade->deductedmark);
|
||||
}
|
||||
$gradeddate = $gradebookgrade->dategraded;
|
||||
|
||||
@@ -5606,16 +5636,18 @@ class assign {
|
||||
}
|
||||
}
|
||||
|
||||
[$penalisedgrade, $deductedmark] = $this->calculate_penalised_grade($grade);
|
||||
|
||||
// Now get the gradefordisplay.
|
||||
if ($controller) {
|
||||
$controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0);
|
||||
$grade->gradefordisplay = $controller->render_grade($PAGE,
|
||||
$grade->id,
|
||||
$gradingitem,
|
||||
$grade->grade,
|
||||
$penalisedgrade,
|
||||
$cangrade);
|
||||
} else {
|
||||
$grade->gradefordisplay = $this->display_grade($grade->grade, false);
|
||||
$grade->gradefordisplay = $this->display_grade($penalisedgrade, false, 0, 0, $deductedmark);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5623,6 +5655,28 @@ class assign {
|
||||
return $grades;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate penalised grade and deducted mark.
|
||||
*
|
||||
* @param stdClass $grade The grade object
|
||||
* @return array [$penalisedgrade, $deductedmark] the penalised grade and the deducted mark
|
||||
*/
|
||||
public function calculate_penalised_grade(stdClass $grade): array {
|
||||
$penalisedgrade = $grade->grade;
|
||||
$deductedmark = 0;
|
||||
|
||||
// No calculation needed if the grade is null or negative.
|
||||
if (is_null($penalisedgrade) || $penalisedgrade < 0) {
|
||||
return [$penalisedgrade, $deductedmark];
|
||||
}
|
||||
|
||||
if ($grade->penalty > 0) {
|
||||
$deductedmark = $grade->grade * $grade->penalty / 100;
|
||||
$penalisedgrade = $grade->grade - $deductedmark;
|
||||
}
|
||||
return [$penalisedgrade, $deductedmark];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the submissions for all previous attempts.
|
||||
*
|
||||
@@ -7697,7 +7751,7 @@ class assign {
|
||||
* @return void
|
||||
*/
|
||||
public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params) {
|
||||
global $USER, $CFG, $SESSION;
|
||||
global $USER, $CFG, $SESSION, $PAGE;
|
||||
$settings = $this->get_instance();
|
||||
|
||||
$rownum = isset($params['rownum']) ? $params['rownum'] : 0;
|
||||
@@ -7819,6 +7873,17 @@ class assign {
|
||||
$gradestring = $usergrade;
|
||||
}
|
||||
|
||||
// Penalty indicator.
|
||||
$userassigngrade = $gradinginfo->items[0]->grades[$userid];
|
||||
if (isset($userassigngrade->grade) && $userassigngrade->deductedmark > 0) {
|
||||
$gradegrade = new \grade_grade();
|
||||
$gradegrade->deductedmark = $userassigngrade->deductedmark;
|
||||
$indicator = new \core_grades\output\penalty_indicator(2, $gradegrade);
|
||||
$renderer = $PAGE->get_renderer('core_grades');
|
||||
$penaltyindicator = $renderer->render_penalty_indicator($indicator);
|
||||
$gradestring = $penaltyindicator . $gradestring;
|
||||
}
|
||||
|
||||
if ($this->get_instance()->markingworkflow) {
|
||||
$states = $this->get_marking_workflow_states_for_current_user();
|
||||
$options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
|
||||
|
||||
+59
-1
@@ -42,7 +42,7 @@ class mod_assign_mod_form extends moodleform_mod {
|
||||
* @return void
|
||||
*/
|
||||
public function definition() {
|
||||
global $CFG, $COURSE;
|
||||
global $CFG, $COURSE, $OUTPUT;;
|
||||
$mform = $this->_form;
|
||||
|
||||
$mform->addElement('header', 'general', get_string('general', 'form'));
|
||||
@@ -83,9 +83,26 @@ class mod_assign_mod_form extends moodleform_mod {
|
||||
$mform->addElement('date_time_selector', 'allowsubmissionsfromdate', $name, $options);
|
||||
$mform->addHelpButton('allowsubmissionsfromdate', 'allowsubmissionsfromdate', 'assign');
|
||||
|
||||
// Add the option to recalculate the penalty if there is existing grade.
|
||||
$penaltysettingmessage = '';
|
||||
if ($assignment->has_instance()
|
||||
&& \mod_assign\penalty\helper::is_penalty_enabled($assignment->get_instance()->id)
|
||||
&& $assignment->count_grades() > 0) {
|
||||
// Create notification.
|
||||
$penaltysettingmessage = $OUTPUT->notification(get_string('penaltyduedatechangemessage', 'assign'), 'warning', false);
|
||||
$mform->addElement('html', $penaltysettingmessage);
|
||||
$mform->addElement('select', 'recalculatepenalty', get_string('modgraderecalculatepenalty', 'grades'), [
|
||||
'' => get_string('choose'),
|
||||
'no' => get_string('no'),
|
||||
'yes' => get_string('yes'),
|
||||
]);
|
||||
$mform->addHelpButton('recalculatepenalty', 'modgraderecalculatepenalty', 'grades');
|
||||
}
|
||||
|
||||
$name = get_string('duedate', 'assign');
|
||||
$mform->addElement('date_time_selector', 'duedate', $name, array('optional'=>true));
|
||||
$mform->addHelpButton('duedate', 'duedate', 'assign');
|
||||
$mform->disabledIf('duedate', 'recalculatepenalty', 'eq', '');
|
||||
|
||||
$name = get_string('cutoffdate', 'assign');
|
||||
$mform->addElement('date_time_selector', 'cutoffdate', $name, array('optional'=>true));
|
||||
@@ -232,12 +249,53 @@ class mod_assign_mod_form extends moodleform_mod {
|
||||
$mform->hideIf('markinganonymous', 'markingworkflow', 'eq', 0);
|
||||
$mform->hideIf('markinganonymous', 'blindmarking', 'eq', 0);
|
||||
|
||||
// Add Penalty settings if the module supports it.
|
||||
if (\core_grades\penalty_manager::is_penalty_enabled_for_module('assign')) {
|
||||
// Show the message if we need to change the penalty settings.
|
||||
if (!empty($penaltysettingmessage)) {
|
||||
$mform->addElement('html', $penaltysettingmessage);
|
||||
}
|
||||
|
||||
// Enable or disable the penalty settings.
|
||||
$mform->addElement('selectyesno', 'gradepenalty', get_string('gradepenalty', 'mod_assign'));
|
||||
$mform->addHelpButton('gradepenalty', 'gradepenalty', 'mod_assign');
|
||||
$mform->setDefault('gradepenalty', 0);
|
||||
|
||||
// Hide if the due date is not enabled.
|
||||
$mform->hideIf('gradepenalty', 'duedate[enabled]');
|
||||
|
||||
// Hide if the grade type is not set to point.
|
||||
$mform->hideIf('gradepenalty', 'grade[modgrade_type]', 'neq', 'point');
|
||||
|
||||
// Disable if the recalculate penalty is not set.
|
||||
$mform->disabledIf('gradepenalty', 'recalculatepenalty', 'eq', '');
|
||||
}
|
||||
|
||||
$this->standard_coursemodule_elements();
|
||||
$this->apply_admin_defaults();
|
||||
|
||||
$this->add_action_buttons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override definition after data has been set.
|
||||
*
|
||||
* The value of date time selector will be lost in a POST request, if the selector is disabled.
|
||||
* So, we need to set the value again.
|
||||
*
|
||||
* return void
|
||||
*/
|
||||
public function definition_after_data() {
|
||||
parent::definition_after_data();
|
||||
$mform = $this->_form;
|
||||
|
||||
// The value of date time selector will be lost in a POST request.
|
||||
$recalculatepenalty = optional_param('recalculatepenalty', null, PARAM_TEXT);
|
||||
if ($recalculatepenalty === '') {
|
||||
$mform->setConstant('duedate', $mform->_defaultValues['duedate']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform minimal validation on the settings form
|
||||
* @param array $data
|
||||
|
||||
@@ -251,8 +251,22 @@ class assign_override_form extends moodleform {
|
||||
get_string('allowsubmissionsfromdate', 'assign'), array('optional' => true));
|
||||
$mform->setDefault('allowsubmissionsfromdate', $assigninstance->allowsubmissionsfromdate);
|
||||
|
||||
// Add the option to recalculate the penalty if there is existing grade.
|
||||
if (\mod_assign\penalty\helper::is_penalty_enabled($assigninstance->id) && $this->assign->count_grades() > 0) {
|
||||
// Create notification.
|
||||
$notice = $OUTPUT->notification(get_string('penaltyduedatechangemessage', 'assign'), 'warning', false);
|
||||
$mform->addElement('html', $notice);
|
||||
$mform->addElement('select', 'recalculatepenalty', get_string('modgraderecalculatepenalty', 'grades'), [
|
||||
'' => get_string('choose'),
|
||||
'no' => get_string('no'),
|
||||
'yes' => get_string('yes'),
|
||||
]);
|
||||
$mform->addHelpButton('recalculatepenalty', 'modgraderecalculatepenalty', 'grades');
|
||||
}
|
||||
|
||||
$mform->addElement('date_time_selector', 'duedate', get_string('duedate', 'assign'), array('optional' => true));
|
||||
$mform->setDefault('duedate', $assigninstance->duedate);
|
||||
$mform->disabledIf('duedate', 'recalculatepenalty', 'eq', '');
|
||||
|
||||
$mform->addElement('date_time_selector', 'cutoffdate', get_string('cutoffdate', 'assign'), array('optional' => true));
|
||||
$mform->setDefault('cutoffdate', $assigninstance->cutoffdate);
|
||||
@@ -286,6 +300,24 @@ class assign_override_form extends moodleform {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Override definition after data has been set.
|
||||
*
|
||||
* The value of date time selector will be lost in a POST request, if the selector is disabled.
|
||||
* So, we need to set the value again.
|
||||
*
|
||||
* return void
|
||||
*/
|
||||
public function definition_after_data() {
|
||||
$mform = $this->_form;
|
||||
|
||||
// The value of date time selector will be lost in a POST request.
|
||||
$recalculatepenalty = optional_param('recalculatepenalty', null, PARAM_TEXT);
|
||||
if ($recalculatepenalty === '') {
|
||||
$mform->setConstant('duedate', $mform->_defaultValues['duedate']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the submitted form data.
|
||||
*
|
||||
|
||||
@@ -30,6 +30,7 @@ require_once($CFG->dirroot.'/mod/assign/override_form.php');
|
||||
|
||||
$overrideid = required_param('id', PARAM_INT);
|
||||
$confirm = optional_param('confirm', false, PARAM_BOOL);
|
||||
$recalculate = optional_param('recalculate', false, PARAM_BOOL);
|
||||
|
||||
if (! $override = $DB->get_record('assign_overrides', array('id' => $overrideid))) {
|
||||
throw new \moodle_exception('invalidoverrideid', 'assign');
|
||||
@@ -66,6 +67,20 @@ if (!empty($override->userid)) {
|
||||
if ($confirm) {
|
||||
require_sesskey();
|
||||
|
||||
if ($recalculate) {
|
||||
$assignintance = clone $assign->get_instance();
|
||||
$assignintance->cmidnumber = $assign->get_course_module()->idnumber;
|
||||
if (!$override->groupid) {
|
||||
assign_update_grades($assignintance, $override->userid);
|
||||
} else {
|
||||
// If it is group mode.
|
||||
$groupmembers = groups_get_members($override->groupid);
|
||||
foreach ($groupmembers as $groupmember) {
|
||||
assign_update_grades($assignintance, $groupmember->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$assign->delete_override($override->id);
|
||||
|
||||
reorder_group_overrides($assign->get_instance()->id);
|
||||
|
||||
@@ -238,6 +238,22 @@ if ($mform->is_cancelled()) {
|
||||
$event->trigger();
|
||||
}
|
||||
|
||||
// Check if we need to recalculate penalty for existing grades.
|
||||
if (!empty($fromform->recalculatepenalty) && $fromform->recalculatepenalty === 'yes') {
|
||||
$assignintance = clone $assign->get_instance();
|
||||
$assignintance->cmidnumber = $assign->get_course_module()->idnumber;
|
||||
// If it is user mode.
|
||||
if (!$groupmode) {
|
||||
assign_update_grades($assignintance, $fromform->userid);
|
||||
} else {
|
||||
// If it is group mode.
|
||||
$groupmembers = groups_get_members($fromform->groupid);
|
||||
foreach ($groupmembers as $groupmember) {
|
||||
assign_update_grades($assignintance, $groupmember->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assign_update_events($assign, $fromform);
|
||||
|
||||
if (!empty($fromform->submitbutton)) {
|
||||
|
||||
@@ -174,6 +174,8 @@ $overrideediturl = new moodle_url('/mod/assign/overrideedit.php');
|
||||
|
||||
$hasinactive = false; // Whether there are any inactive overrides.
|
||||
|
||||
$PAGE->requires->js_call_amd('mod_assign/override_modal_manager', 'init', [$mode, !empty($assign->gradepenalty)]);
|
||||
|
||||
foreach ($overrides as $override) {
|
||||
|
||||
$fields = array();
|
||||
@@ -229,15 +231,24 @@ foreach ($overrides as $override) {
|
||||
array('id' => $override->id, 'action' => 'duplicate'));
|
||||
$iconstr .= '<a title="' . get_string('copy') . '" href="' . $copyurlstr . '">' .
|
||||
$OUTPUT->pix_icon('t/copy', get_string('copy')) . '</a> ';
|
||||
|
||||
// Delete.
|
||||
$deleteurlstr = $overridedeleteurl->out(true,
|
||||
array('id' => $override->id, 'sesskey' => sesskey()));
|
||||
$iconstr .= '<a title="' . get_string('delete') . '" href="' . $deleteurlstr . '">' .
|
||||
$OUTPUT->pix_icon('t/delete', get_string('delete')) . '</a> ';
|
||||
$deletelink = html_writer::link("#",
|
||||
$OUTPUT->pix_icon('t/delete', get_string('delete')),
|
||||
[
|
||||
'class' => 'delete-override',
|
||||
'data-overrideid' => $override->id,
|
||||
'data-sesskey' => sesskey(),
|
||||
]
|
||||
);
|
||||
$iconstr .= $deletelink;
|
||||
|
||||
if ($groupmode) {
|
||||
$usergroupstr = '<a href="' . $groupurl->out(true, ['group' => $override->groupid]) . '" >' .
|
||||
format_string($override->name, true, ['context' => $context]) . '</a>';
|
||||
$usergroupstr = html_writer::link(
|
||||
$groupurl->out(true, ['group' => $override->groupid]),
|
||||
format_string($override->name, true, ['context' => $context]),
|
||||
['class' => 'usergroupname']
|
||||
);
|
||||
|
||||
// Move up.
|
||||
if ($override->sortorder > 1) {
|
||||
@@ -259,9 +270,11 @@ foreach ($overrides as $override) {
|
||||
|
||||
|
||||
} else {
|
||||
$usergroupstr = html_writer::link($userurl->out(false,
|
||||
array('id' => $override->userid, 'course' => $course->id)),
|
||||
fullname($override));
|
||||
$usergroupstr = html_writer::link(
|
||||
$userurl->out(false, ['id' => $override->userid, 'course' => $course->id]),
|
||||
fullname($override),
|
||||
['class' => 'usergroupname']
|
||||
);
|
||||
}
|
||||
|
||||
$class = '';
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
{{!
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template mod_assign/override_delete_modal
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
}
|
||||
|
||||
}}
|
||||
{{< core/modal }}
|
||||
{{$title}}{{#str}} confirm {{/str}}{{/title}}
|
||||
{{$body}}
|
||||
<div class="container">
|
||||
<form>
|
||||
<div class="form-group row">
|
||||
<p>{{confirmmessage}}</p>
|
||||
</div>
|
||||
|
||||
{{#showpenaltyrecalculation}}
|
||||
<div class="form-group row">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="recalculatepenalties" name="recalculatepenalties"/>
|
||||
|
||||
<label class="form-check-label" for="recalculatepenalties">
|
||||
{{#str}} overriderecalculatepenalty, assign {{/str}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{/showpenaltyrecalculation}}
|
||||
</form>
|
||||
</div>
|
||||
{{/body}}
|
||||
{{$footer}}
|
||||
<button type="button" class="btn btn-secondary" data-action="cancel">{{#str}} cancel {{/str}}</button>
|
||||
<button type="button" class="btn btn-danger" data-action="delete">{{#str}} continue {{/str}}</button>
|
||||
{{/footer}}
|
||||
{{/ core/modal }}
|
||||
@@ -126,6 +126,7 @@ final class restore_date_test extends \restore_date_testcase {
|
||||
],
|
||||
'timelimit' => DAYSECS,
|
||||
'submissionattachments' => 1,
|
||||
'gradepenalty' => 1,
|
||||
];
|
||||
$assign = $this->getDataGenerator()->create_module('assign', $record);
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ Feature: Assign group override
|
||||
| activity | name | intro | course | assignsubmission_onlinetext_enabled |
|
||||
| assign | Test assignment name | Submit your online text | C1 | 1 |
|
||||
|
||||
@javascript
|
||||
Scenario: Add, modify then delete a group override
|
||||
Given I am on the "Test assignment name" Activity page logged in as teacher1
|
||||
When I navigate to "Overrides" in current page administration
|
||||
|
||||
@@ -71,4 +71,19 @@ class behat_mod_assign extends behat_base {
|
||||
$criteriacheck++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable grade penalty.
|
||||
*
|
||||
* @Given I enable grade penalties for assignment
|
||||
*/
|
||||
public function i_enable_grade_penalties_for_assignment(): void {
|
||||
global $DB;
|
||||
|
||||
\core_grades\penalty_manager::enable_module('assign');
|
||||
\core\plugininfo\gradepenalty::enable_plugin('duedate', true);
|
||||
|
||||
$rule = ['contextid' => 1, 'overdueby' => DAYSECS, 'penalty' => 10, 'sortorder' => 0];
|
||||
$DB->insert_record('gradepenalty_duedate_rule', (object) $rule);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@mod @mod_assign
|
||||
@mod @mod_assign @assign_grade
|
||||
Feature: Check that the assignment grade can be updated correctly
|
||||
In order to ensure that the grade is shown correctly in the grading table
|
||||
As a teacher
|
||||
@@ -70,3 +70,45 @@ Feature: Check that the assignment grade can be updated correctly
|
||||
And I press "Save changes"
|
||||
And I follow "View all submissions"
|
||||
Then "Student 1" row "Grade" column of "generaltable" table should contain "50.00"
|
||||
|
||||
@javascript
|
||||
Scenario: Update the grade for an assignment with penalty
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category | groupmode |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| student1 | Student | 1 | student10@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "groups" exist:
|
||||
| name | course | idnumber |
|
||||
| Group 1 | C1 | G1 |
|
||||
And I enable grade penalties for assignment
|
||||
And the following "activity" exists:
|
||||
| activity | assign |
|
||||
| course | C1 |
|
||||
| name | Test assignment name |
|
||||
| intro | Test assignment description |
|
||||
| grade | 100 |
|
||||
| duedate | ##yesterday## |
|
||||
| gradepenalty | 1 |
|
||||
| assignsubmission_onlinetext_enabled | 1 |
|
||||
| submissiondrafts | 0 |
|
||||
# Add a submission.
|
||||
And the following "mod_assign > submissions" exist:
|
||||
| assign | user | onlinetext |
|
||||
| Test assignment name | student1 | I'm the student first submission |
|
||||
And I am on the "Test assignment name" Activity page logged in as teacher1
|
||||
When I change window size to "large"
|
||||
And I go to "Student 1" "Test assignment name" activity advanced grading page
|
||||
And I set the field "Grade out of 100" to "100"
|
||||
And I set the field "Notify student" to "0"
|
||||
And I press "Save changes"
|
||||
And I follow "View all submissions"
|
||||
And "Student 1" row "Grade" column of "generaltable" table should contain "100.00"
|
||||
And "Student 1" row "Final grade" column of "generaltable" table should contain "90.00"
|
||||
And the "title" attribute of ".penalty-indicator-icon" "css_element" should contain "Late penalty applied -10.00 marks"
|
||||
|
||||
@@ -189,6 +189,7 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
|
||||
'markingallocation' => 1,
|
||||
'blindmarking' => 1,
|
||||
'markinganonymous' => 1,
|
||||
'gradepenalty' => 1,
|
||||
'activityeditor' => [
|
||||
'text' => 'Test activity',
|
||||
'format' => 1,
|
||||
@@ -253,6 +254,7 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
|
||||
$this->assertEquals(1, $assignment['markingallocation']);
|
||||
$this->assertEquals(1, $assignment['blindmarking']);
|
||||
$this->assertEquals(1, $assignment['markinganonymous']);
|
||||
$this->assertEquals(1, $assignment['gradepenalty']);
|
||||
$this->assertEquals(0, $assignment['preventsubmissionnotingroup']);
|
||||
$this->assertEquals(0, $assignment['timelimit']);
|
||||
$this->assertEquals(0, $assignment['submissionattachments']);
|
||||
@@ -286,6 +288,7 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
|
||||
$this->assertEquals(1, $assignment['markingallocation']);
|
||||
$this->assertEquals(1, $assignment['blindmarking']);
|
||||
$this->assertEquals(1, $assignment['markinganonymous']);
|
||||
$this->assertEquals(1, $assignment['gradepenalty']);
|
||||
$this->assertEquals(0, $assignment['preventsubmissionnotingroup']);
|
||||
|
||||
$result = mod_assign_external::get_assignments(array($course2->id));
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace gradepenalty_fake_deduction;
|
||||
|
||||
use core_grades\penalty_container;
|
||||
|
||||
/**
|
||||
* Penalty plugins must override this class to implement their own penalty calculation.
|
||||
*
|
||||
* @package mod_assign
|
||||
* @copyright 2024 Catalyst IT Australia Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class penalty_calculator extends \core_grades\penalty_calculator {
|
||||
/**
|
||||
* Calculate the penalty for the given activity.
|
||||
*
|
||||
* @param penalty_container $container The penalty container.
|
||||
*/
|
||||
public static function calculate_penalty(penalty_container $container): void {
|
||||
|
||||
// Dates.
|
||||
debugging('Submission date: ' . $container->get_submission_date());
|
||||
debugging('Due date: ' . $container->get_due_date());
|
||||
|
||||
// Calculate the deducted grade based on the max grade.
|
||||
if ($container->get_submission_date() > $container->get_due_date()) {
|
||||
$deductedgrade = $container->get_max_grade() * 10 / 100;
|
||||
$container->aggregate_penalty($deductedgrade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Fake component for testing
|
||||
*
|
||||
* @package mod_assign
|
||||
* @copyright 2024 Catalyst IT Australia Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2022050200;
|
||||
$plugin->requires = 2022041200;
|
||||
$plugin->component = 'gradepenalty_fake_deduction';
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace mod_assign;
|
||||
|
||||
use core_component;
|
||||
use core_grades\penalty_manager;
|
||||
use grade_item;
|
||||
use mod_assign_test_generator;
|
||||
use mod_assign_testable_assign;
|
||||
use ReflectionClass;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/mod/assign/locallib.php');
|
||||
require_once($CFG->dirroot . '/mod/assign/tests/generator.php');
|
||||
|
||||
/**
|
||||
* Penalty test.
|
||||
*
|
||||
* @package mod_assign
|
||||
* @copyright 2024 Catalyst IT Australia Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class penalty_test extends \advanced_testcase {
|
||||
// Use the generator helper.
|
||||
use mod_assign_test_generator;
|
||||
|
||||
/**
|
||||
* Set up test
|
||||
*
|
||||
* @return array The course and student.
|
||||
*/
|
||||
protected function set_up_test(): array {
|
||||
global $CFG;
|
||||
$this->setAdminUser();
|
||||
|
||||
penalty_manager::enable_module('assign');
|
||||
|
||||
// Load a mocked grade penalty plugin.
|
||||
$mockedcomponent = new ReflectionClass(core_component::class);
|
||||
$mockedplugins = $mockedcomponent->getProperty('plugins');
|
||||
$plugins = $mockedplugins->getValue();
|
||||
$plugins['gradepenalty'] = ["fake_deduction" => "{$CFG->dirroot}/mod/assign/tests/fixtures/fakeplugins/fake_deduction"];
|
||||
// Load the penalty_calculator class.
|
||||
require_once($CFG->dirroot . '/mod/assign/tests/fixtures/fakeplugins/fake_deduction/classes/penalty_calculator.php');
|
||||
$mockedplugins->setValue(null, $plugins);
|
||||
|
||||
\core\plugininfo\gradepenalty::enable_plugin('fake_deduction', true);
|
||||
|
||||
// Create a course with user.
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$student = $this->getDataGenerator()->create_and_enrol($course);
|
||||
|
||||
return [$course, $student];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test penalty support.
|
||||
*
|
||||
* @covers ::assign_supports
|
||||
*/
|
||||
public function test_penalty_support(): void {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Assign should be in the supported list.
|
||||
$this->assertTrue(in_array('assign', penalty_manager::get_supported_modules()));
|
||||
|
||||
// Penalty is not enabled for any modules by default.
|
||||
$this->assertFalse(penalty_manager::is_penalty_enabled_for_module('assign'));
|
||||
|
||||
// Enable penalty for assign.
|
||||
penalty_manager::enable_module('assign');
|
||||
|
||||
// Assign should be enabled by now.
|
||||
$this->assertTrue(penalty_manager::is_penalty_enabled_for_module('assign'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_hook_callback.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function apply_penalty_provider(): array {
|
||||
return [
|
||||
// Submission date, Due date, User override, Group override, Extension due date, Expected messages, Expected grade.
|
||||
// No overrides.
|
||||
[DAYSECS, DAYSECS, null, null, null, ['Submission date: 86400', 'Due date: 86400'], 50],
|
||||
[DAYSECS + 1, DAYSECS, null, null, null, ['Submission date: 86401', 'Due date: 86400'], 30],
|
||||
// User override.
|
||||
[DAYSECS + 1, DAYSECS, DAYSECS + 1, null, null, ['Submission date: 86401', 'Due date: 86401'], 50],
|
||||
[DAYSECS + 2, DAYSECS, DAYSECS + 1, null, null, ['Submission date: 86402', 'Due date: 86401'], 30],
|
||||
// Group override.
|
||||
[DAYSECS + 1, DAYSECS, null, DAYSECS + 1, null, ['Submission date: 86401', 'Due date: 86401'], 50],
|
||||
[DAYSECS + 2, DAYSECS, null, DAYSECS + 1, null, ['Submission date: 86402', 'Due date: 86401'], 30],
|
||||
// User and group override.
|
||||
[DAYSECS + 1, DAYSECS, DAYSECS + 1, DAYSECS + 2, null, ['Submission date: 86401', 'Due date: 86401'], 50],
|
||||
[DAYSECS + 2, DAYSECS, DAYSECS + 1, DAYSECS + 2, null, ['Submission date: 86402', 'Due date: 86401'], 30],
|
||||
// User, group override and extension.
|
||||
[DAYSECS + 3, DAYSECS, DAYSECS + 1, DAYSECS + 2, DAYSECS + 3, ['Submission date: 86403', 'Due date: 86403'], 50],
|
||||
[DAYSECS + 4, DAYSECS, DAYSECS + 1, DAYSECS + 2, DAYSECS + 3, ['Submission date: 86404', 'Due date: 86403'], 30],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for hook_listener class.
|
||||
*
|
||||
* @dataProvider apply_penalty_provider
|
||||
*
|
||||
* @covers \mod_assign\penalty\helper::apply_penalty_to_submission
|
||||
*
|
||||
* @param int $submissiondate The submission date.
|
||||
* @param int $duedate The due date.
|
||||
* @param int $useroverrideduedate The user override due date.
|
||||
* @param int $groupoverrideduedate The group override due date.
|
||||
* @param int $extensionduedate The extension due date.
|
||||
* @param array $expectedmessages The expected debug messages.
|
||||
* @param float $expectedgrade The expected final grade.
|
||||
*
|
||||
*/
|
||||
public function test_apply_penalty($submissiondate, $duedate,
|
||||
$useroverrideduedate, $groupoverrideduedate, $extensionduedate,
|
||||
$expectedmessages, $expectedgrade): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
[$course, $student] = $this->set_up_test();
|
||||
|
||||
// Assignment.
|
||||
$generator = $this->getDataGenerator();
|
||||
$assignmentgenerator = $generator->get_plugin_generator('mod_assign');
|
||||
$instance = $assignmentgenerator->create_instance([
|
||||
'course' => $course->id,
|
||||
'duedate' => $duedate,
|
||||
'assignsubmission_onlinetext_enabled' => 1,
|
||||
'gradepenalty' => 1,
|
||||
'grade' => 200,
|
||||
]);
|
||||
$cm = get_coursemodule_from_instance('assign', $instance->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
$assign = new mod_assign_testable_assign($context, $cm, $course);
|
||||
|
||||
// If there is user override.
|
||||
if ($useroverrideduedate) {
|
||||
$assignmentgenerator->create_override([
|
||||
'assignid' => $instance->id,
|
||||
'userid' => $student->id,
|
||||
'duedate' => $useroverrideduedate,
|
||||
]);
|
||||
}
|
||||
|
||||
// If there is extension.
|
||||
if ($extensionduedate) {
|
||||
$flags = $assign->get_user_flags($student->id, true);
|
||||
$flags->extensionduedate = $extensionduedate;
|
||||
$assign->update_user_flags($flags);
|
||||
}
|
||||
|
||||
// If there is group override.
|
||||
if ($groupoverrideduedate) {
|
||||
$group = $generator->create_group(['courseid' => $course->id]);
|
||||
$generator->create_group_member(['groupid' => $group->id, 'userid' => $student->id]);
|
||||
$assignmentgenerator->create_override([
|
||||
'assignid' => $instance->id,
|
||||
'groupid' => $group->id,
|
||||
'duedate' => $groupoverrideduedate,
|
||||
]);
|
||||
}
|
||||
|
||||
// Add submission and grade.
|
||||
$this->add_submission($student, $assign, 'Sample text');
|
||||
$this->submit_for_grading($student, $assign);
|
||||
// Submission date.
|
||||
$DB->set_field('assign_submission', 'timemodified', $submissiondate, ['userid' => $student->id]);
|
||||
$assign->testable_apply_grade_to_user((object)['grade' => 50.0], $student->id, 0);
|
||||
|
||||
// Expect debug messages.
|
||||
$this->assertdebuggingcalledcount(count($expectedmessages), $expectedmessages);
|
||||
|
||||
// The expected final grade.
|
||||
$gradeitem = grade_item::fetch(
|
||||
[
|
||||
'courseid' => $course->id,
|
||||
'itemtype' => 'mod',
|
||||
'itemmodule' => 'assign',
|
||||
'iteminstance' => $instance->id,
|
||||
'itemnumber' => 0,
|
||||
]
|
||||
);
|
||||
$this->assertEquals($expectedgrade, $gradeitem->get_final($student->id)->finalgrade);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test recalculation.
|
||||
*
|
||||
* @covers \mod_assign\penalty\helper::apply_penalty_to_submission
|
||||
*
|
||||
*/
|
||||
public function test_recalculate_penalty(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
[$course, $student] = $this->set_up_test();
|
||||
|
||||
// Assignment.
|
||||
$duedate = time() + DAYSECS;
|
||||
$generator = $this->getDataGenerator();
|
||||
$assignmentgenerator = $generator->get_plugin_generator('mod_assign');
|
||||
$instance = $assignmentgenerator->create_instance([
|
||||
'course' => $course->id,
|
||||
'duedate' => $duedate,
|
||||
'assignsubmission_onlinetext_enabled' => 1,
|
||||
'gradepenalty' => 1,
|
||||
'grade' => 200,
|
||||
]);
|
||||
$cm = get_coursemodule_from_instance('assign', $instance->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
$assign = new mod_assign_testable_assign($context, $cm, $course);
|
||||
|
||||
// Add submission and grade.
|
||||
$submissiondate = $duedate + HOURSECS;
|
||||
$this->add_submission($student, $assign, 'Sample text');
|
||||
$this->submit_for_grading($student, $assign);
|
||||
// Submission date.
|
||||
$DB->set_field('assign_submission', 'timemodified', $submissiondate, ['userid' => $student->id]);
|
||||
$assign->testable_apply_grade_to_user((object)['grade' => 50.0], $student->id, 0);
|
||||
|
||||
$this->assertdebuggingcalledcount(2);
|
||||
|
||||
// Check the grade.
|
||||
$gradeitem = grade_item::fetch(
|
||||
[
|
||||
'courseid' => $course->id,
|
||||
'itemtype' => 'mod',
|
||||
'itemmodule' => 'assign',
|
||||
'iteminstance' => $instance->id,
|
||||
'itemnumber' => 0,
|
||||
]
|
||||
);
|
||||
$this->assertEquals(30, $gradeitem->get_final($student->id)->finalgrade);
|
||||
|
||||
// Change the due date.
|
||||
$duedate = time() + DAYSECS * 2;
|
||||
$DB->set_field('assign', 'duedate', $duedate, ['id' => $instance->id]);
|
||||
|
||||
// Recalculate the penalty.
|
||||
$clonedassign = clone $assign->get_instance();
|
||||
$clonedassign->cmidnumber = $assign->get_course_module()->idnumber;
|
||||
assign_update_grades($clonedassign);
|
||||
$this->assertdebuggingcalledcount(2);
|
||||
|
||||
// Check the grade.
|
||||
$this->assertEquals(50, $gradeitem->get_final($student->id)->finalgrade);
|
||||
}
|
||||
}
|
||||
@@ -25,5 +25,5 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
|
||||
$plugin->version = 2024121800; // The current module version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2024121801; // The current module version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2024100100; // Requires this Moodle version.
|
||||
|
||||
Reference in New Issue
Block a user