From 8f4a851c7f3529f5a9e370a3231aba3e7c91cb9e Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Wed, 6 Nov 2024 12:55:02 +1100 Subject: [PATCH] MDL-82126 gradereport_grader: apply penalty to overridden grade --- admin/settings/grades.php | 5 ++ .../grader/lang/en/gradereport_grader.php | 2 + grade/report/grader/lib.php | 47 +++++++++++++++- grade/report/grader/templates/cell.mustache | 10 +++- .../tests/behat/behat_gradereport_grader.php | 10 ++++ .../grade_override_with_deduction.feature | 52 ++++++++++++++++++ .../grader/overriden_with_penalty.mustache | 54 +++++++++++++++++++ lang/en/grades.php | 2 + lib/db/install.xml | 1 + lib/db/upgrade.php | 16 ++++++ lib/grade/grade_grade.php | 46 +++++++++++++++- lib/grade/grade_item.php | 15 ++++++ lib/gradelib.php | 2 +- version.php | 2 +- 14 files changed, 257 insertions(+), 7 deletions(-) create mode 100644 grade/report/grader/tests/behat/grade_override_with_deduction.feature create mode 100644 grade/templates/grades/grader/overriden_with_penalty.mustache diff --git a/admin/settings/grades.php b/admin/settings/grades.php index 2d6e86874a71b..c99121cc5f04c 100644 --- a/admin/settings/grades.php +++ b/admin/settings/grades.php @@ -246,6 +246,11 @@ new lang_string('gradepenalty_supportedplugins', 'grades'), new lang_string('gradepenalty_supportedplugins_help', 'grades'), [], $options)); + // Option to apply penalty to overridden grades. + $temp->add(new admin_setting_configcheckbox('gradepenalty_overriddengrade', + new lang_string('gradepenalty_overriddengrade', 'grades'), + new lang_string('gradepenalty_overriddengrade_help', 'grades'), 0)); + $ADMIN->add('gradepenalty', $temp); } diff --git a/grade/report/grader/lang/en/gradereport_grader.php b/grade/report/grader/lang/en/gradereport_grader.php index bf27242c63cd8..207932dd864e8 100644 --- a/grade/report/grader/lang/en/gradereport_grader.php +++ b/grade/report/grader/lang/en/gradereport_grader.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['applypenaltytext'] = 'Penalty exemption'; +$string['applypenaltytooltip'] = 'If the box is checked, penalty will not be applied to the overridden grade.'; $string['aria:dropdowncolumns'] = 'Collapsed columns found'; $string['clearsearch'] = 'Clear searched users'; $string['collapsedcolumns'] = 'Collapsed columns {$a}'; diff --git a/grade/report/grader/lib.php b/grade/report/grader/lib.php index 413bbf02a113a..d6df8a9032b9e 100644 --- a/grade/report/grader/lib.php +++ b/grade/report/grader/lib.php @@ -241,17 +241,29 @@ public function process_data($data) { continue; } + // Detect changes in exemption checkbox. + if ($oldvalue->can_apply_penalty_to_overridden_mark()) { + if (!isset($data->exemption[$userid][$itemid])) { + $newvalue = format_float($postedvalue - $oldvalue->deductedmark, + $oldvalue->grade_item->get_decimals()); + } else { + $newvalue = $postedvalue; + } + } else { + $newvalue = $postedvalue; + } + // If the grade item uses a custom scale if (!empty($oldvalue->grade_item->scaleid)) { - if ((int)$oldvalue->finalgrade === (int)$postedvalue) { + if ((int)$oldvalue->finalgrade === (int)$newvalue) { continue; } } else { // The grade item uses a numeric scale // Format the finalgrade from the DB so that it matches the grade from the client - if ($postedvalue === format_float($oldvalue->finalgrade, $oldvalue->grade_item->get_decimals())) { + if ($newvalue === format_float($oldvalue->finalgrade, $oldvalue->grade_item->get_decimals())) { continue; } } @@ -326,8 +338,19 @@ public function process_data($data) { } } + // Save final grade, without penalty. $gradeitem->update_final_grade($userid, $finalgrade, 'gradebook', false, FORMAT_MOODLE, null, null, true); + + // Save overridden mark, without penalty. + $gradeitem->update_overridden_mark($userid, $finalgrade); + + // Apply penalty. + if ($oldvalue->can_apply_penalty_to_overridden_mark() && !isset($data->exemption[$userid][$itemid])) { + // Apply penalty. + $gradeitem->update_final_grade($userid, $newvalue, 'gradepenalty', false, + FORMAT_MOODLE, null, null, true); + } } } } @@ -1146,6 +1169,26 @@ public function get_right_rows(bool $displayaverages): array { if ($context->statusicons) { $context->extraclasses .= ' statusicons'; } + + // Show option for user to apply penalty or not. + if ($grade->can_apply_penalty_to_overridden_mark()) { + $context->canapplypenalty = true; + if ($grade->is_penalty_applied_to_final_grade()) { + // We are editing the original grade value, ie, before applying penalty. + $context->value = format_float($gradeval + $grade->deductedmark, $decimalpoints); + } else { + $context->value = $value; + } + // Current grade. + $context->effectivegrade = $value; + $context->deductedmark = format_float($grade->deductedmark, $decimalpoints); + $context->penaltyexempted = !$grade->is_penalty_applied_to_final_grade(); + $context->exemptionid = 'exemption' . $userid . '_' . $item->id; + $context->exemptionname = 'exemption[' . $userid . '][' . $item->id .']'; + $context->exemptionlabel = $gradelabel . ' ' . + get_string('applypenaltytext', 'gradereport_grader'); + $context->exemptiontooltip = get_string('applypenaltytooltip', 'gradereport_grader'); + } } else { $context->extraclasses = 'gradevalue' . $hidden . $gradepass; $context->text = format_float($gradeval, $decimalpoints); diff --git a/grade/report/grader/templates/cell.mustache b/grade/report/grader/templates/cell.mustache index 52b8910a844b2..3114041a0af87 100644 --- a/grade/report/grader/templates/cell.mustache +++ b/grade/report/grader/templates/cell.mustache @@ -30,7 +30,8 @@ "extraclasses": "statusicons", "value": "Text information", "tabindex": "1", - "name": "grade[313][624]" + "name": "grade[313][624]", + "canapplypenalty": "false" } }}
@@ -41,7 +42,12 @@ {{>core_grades/grades/grader/scale}} {{/scale}} {{^scale}} - {{>core_grades/grades/grader/input}} + {{#canapplypenalty}} + {{>core_grades/grades/grader/overriden_with_penalty}} + {{/canapplypenalty}} + {{^canapplypenalty}} + {{>core_grades/grades/grader/input}} + {{/canapplypenalty}} {{/scale}} {{/iseditable}} {{^iseditable}} diff --git a/grade/report/grader/tests/behat/behat_gradereport_grader.php b/grade/report/grader/tests/behat/behat_gradereport_grader.php index 46acb8c3fc1f7..24132ed6e5a02 100644 --- a/grade/report/grader/tests/behat/behat_gradereport_grader.php +++ b/grade/report/grader/tests/behat/behat_gradereport_grader.php @@ -144,4 +144,14 @@ public static function get_partial_named_selectors(): array { ), ]; } + + /** + * Enable penalty for overridden grade. + * + * @Given I enable penalty for overridden grade + */ + public function i_enable_penalty_for_overridden_grade(): void { + set_config('gradepenalty_enabled', 1); + set_config('gradepenalty_overriddengrade', 1); + } } diff --git a/grade/report/grader/tests/behat/grade_override_with_deduction.feature b/grade/report/grader/tests/behat/grade_override_with_deduction.feature new file mode 100644 index 0000000000000..e87896e87d965 --- /dev/null +++ b/grade/report/grader/tests/behat/grade_override_with_deduction.feature @@ -0,0 +1,52 @@ +@gradereport @gradereport_grader @gradereport_grader_deduction +Feature: As a teacher, I want to override a grade with a deduction and check the gradebook. + + Background: + Given the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And I enable penalty for overridden grade + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "grade items" exist: + | itemname | grademin | grademax | course | + | Manual grade 01 | 0 | 100 | C1 | + | Manual grade 02 | 0 | 100 | C1 | + And the following "grade grades" exist: + | gradeitem | user | grade | deductedmark | + | Manual grade 01 | student1 | 60 | 10 | + | Manual grade 02 | student1 | 80 | 20 | + When I log in as "teacher1" + + @javascript + Scenario: Override a grade with a deduction and check the gradebook + Given I am on "Course 1" course homepage + And I navigate to "View > Grader report" in the course gradebook + And the following should exist in the "user-grades" table: + | -1- | -2- | -3- | -4- | -5- | + | Student 1 | student1@example.com | 60 | 80 | 140 | + And I turn editing mode on + And I set the following fields to these values: + | Student 1 Manual grade 01 grade | 80 | + | Student 1 Manual grade 01 Penalty exemption | 0 | + | Student 1 Manual grade 02 Penalty exemption | 1 | + And I click on "Save changes" "button" + When I turn editing mode off + Then the following should exist in the "user-grades" table: + | -1- | -2- | -3- | -4- | -5- | + | Student 1 | student1@example.com | 70 | 100 | 170 | + When I turn editing mode on + And I set the following fields to these values: + | Student 1 Manual grade 02 grade | 100 | + | Student 1 Manual grade 02 Penalty exemption | 0 | + And I click on "Save changes" "button" + And I turn editing mode off + Then the following should exist in the "user-grades" table: + | -1- | -2- | -3- | -4- | -5- | + | Student 1 | student1@example.com | 70 | 80 | 150 | diff --git a/grade/templates/grades/grader/overriden_with_penalty.mustache b/grade/templates/grades/grader/overriden_with_penalty.mustache new file mode 100644 index 0000000000000..7f9f20cb86557 --- /dev/null +++ b/grade/templates/grades/grader/overriden_with_penalty.mustache @@ -0,0 +1,54 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_grades/grades/grader/overriden_with_penalty + + Input template for grader report cell. + + Example context (json): + { + "deductedmark": "30.00", + "effectivegrade": "70.00", + "exemptionid": "exemption3_2", + "exemptionlabel": "Exempt penalty", + "exemptiontooltip": "Exempt penalty", + "exemptionname": "exemption[3][2]", + "penaltyexempted": "true", + "id": "grade_313_624", + "label": "grade_313_624", + "name": "grade[313][624]" + } +}} + +
+
+
Original grade:
+ {{>core_grades/grades/grader/input}} +
+ +
+
Current grade: {{{effectivegrade}}}
+
+ +
+
Exempt penalty:
+ + +
+
diff --git a/lang/en/grades.php b/lang/en/grades.php index a886c4a7d36d5..7bed40640f2de 100644 --- a/lang/en/grades.php +++ b/lang/en/grades.php @@ -334,6 +334,8 @@ $string['gradepenalty_enabled'] = 'Grade penalty'; $string['gradepenalty_enabled_help'] = 'If enabled, the penalty will be applied to the grades of supported modules.'; $string['gradepenalty_indicator_info'] = 'Late penalty applied -{$a} marks'; +$string['gradepenalty_overriddengrade'] = 'Apply penalty to overridden grades'; +$string['gradepenalty_overriddengrade_help'] = 'If enabled, the penalty will be applied to overridden grades.'; $string['gradepenalty_supportedplugins'] = 'Supported modules'; $string['gradepenalty_supportedplugins_help'] = 'Enable the grade penalty for the selected modules.'; $string['gradepointdefault'] = 'Grade point default'; diff --git a/lib/db/install.xml b/lib/db/install.xml index 813fe9beb96a7..444391fb1df56 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -2060,6 +2060,7 @@ + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 3779ae19bbc6d..5bc45491cb9c7 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1338,5 +1338,21 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2024120500.03); } + if ($oldversion < 2024121301.00) { + + // Define field overriddenmark to be added to grade_grades. + $table = new xmldb_table('grade_grades'); + $field = new xmldb_field('overriddenmark', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'deductedmark'); + + // Conditionally launch add field penalty. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2024121301.00); + } + return true; } diff --git a/lib/grade/grade_grade.php b/lib/grade/grade_grade.php index 1ac340ba0aaae..91e4b5d00a651 100644 --- a/lib/grade/grade_grade.php +++ b/lib/grade/grade_grade.php @@ -50,7 +50,7 @@ class grade_grade extends grade_object { public $required_fields = array('id', 'itemid', 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin', 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime', 'exported', 'overridden', 'excluded', 'timecreated', - 'timemodified', 'aggregationstatus', 'aggregationweight', 'deductedmark'); + 'timemodified', 'aggregationstatus', 'aggregationweight', 'deductedmark', 'overriddenmark'); /** * Array of optional fields with default values (these should match db defaults) @@ -221,6 +221,9 @@ class grade_grade extends grade_object { /** @var float $deductedmark mark deducted from final grade */ public $deductedmark = 0; + /** @var float $overriddenmark mark overridden by teacher */ + public $overriddenmark = 0; + /** * Returns array of grades for given grade_item+users * @@ -1287,4 +1290,45 @@ public function get_context() { $this->load_grade_item(); return $this->grade_item->get_context(); } + + /** + * Determine if penalty is applied to this overridden mark. + * + * @return bool whether penalty is applied + */ + public function can_apply_penalty_to_overridden_mark(): bool { + // Check config. + if (!get_config('core', 'gradepenalty_overriddengrade')) { + return false; + } + + // Check if the raw grade was deducted. + if ($this->deductedmark <= 0) { + return false; + } + + return true; + } + + /** + * Whether the penalty is applied to this overridden mark. + * + * @return bool whether penalty is applied + */ + public function is_penalty_applied_to_overridden_mark(): bool { + return $this->overridden > 0 && $this->overriddenmark > $this->finalgrade; + } + + /** + * Whether the penalty is applied to this final grade. + * + * @return bool whether penalty is applied + */ + public function is_penalty_applied_to_final_grade(): bool { + if ($this->overridden > 0) { + return $this->is_penalty_applied_to_overridden_mark(); + } else { + return $this->deductedmark > 0; + } + } } diff --git a/lib/grade/grade_item.php b/lib/grade/grade_item.php index a57d577d0816a..09ce089bc0008 100644 --- a/lib/grade/grade_item.php +++ b/lib/grade/grade_item.php @@ -2161,6 +2161,21 @@ public function update_deducted_mark(int $userid, float $deductedmark): void { $grade->update(); } + /** + * Update overridden mark for given user + * + * @param int $userid The graded user + * @param float $overriddenmark The mark deducted from final grade + */ + public function update_overridden_mark(int $userid, float $overriddenmark): void { + $grade = new grade_grade([ + 'itemid' => $this->id, + 'userid' => $userid, + ]); + $grade->overriddenmark = $overriddenmark; + $grade->update(); + } + /** * Calculates final grade values using the formula in the calculation property. * The parameters are taken from final grades of grade items in current course only. diff --git a/lib/gradelib.php b/lib/gradelib.php index 2bc2b877e33f4..48b91e230a91e 100644 --- a/lib/gradelib.php +++ b/lib/gradelib.php @@ -859,7 +859,7 @@ function show_penalty_indicator(grade_grade $grade): string { global $PAGE; // Show penalty indicator if penalty is greater than 0. - if ($grade->deductedmark > 0) { + if ($grade->is_penalty_applied_to_final_grade()) { $indicator = new \core_grades\output\penalty_indicator(2, $grade); $renderer = $PAGE->get_renderer('core_grades'); return $renderer->render_penalty_indicator($indicator); diff --git a/version.php b/version.php index c00b3a8a5bd1c..9e38399b3b1ae 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2024121300.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2024121301.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '5.0dev (Build: 20241213)'; // Human-friendly version name