MDL-80984 mod_assign: penalty implementation

This commit is contained in:
Nathan Nguyen
2025-03-19 23:00:52 +08:00
committed by Andrew Nicols
parent cfc6db3866
commit cd239dd1c3
33 changed files with 1276 additions and 26 deletions
+3
View File
@@ -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"}
+10
View File
@@ -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"}
+141
View File
@@ -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);
+148
View File
@@ -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);
}
}
+4 -1
View File
@@ -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>
+32
View File
@@ -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;
}
+3
View File
@@ -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'),
+4 -3
View File
@@ -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;
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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 .= '&nbsp;/&nbsp;' . 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 .= '&nbsp;/&nbsp;' . 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
View File
@@ -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
+32
View File
@@ -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.
*
+15
View File
@@ -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);
+16
View File
@@ -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)) {
+22 -9
View File
@@ -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);
}
}
+43 -1
View File
@@ -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"
+3
View File
@@ -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));
@@ -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';
+272
View File
@@ -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);
}
}
+1 -1
View File
@@ -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.