Skip to content

Commit

Permalink
MDL-82129 mod_assign: penalty implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Nathan Nguyen committed Oct 28, 2024
1 parent d6b492d commit cd85b10
Show file tree
Hide file tree
Showing 16 changed files with 547 additions and 6 deletions.
4 changes: 3 additions & 1 deletion mod/assign/backup/moodle2/backup_assign_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ protected function define_structure() {
'activity',
'activityformat',
'timelimit',
'submissionattachments'));
'submissionattachments',
'gradepenalty'));

$userflags = new backup_nested_element('userflags');

Expand Down Expand Up @@ -129,6 +130,7 @@ protected function define_structure() {
'timemodified',
'grader',
'grade',
'penalty',
'attemptnumber'));

$pluginconfigs = new backup_nested_element('plugin_configs');
Expand Down
5 changes: 5 additions & 0 deletions mod/assign/backup/moodle2/restore_assign_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ protected function process_assign($data) {
$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);
Expand Down
150 changes: 150 additions & 0 deletions mod/assign/classes/penalty/helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?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;

defined('MOODLE_INTERNAL') || die();

require_once($CFG->dirroot . '/mod/assign/locallib.php');

use assign;
use context_module;
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 {
// Check if the penalty feature is enabled.
if (!\core_grades\local\penalty\manager::is_penalty_enabled()) {
return false;
}

// 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 <= 0) {
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 $DB;

// 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.
$deductedpercentage = apply_grade_penalty_to_user($userid, $gradeitem, $submissiondate, $duedate);

// 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]);
}
}
5 changes: 4 additions & 1 deletion mod/assign/db/install.xml
Original file line number Diff line number Diff line change
@@ -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"
>
Expand Down Expand Up @@ -41,13 +41,15 @@
<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."/>
</KEYS>
<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.">
Expand Down Expand Up @@ -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>
Expand Down
32 changes: 32 additions & 0 deletions mod/assign/db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,37 @@ function xmldb_assign_upgrade($oldversion) {
// Automatically generated Moodle v4.5.0 release upgrade line.
// Put any upgrade step following this.

if ($oldversion < 2024100701) {

// 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, 2024100701, 'assign');
}

return true;
}
3 changes: 3 additions & 0 deletions mod/assign/externallib.php
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ public static function get_assignments($courseids = array(), $capabilities = arr
'm.duedate, ' .
'm.allowsubmissionsfromdate, '.
'm.grade, ' .
'm.gradepenalty, ' .
'm.timemodified, '.
'm.completionsubmit, ' .
'm.cutoffdate, ' .
Expand Down Expand Up @@ -422,6 +423,7 @@ public static function get_assignments($courseids = array(), $capabilities = arr
'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,
Expand Down Expand Up @@ -541,6 +543,7 @@ private static function get_assignments_assignment_structure() {
'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'),
Expand Down
2 changes: 2 additions & 0 deletions mod/assign/lang/en/assign.php
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@
$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';
Expand Down
37 changes: 36 additions & 1 deletion mod/assign/lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -471,6 +473,14 @@ function assign_extend_settings_navigation(settings_navigation $settings, naviga
key: 'mod_assign_submissions'
);
}

// Allow changing grade penalty settings at course module level.
if (\mod_assign\penalty\helper::is_penalty_enabled($cm->instance)) {
$gradepenalties = get_plugin_list_with_function('gradepenalty', 'extend_navigation_module', 'lib.php');
foreach ($gradepenalties as $penaltyfunction) {
$penaltyfunction($navref, $cm);
}
}
}

/**
Expand Down Expand Up @@ -1063,14 +1073,39 @@ function assign_grade_item_update($assign, $grades=null) {
$grades = null;
}

return grade_update('mod/assign',
$result = grade_update('mod/assign',
$assign->courseid,
'mod',
'assign',
$assign->id,
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)) {
$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;
}

/**
Expand Down
Loading

0 comments on commit cd85b10

Please sign in to comment.