diff --git a/UPGRADING.md b/UPGRADING.md index 98fdb8cb01639..27792ef402102 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -10,6 +10,8 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt ### core +- Modules using the grade penalty MUST declare their use of it with the xxx_supports() flag FEATURE_GRADE_HAS_PENALTY. + #### Removed - The previously deprecated function `search_generate_text_SQL` has been removed and can no longer be used. diff --git a/admin/settings/grades.php b/admin/settings/grades.php index b84239e5fc74b..9ff110ad00416 100644 --- a/admin/settings/grades.php +++ b/admin/settings/grades.php @@ -221,5 +221,41 @@ } } -} // end of speedup + // Penalty. + $ADMIN->add('grades', new admin_category('gradepenalty', new lang_string('gradepenalty', 'grades'))); + // General settings for penalty. + $temp = new admin_settingpage('penaltysettings', new lang_string('gradepenalty_general_settings', 'grades'), + 'moodle/grade:manage'); + if ($ADMIN->fulltree) { + // Enable. + $temp->add(new admin_setting_configcheckbox('gradepenalty_enabled', + new lang_string('gradepenalty_enabled', 'grades'), + new lang_string('gradepenalty_enabled_help', 'grades'), 0)); + // List of modules which support penalty. + $supported = core_grades\local\penalty\manager::get_supported_modules(); + $temp->add(new admin_setting_configmultiselect('gradepenalty_supportedmodules', + new lang_string('gradepenalty_supportedmodules', 'grades'), + new lang_string('gradepenalty_supportedmodules_help', 'grades'), $supported, $supported)); + } + $ADMIN->add('gradepenalty', $temp); + + if (get_config('core', 'gradepenalty_enabled')) { + // External page to manage the penalty plugins. + $temp = new admin_externalpage( + 'managepenaltyplugins', + get_string('managepenaltyplugins', 'grades'), + new moodle_url('/grade/penalty/manage_penalty_plugins.php'), + 'moodle/grade:manage' + ); + $ADMIN->add('gradepenalty', $temp); + + // Settings from each penalty plugin. + foreach (core_component::get_plugin_list('gradepenalty') as $plugin => $plugindir) { + // Include all the settings commands for this plugin if there are any. + if (file_exists($plugindir.'/settings.php')) { + include($plugindir.'/settings.php'); + } + } + } +} // end of speedup diff --git a/grade/classes/hook/after_penalty_applied.php b/grade/classes/hook/after_penalty_applied.php new file mode 100644 index 0000000000000..92cea85789deb --- /dev/null +++ b/grade/classes/hook/after_penalty_applied.php @@ -0,0 +1,74 @@ +. + +namespace core_grades\hook; + +use Psr\EventDispatcher\StoppableEventInterface; +/** + * Hook after penalty is applied. + * + * This hook will be dispatched after the penalty is applied to the grade. + * + * @package core_grades + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[\core\attribute\label('Notify that the penalty has been applied to a given grade.')] +#[\core\attribute\tags('grade')] +class after_penalty_applied implements StoppableEventInterface { + /** + * Constructor for the hook. + * + * @param int $courseid The course id. + * @param string $itemtype The type of the grade item. + * @param string $itemmodule The module of the grade item. + * @param int $iteminstance The instance of the grade item. + * @param int $itemnumber The number of the grade item. + * @param int $userid the user id + */ + public function __construct( + /** @var int The course id */ + public readonly int $courseid, + /** @var string The type of the grade item */ + public readonly string $itemtype, + /** @var string The module of the grade item */ + public readonly string $itemmodule, + /** @var int The instance of the grade item */ + public readonly int $iteminstance, + /** @var int The number of the grade item */ + public readonly int $itemnumber, + /** @var int The course id */ + public readonly int $userid, + ) { + } + + /** + * Should the next listener be called? + * + * @return bool + */ + public function isPropagationStopped(): bool { + return $this->stopped; + } + + /** + * Stop the propagation of the event. + * + */ + public function stop(): void { + $this->stopped = true; + } +} diff --git a/grade/classes/hook/before_penalty_applied.php b/grade/classes/hook/before_penalty_applied.php new file mode 100644 index 0000000000000..74aa5df6c0e1a --- /dev/null +++ b/grade/classes/hook/before_penalty_applied.php @@ -0,0 +1,117 @@ +. + +namespace core_grades\hook; + +use Psr\EventDispatcher\StoppableEventInterface; + +/** + * Hook before penalty is applied. + * + * This hook will be dispatched before the penalty is applied to the grade. + * + * @package core_grades + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[\core\attribute\label('Allows plugins to add required data before the penalty is applied to the grade.')] +#[\core\attribute\tags('grade')] +class before_penalty_applied implements StoppableEventInterface { + /** @var int submission date*/ + protected int $submissiondate = 0; + + /** @var int due date */ + protected int $duedate = 0; + + /** + * Constructor for the hook. + * + * @param int $courseid The course id. + * @param string $itemtype The type of the grade item. + * @param string $itemmodule The module of the grade item. + * @param int $iteminstance The instance of the grade item. + * @param int $itemnumber The number of the grade item. + * @param int $userid the user id + */ + public function __construct( + /** @var int The course id */ + public readonly int $courseid, + /** @var string The type of the grade item */ + public readonly string $itemtype, + /** @var string The module of the grade item */ + public readonly string $itemmodule, + /** @var int The instance of the grade item */ + public readonly int $iteminstance, + /** @var int The number of the grade item */ + public readonly int $itemnumber, + /** @var int The course id */ + public readonly int $userid, + ) { + } + + /** + * Set the submission date. + * + * @param int $submissiondate The submission date. + */ + public function set_submission_date(int $submissiondate): void { + $this->submissiondate = $submissiondate; + } + + /** + * Set the due date. + * + * @param int $duedate The due date. + */ + public function set_due_date(int $duedate): void { + $this->duedate = $duedate; + } + + /** + * Get the submission date. + * + * @return int The submission date. + */ + public function get_submission_date(): int { + return $this->submissiondate; + } + + /** + * Get the due date. + * + * @return int The due date. + */ + public function get_due_date(): int { + return $this->duedate; + } + + /** + * Should the next listener be called? + * + * @return bool + */ + public function isPropagationStopped(): bool { + return $this->stopped; + } + + /** + * Stop the propagation of the event. + * + */ + public function stop(): void { + $this->stopped = true; + } +} diff --git a/grade/classes/local/penalty/grade_penalty.php b/grade/classes/local/penalty/grade_penalty.php new file mode 100644 index 0000000000000..aa008275e0547 --- /dev/null +++ b/grade/classes/local/penalty/grade_penalty.php @@ -0,0 +1,112 @@ +. + +/** + * Grade penalty abstract class. + * + * @package core_grades + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_grades\local\penalty; + +defined('MOODLE_INTERNAL') || die(); + +use grade_item; +use stdClass; + +require_once($CFG->libdir.'/gradelib.php'); + +/** + * Grade penalty abstract class. + * + * @package core_grades + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class grade_penalty { + /** @var stdClass The course moudule object */ + public readonly stdClass $cm; + + /** + * Constructor for the hook. + * + * @param int $courseid The course id. + * @param string $itemtype The type of the grade item. + * @param string $itemmodule The module of the grade item. + * @param int $iteminstance The instance of the grade item. + * @param int $itemnumber The number of the grade item. + * @param int $userid the user id + */ + public function __construct( + /** @var int The course id */ + public readonly int $courseid, + /** @var string The type of the grade item */ + public readonly string $itemtype, + /** @var string The module of the grade item */ + public readonly string $itemmodule, + /** @var int The instance of the grade item */ + public readonly int $iteminstance, + /** @var int The number of the grade item */ + public readonly int $itemnumber, + /** @var int The course id */ + public readonly int $userid, + /** @var int The submission date */ + public readonly int $submissiondate, + /** @var int The due date */ + public readonly int $duedate, + ) { + // Course module required to get the penalty rules. + $this->cm = get_coursemodule_from_instance($itemmodule, $iteminstance, $courseid); + } + + /** + * Mark will be deducted from student grade. + * + * @return float + */ + abstract public function calculate_penalty($finalgrade): float; + + /** + * Apply penalty to the grade. + * + * @return bool return true if penalty is applied successfully. + */ + final public function apply_penalty(): bool { + // Fetch the grade item. + $gradeitem = grade_item::fetch( + [ + 'courseid' => $this->courseid, + 'itemtype' => $this->itemtype, + 'itemmodule' => $this->itemmodule, + 'iteminstance' => $this->iteminstance, + 'itemnumber' => $this->itemnumber, + ] + ); + + // Get the final grade. It returns a single grade object as we specify the user id. + $usergrade = $gradeitem->get_final($this->userid); + $finalgrade = $usergrade->finalgrade; + + // Calculate the penalty. + $deductedgrade = $this->calculate_penalty($finalgrade); + $finalgrade -= $deductedgrade; + + // Update the final grade. + return $gradeitem->update_final_grade($this->userid, $finalgrade); + } +} diff --git a/grade/classes/local/penalty/manager.php b/grade/classes/local/penalty/manager.php new file mode 100644 index 0000000000000..22b5988319ad3 --- /dev/null +++ b/grade/classes/local/penalty/manager.php @@ -0,0 +1,120 @@ +. + +namespace core_grades\local\penalty; + +use core\check\performance\debugging; +use core\di; +use core\hook; +use core\plugininfo\gradepenalty; + +/** + * Helper class for grade penalty. + * + * All the functions which has a potential to be used by different features or + * plugins, should go here. + * + * @package core_grades + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manager { + /** + * Lists of modules which support grade penalty feature. + * + * @return array list of supported modules. + */ + public static function get_supported_modules(): array { + $mods = \core_component::get_plugin_list('mod'); + $supported = []; + foreach ($mods as $mod => $plugindir) { + if (plugin_supports('mod', $mod, FEATURE_GRADE_HAS_PENALTY)) { + $supported[] = $mod; + } + } + return $supported; + } + + /** + * Get active grade penalty plugin. + * + * @return string the active grade penalty plugin. + */ + public static function get_active_grade_penalty_plugin(): string { + $plugins = gradepenalty::get_enabled_plugins(); + return reset($plugins); + } + + /** + * Get required class that provide required data for the penalty calculation. + * + * @param string $plugin the plugin name. + * @return string|null the required class. + */ + public static function get_required_class(string $plugin): ?string { + $pluginclass = 'gradepenalty_' . $plugin; + // Check if the plugin class exists. + if (!class_exists($pluginclass)) { + debugging('The grade penalty plugin class ' . $pluginclass . ' does not exist.', DEBUG_DEVELOPER); + return null; + } + return $pluginclass; + } + + /** + * Run after a grade is updated / created. + * + * @param int $courseid ID of course + * @param string $itemtype Type of grade item + * @param string $itemmodule More specific then $itemtype + * @param int $iteminstance Instance ID of graded item + * @param int $itemnumber modules can use other numbers when having more than one grade for each user + * @return bool if the penalty is applied successfully. + */ + public static function apply_penalty(int $courseid, string $itemtype, string $itemmodule, + int $iteminstance, int $itemnumber, int $userid): bool { + + // Get the active grade penalty plugin. + $plugin = self::get_active_grade_penalty_plugin(); + // No active plugin, return. + if (empty($plugin)) { + return true; + } + + // Hook for plugins to add required data before the penalty is applied to the grade. + $hook = new \core_grades\hook\before_penalty_applied($courseid, $itemtype, $itemmodule, + $iteminstance, $itemnumber, $userid); + di::get(hook\manager::class)->dispatch($hook); + + // Run the penalty calculation. + $pluginclass = self::get_required_class($plugin); + if (empty($pluginclass)) { + return false; + } + $penalty = new $pluginclass($courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $userid, + $hook->get_submission_date(), $hook->get_due_date()); + // Apply the penalty. + $success = $penalty->apply_penalty(); + + // Hook for plugins to process further after the penalty is applied to the grade. + $hook = new \core_grades\hook\after_penalty_applied($courseid, $itemtype, $itemmodule, + $iteminstance, $itemnumber, $userid); + di::get(hook\manager::class)->dispatch($hook); + + return $success; + } +} diff --git a/grade/classes/table/gradepenalty_management_table.php b/grade/classes/table/gradepenalty_management_table.php new file mode 100644 index 0000000000000..b35aeb27de9e6 --- /dev/null +++ b/grade/classes/table/gradepenalty_management_table.php @@ -0,0 +1,49 @@ +. + +namespace core_grades\table; + +use core_admin\table\plugin_management_table; +use moodle_url; + +/** + * Table to manage grade penalty plugin. + * + * @package core_grades + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class gradepenalty_management_table extends plugin_management_table { + + /** + * Return the penalty plugin type. + * + * @return string + */ + protected function get_plugintype(): string { + return 'gradepenalty'; + } + + /** + * Get the URL to manage the penalty plugin. + * + * @param array $params + * @return moodle_url + */ + protected function get_action_url(array $params = []): moodle_url { + return new moodle_url('/grade/penalty/manage_penalty_plugins.php', $params); + } +} diff --git a/grade/penalty/duedate/classes/gradepenalty_duedate.php b/grade/penalty/duedate/classes/gradepenalty_duedate.php new file mode 100644 index 0000000000000..a25efc23bc744 --- /dev/null +++ b/grade/penalty/duedate/classes/gradepenalty_duedate.php @@ -0,0 +1,94 @@ +. + +namespace gradepenalty_duedate; + +use context_module; +use context_course; +use context_system; +use core_grades\local\penalty\grade_penalty; + +/** + * Calculate penalty. + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class gradepenalty_duedate extends grade_penalty { + + /** + * Mark will be deducted from student grade. + * + * @return float + */ + public function calculate_penalty($finalgrade): float { + $penalty = 0.0; + + // Calculate the difference between the submission date and the due date. + $diff = $this->submissiondate - $this->duedate; + + // If the submission date is after the due date, calculate the penalty. + if ($diff > 0) { + // Get all penalty rules, ordered by the highest penalty first. + $penaltyrules = $this->find_effective_penalty_rules(); + + // Check each rule to see which rule will apply. + if (!empty($penaltyrules)) { + foreach ($penaltyrules as $penaltyrule) { + if ($diff >= $penaltyrule->get('latefor')) { + $penalty = $penaltyrule->get('penalty'); + break; + } + } + } + } + + // Calculate the deducted grade. + return $finalgrade * $penalty / 100; + } + + /** + * Find effecitve penalty rule. + * + * @param int $cmid Course module id. + * @return array + */ + public function find_effective_penalty_rules(): array { + // Course module context id. + $modulecontext = context_module::instance($this->cm->id); + + // Get all penalty rules, ordered by the highest penalty first. + $penaltyrules = penalty_rule::get_records(['contextid' => $modulecontext->id], 'sortorder DESC'); + + // If there is no penalty rule, go to the course context. + if (empty($penaltyrules)) { + // Find course content. + $course = get_course($this->cm->course); + $coursecontext = context_course::instance($course->id); + + $penaltyrules = penalty_rule::get_records(['contextid' => $coursecontext->id], 'sortorder DESC'); + } + + // If there is no penalty rule, go to the system context. + if (empty($penaltyrules)) { + $systemcontext = context_system::instance(); + $penaltyrules = penalty_rule::get_records(['contextid' => $systemcontext->id], 'sortorder DESC'); + } + + return $penaltyrules; + } +} diff --git a/grade/penalty/duedate/classes/helper.php b/grade/penalty/duedate/classes/helper.php new file mode 100644 index 0000000000000..7eb24e68e61f3 --- /dev/null +++ b/grade/penalty/duedate/classes/helper.php @@ -0,0 +1,124 @@ +. + +namespace gradepenalty_duedate; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../../../lib.php'); + +/** + * Helper for grade penalty + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + /** + * Determine min and max value for latefor and penalty for a rule when updating or inserting. + * If updating, the ruleid belongs to the rule we are updating. + * If inserting, the ruleid belong to the rule which we will insert new rule before or after it + * + * @param int $ruleid the rule id we are updating or inserting + * @param int $action we are updating or inserting + * @param string $field which is 'latefor' or 'penalty' + * @param int $defaultmin default min value + * @param int $defaultmax default max value + * @return array + */ + public static function calculate_min_max_values($ruleid, $action, $field, $defaultmin, $defaultmax) { + global $DB; + + $minlatefor = $defaultmin; + $maxlatefor = $defaultmax; + + if ($ruleid !== 0) { + // Current rule. + $currentrule = $DB->get_record('gradepenalty_duedate_rule', ['id' => $ruleid]); + + // Get the previous rule. + $previousrule = $DB->get_record('gradepenalty_duedate_rule', [ + 'sortorder' => $currentrule->sortorder - 1, + 'contextid' => $currentrule->contextid, + ]); + + // Get the next rule. + $nextrule = $DB->get_record('gradepenalty_duedate_rule', [ + 'sortorder' => $currentrule->sortorder + 1, + 'contextid' => $currentrule->contextid, + ]); + + if ($action === GRADEPENALTY_DUEDATE_ACTION_INSERT_ABOVE) { + // We will insert new rule above the current rule. + $minlatefor = $previousrule ? $previousrule->$field + 1 : $defaultmin; + $maxlatefor = $currentrule->$field - 1; + } else if ($action === GRADEPENALTY_DUEDATE_ACTION_INSERT_BELOW) { + // We will insert new rule below the current rule. + $minlatefor = $currentrule->$field + 1; + $maxlatefor = $nextrule ? $nextrule->$field - 1 : $defaultmax; + } else if ($action === GRADEPENALTY_DUEDATE_ACTION_UPDATE) { + // We are updating the rule, so we need to check the min and max value for the rule. + $minlatefor = $previousrule ? $previousrule->$field + 1 : $defaultmin; + $maxlatefor = $nextrule ? $nextrule->$field - 1 : $defaultmax; + } + } + + return [$minlatefor, $maxlatefor]; + } + + /** + * Whether we can insert rule above or below. + * + * @param int $ruleid the rule id which we want to insert above or below. + * @param int $action insert above or below. + * + * @return bool + */ + public static function can_insert_rule(int $ruleid, int $action): bool { + global $DB; + + if ($ruleid === 0) { + return false; + } + + $currentrule = $DB->get_record('gradepenalty_duedate_rule', ['id' => $ruleid]); + + if ($action === GRADEPENALTY_DUEDATE_ACTION_INSERT_ABOVE) { + // Get the previous rule. + $previousrule = $DB->get_record('gradepenalty_duedate_rule', [ + 'sortorder' => $currentrule->sortorder - 1, + 'contextid' => $currentrule->contextid, + ]); + $previouslatefor = $previousrule ? $previousrule->latefor : GRADEPENALTY_DUEDATE_MIN_LATEFOR; + $previouspenalty = $previousrule ? $previousrule->penalty : GRADEPENALTY_DUEDATE_MIN_PENALTY; + // Check if we still have space for insertion (at least 1 second and 1 percent). + return $currentrule->latefor > ($previouslatefor + 1) && $currentrule->penalty > ($previouspenalty + 1); + } else if ($action === GRADEPENALTY_DUEDATE_ACTION_INSERT_BELOW) { + // Get the next rule. + $nextrule = $DB->get_record('gradepenalty_duedate_rule', [ + 'sortorder' => $currentrule->sortorder + 1, + 'contextid' => $currentrule->contextid, + ]); + $nextlatefor = $nextrule ? $nextrule->latefor : GRADEPENALTY_DUEDATE_MAX_LATEFOR; + $nextpenalty = $nextrule ? $nextrule->penalty : GRADEPENALTY_DUEDATE_MAX_PENALTY; + // Check if we still have space for insertion (at least 1 second and 1 percent). + return $currentrule->latefor < ($nextlatefor - 1) && $currentrule->penalty < ($nextpenalty - 1); + } + + return false; + } +} diff --git a/grade/penalty/duedate/classes/output/form/penalty_rule_form.php b/grade/penalty/duedate/classes/output/form/penalty_rule_form.php new file mode 100644 index 0000000000000..3efd93f1c733d --- /dev/null +++ b/grade/penalty/duedate/classes/output/form/penalty_rule_form.php @@ -0,0 +1,153 @@ +. + +namespace gradepenalty_duedate\output\form; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); +require_once(__DIR__ . '/../../../lib.php'); + +use gradepenalty_duedate\helper; +use gradepenalty_duedate\penalty_rule; +use moodleform; + +/** + * Form to set up the penalty rules for the gradepenalty_duedate plugin. + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class penalty_rule_form extends moodleform { + /** @var int Min latefor value */ + protected $minlatefor = GRADEPENALTY_DUEDATE_MIN_LATEFOR; + + /** @var int Max latefor value */ + protected $maxlatefor = GRADEPENALTY_DUEDATE_MAX_LATEFOR; + + /** @var int Min penalty value */ + protected $minpenalty = GRADEPENALTY_DUEDATE_MIN_PENALTY; + + /** @var int Max penalty value */ + protected $maxpenalty = GRADEPENALTY_DUEDATE_MAX_PENALTY; + + /** @var int ruleid */ + protected $ruleid = 0; + + /** @var int contextid */ + protected $contextid = 0; + + /** @var int action */ + protected $action = GRADEPENALTY_DUEDATE_ACTION_CREATE; + + /** + * Define the form. + * + * @return void + */ + public function definition() { + $mform = $this->_form; + + // Set up min/max for latefor and penalty. + $this->ruleid = $this->_customdata['ruleid'] ?? 0; + $this->contextid = $this->_customdata['contextid'] ?? 0; + $this->action = $this->_customdata['action'] ?? GRADEPENALTY_DUEDATE_ACTION_CREATE; + + // Calculate min/max value for latefor. + list($this->minlatefor, $this->maxlatefor) = helper::calculate_min_max_values($this->ruleid, $this->action, 'latefor', + GRADEPENALTY_DUEDATE_MIN_LATEFOR, GRADEPENALTY_DUEDATE_MAX_LATEFOR); + // And for penalty. + list($this->minpenalty, $this->maxpenalty) = helper::calculate_min_max_values($this->ruleid, $this->action, 'penalty', + GRADEPENALTY_DUEDATE_MIN_PENALTY, GRADEPENALTY_DUEDATE_MAX_PENALTY); + + // Hidden context id, value is stored in $mform. + $mform->addElement('hidden', 'contextid'); + $mform->setType('contextid', PARAM_INT); + $mform->setDefault('contextid', $this->contextid); + + // Hidden rule id, value is stored in $mform. + $mform->addElement('hidden', 'ruleid'); + $mform->setType('ruleid', PARAM_INT); + $mform->setDefault('ruleid', $this->ruleid); + + // Hidden action, value is stored in $mform. + $mform->addElement('hidden', 'action'); + $mform->setType('action', PARAM_INT); + $mform->setDefault('action', $this->action); + + // If ruleid is not 0, then we are editing an existing rule. + $rule = new penalty_rule($this->ruleid); + + // Latefor field. + $mform->addElement('duration', 'latefor', get_string('latefor', 'gradepenalty_duedate'), ['defaultunit' => DAYSECS]); + $mform->setType('latefor', PARAM_INT); + // Default value. If we are updating a rule, use the current value. + $mform->setDefault('latefor', $this->action === GRADEPENALTY_DUEDATE_ACTION_UPDATE ? + $rule->get('latefor') : $this->minlatefor); + // Required rule. + $mform->addRule('latefor', get_string('required'), 'required'); + // Help button. + $mform->addHelpButton('latefor', 'latefor', 'gradepenalty_duedate'); + + // Penalty field. + $mform->addElement('text', 'penalty', get_string('penalty', 'gradepenalty_duedate')); + $mform->setType('penalty', PARAM_INT); + // Default value. If we are updating a rule, use the current value. + $mform->setDefault('penalty', $this->action === GRADEPENALTY_DUEDATE_ACTION_UPDATE ? + $rule->get('penalty') : $this->minpenalty); + // Required rule. + $mform->addRule('penalty', get_string('required'), 'required'); + // Help button. + $mform->addHelpButton('penalty', 'penalty', 'gradepenalty_duedate'); + + // Add buttons. + $this->add_action_buttons(); + } + + /** + * Make sure the latefor and penalty values are within the min/max values. + * + * @param $data data from the form. + * @param $files files from the form. + * @return array + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + // Validate latefor. + // Min value. + if ($data['latefor'] < $this->minlatefor) { + $errors['latefor'] = get_string('error_latefor_minvalue', 'gradepenalty_duedate', $this->minlatefor); + } + // Max value. + if ($data['latefor'] > $this->maxlatefor) { + $errors['latefor'] = get_string('error_latefor_maxvalue', 'gradepenalty_duedate', $this->maxlatefor); + } + + // Validate penalty. + // Min value. + if ($data['penalty'] < $this->minpenalty) { + $errors['penalty'] = get_string('error_penalty_minvalue', 'gradepenalty_duedate', $this->minpenalty); + } + // Max value. + if ($data['penalty'] > $this->maxpenalty) { + $errors['penalty'] = get_string('error_penalty_maxvalue', 'gradepenalty_duedate', $this->maxpenalty); + } + + return $errors; + } +} diff --git a/grade/penalty/duedate/classes/penalty_rule.php b/grade/penalty/duedate/classes/penalty_rule.php new file mode 100644 index 0000000000000..3c91e24e5a792 --- /dev/null +++ b/grade/penalty/duedate/classes/penalty_rule.php @@ -0,0 +1,224 @@ +. + +namespace gradepenalty_duedate; + +use core\persistent; + +/** + * To create/load/update/delete penalty rules. + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class penalty_rule extends persistent { + /** The table name this persistent object maps to. */ + const TABLE = 'gradepenalty_duedate_rule'; + + /** + * Return the definition of the properties of this model. + */ + protected static function define_properties() { + return [ + 'contextid' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + ], + 'latefor' => [ + 'type' => PARAM_TEXT, + 'null' => NULL_NOT_ALLOWED, + ], + 'penalty' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + ], + 'sortorder' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + 'default' => 0, + ], + ]; + } + + /** + * Function to get next rule, which has higher priority. + * + * @param int $sortorder + * @param int $contextid + */ + public static function get_next_rule($sortorder, $contextid) { + return self::get_record(['sortorder' => $sortorder + 1, 'contextid' => $contextid]); + } + + /** + * Function to get previous rule, which has lower priority. + * + * @param int $sortorder + * @param int $contextid + */ + public static function get_previous_rule($sortorder, $contextid) { + return self::get_record(['sortorder' => $sortorder - 1, 'contextid' => $contextid]); + } + + /** + * Validate sort order. + */ + protected function validate_sortorder() { + $sortorder = $this->raw_get('sortorder'); + $contextid = $this->raw_get('contextid'); + $ruleid = $this->raw_get('id'); + + // We are creating first rule. + if (empty($ruleid) && $sortorder == 0) { + $rules = self::get_records(['contextid' => $contextid]); + // There should be no existing rule. + if (!empty($rules)) { + return get_string('validation_this_is_not_first_rule', 'gradepenalty_duedate'); + } + return true; + } + + // We are updating or inserting new rule. + $newlatefor = $this->raw_get('latefor'); + $newpenalty = $this->raw_get('penalty'); + if (!empty($this->raw_get('id'))) { + // We are updating existing rule. + $previousrule = self::get_previous_rule($sortorder, $contextid); + $nextrule = self::get_next_rule($sortorder, $contextid); + } else { + // We are inserting new rule. Find the existing rule with the same sort order. + $existingrule = self::get_record(['sortorder' => $sortorder, 'contextid' => $contextid]); + // We are inserting new rule above or below the existing rule. + if ($existingrule) { + if ($newlatefor < $existingrule->get('latefor')) { + // We are inserting above. + $previousrule = self::get_previous_rule($sortorder, $contextid); + $nextrule = $existingrule; + } else { + // We are inserting below. + $previousrule = $existingrule; + $nextrule = self::get_next_rule($sortorder, $contextid); + } + } else { + // We use sort order as a reference for insertion. So this should not happen. + return get_string('validation_sort_order_is_not_valid', 'gradepenalty_duedate'); + } + } + // The order of latefor and penalty should match. + $isvalid = true; + if ($previousrule) { + $isvalid = $newlatefor > $previousrule->get('latefor') && $newpenalty > $previousrule->get('penalty'); + } + if ($nextrule) { + $isvalid = $isvalid && $newlatefor < $nextrule->get('latefor') && $newpenalty < $nextrule->get('penalty'); + } + + if (!$isvalid) { + return get_string('validation_cannot_insert_new_rule', 'gradepenalty_duedate'); + } + + return true; + } + + /** + * Update the sort order of the rules after adding new rule. + */ + protected function after_create() { + $this->update_sortorder(); + } + + /** + * Update the sort order of the rules after deletion. + * + * @param bool $result Whether or not the delete was successful. + */ + protected function after_delete($result) { + if ($result) { + $this->update_sortorder(); + } + } + + /** + * Update sort order + * + * @return void + */ + private function update_sortorder() { + global $DB; + // Update the sort order of the rules. + $rules = $DB->get_records(self::TABLE, ['contextid' => $this->raw_get('contextid')], 'latefor ASC'); + $sortorder = 1; + foreach ($rules as $rule) { + $DB->update_record(self::TABLE, (object) [ + 'id' => $rule->id, + 'sortorder' => $sortorder, + ]); + $sortorder++; + } + } + + /** + * Create first rule. + * + * @param array $data Rule data. + * @return void + */ + public static function create_first_rule($data) { + // We are creating first rule. + $newrule = new penalty_rule(0, $data); + // Set sort order to 0. This sort order will become 1 when resorting runs after creation. + $newrule->set('sortorder', 0); + $newrule->save(); + } + + /** + * Insert new rule before or after the specified rule. + * The sort order of the new rule is not '0', to distinguish it from the first rule. + * + * @param int $ruleid id of the rule which we are inserting before or after. + * @param array $data Rule data. + */ + public static function insert_rule($ruleid, $data) { + $newrule = new penalty_rule(0, $data); + $currentrule = new penalty_rule($ruleid); + // Set sort order to the same as the current rule. + // We will use this sort order to find the position of the new rule. + $newrule->set('sortorder', $currentrule->get('sortorder')); + $newrule->save(); + } + + /** + * Update an existing rule. + * + * @param int $ruleid id of the rule which we are updating. + * @param array $data Rule data. + */ + public static function update_rule($ruleid, $data) { + $rule = new penalty_rule($ruleid, $data); + $rule->save(); + } + + /** + * Delete a rule. + * + * @param int $ruleid id of the rule which we are deleting. + */ + public static function delete_rule($ruleid) { + $rule = new penalty_rule($ruleid); + $rule->delete(); + } +} diff --git a/grade/penalty/duedate/classes/privacy/provider.php b/grade/penalty/duedate/classes/privacy/provider.php new file mode 100644 index 0000000000000..bde31775827a3 --- /dev/null +++ b/grade/penalty/duedate/classes/privacy/provider.php @@ -0,0 +1,90 @@ +. + +namespace gradepenalty_duedate\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\userlist; + +/** + * Privacy Subsystem for gradepenalty_duedate implementing null_provider. + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\core_userlist_provider, + \core_privacy\local\request\plugin\provider { + + public static function get_contexts_for_userid(int $userid): contextlist + { + // TODO: Implement get_contexts_for_userid() method. + } + + public static function export_user_data(approved_contextlist $contextlist) + { + // TODO: Implement export_user_data() method. + } + + public static function delete_data_for_all_users_in_context(\context $context) + { + // TODO: Implement delete_data_for_all_users_in_context() method. + } + + public static function delete_data_for_user(approved_contextlist $contextlist) + { + // TODO: Implement delete_data_for_user() method. + } + + public static function get_users_in_context(userlist $userlist) + { + // TODO: Implement get_users_in_context() method. + } + + public static function delete_data_for_users(approved_userlist $userlist) + { + // TODO: Implement delete_data_for_users() method. + } + + /** + * Retrieve the user metadata stored by plugin. + * + * @param collection $collection Collection of metadata. + * @return collection Collection of metadata. + */ + public static function get_metadata(collection $collection): collection { + $collection->add_database_table( + 'gradepenalty_duedate_rule', + [ + 'userid' => 'privacy:metadata:gradepenalty_duedate:userid', + 'courseid' => 'privacy:metadata:gradepenalty_duedate:courseid', + 'itemtype' => 'privacy:metadata:gradepenalty_duedate:itemtype', + 'itemmodule' => 'privacy:metadata:gradepenalty_duedate:itemmodule', + 'iteminstance' => 'privacy:metadata:gradepenalty_duedate:iteminstance', + 'itemnumber' => 'privacy:metadata:gradepenalty_duedate:itemnumber', + 'duedate' => 'privacy:metadata:gradepenalty_duedate:duedate', + 'submissiondate' => 'privacy:metadata:gradepenalty_duedate:submissiondate', + 'penalty' => 'privacy:metadata:gradepenalty_duedate:penalty', + ], + 'privacy:metadata:gradepenalty_duedate_rule' + ); + } +} diff --git a/grade/penalty/duedate/classes/reportbuilder/local/entities/penalty_rule.php b/grade/penalty/duedate/classes/reportbuilder/local/entities/penalty_rule.php new file mode 100644 index 0000000000000..b723d93f64cf3 --- /dev/null +++ b/grade/penalty/duedate/classes/reportbuilder/local/entities/penalty_rule.php @@ -0,0 +1,98 @@ +. + +namespace gradepenalty_duedate\reportbuilder\local\entities; + +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\helpers\format; +use core_reportbuilder\local\report\column; +use lang_string; + +/** + * Penalty rule entity. + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class penalty_rule extends base { + + /** + * Set the default tables for the penalty rule entity. + * + * @return string[] + */ + protected function get_default_tables(): array { + return [ + 'gradepenalty_duedate_rule', + ]; + } + + /** + * Get the default entity title. + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('penaltyrule', 'gradepenalty_duedate'); + } + + /** + * Initialise the penalty rule entity. + * + * @return base + */ + public function initialise(): base { + $columns = $this->get_all_columns(); + foreach ($columns as $column) { + $this->add_column($column); + } + return $this; + } + + /** + * Get the columns for the penalty rule report. + */ + protected function get_all_columns(): array { + $penaltyrulealias = $this->get_table_alias('gradepenalty_duedate_rule'); + + // Late for column. + $columns[] = (new column( + 'latefor', + new lang_string('latefor', 'gradepenalty_duedate'), + $this->get_entity_name() + )) + ->set_type(column::TYPE_TIMESTAMP) + ->add_field($penaltyrulealias . '.latefor') + ->set_is_sortable(false) + ->add_callback(function ($value) { + return format_time($value); + }); + + // Penalty column. + $columns[] = (new column( + 'penalty', + new lang_string('penalty', 'gradepenalty_duedate'), + $this->get_entity_name() + )) + ->set_type(column::TYPE_INTEGER) + ->add_field($penaltyrulealias . '.penalty') + ->set_is_sortable(false) + ->add_callback([format::class, 'percent']); + + return $columns; + } +} diff --git a/grade/penalty/duedate/classes/reportbuilder/local/systemreports/penalty_rules.php b/grade/penalty/duedate/classes/reportbuilder/local/systemreports/penalty_rules.php new file mode 100644 index 0000000000000..72b6dda5d4d59 --- /dev/null +++ b/grade/penalty/duedate/classes/reportbuilder/local/systemreports/penalty_rules.php @@ -0,0 +1,171 @@ +. + +namespace gradepenalty_duedate\reportbuilder\local\systemreports; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../../../../lib.php'); + +use context_system; +use core_reportbuilder\local\helpers\database; +use core_reportbuilder\local\report\action; +use core_reportbuilder\system_report; +use gradepenalty_duedate\helper; +use gradepenalty_duedate\reportbuilder\local\entities\penalty_rule; +use lang_string; +use moodle_url; +use pix_icon; +use stdClass; + +/** + * System report for listing all penalty rules. + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class penalty_rules extends system_report { + /** + * Initialise the report. + */ + protected function initialise(): void { + // Penalty rule entity. + $entitymain = new penalty_rule(); + $entitymainalias = $entitymain->get_table_alias('gradepenalty_duedate_rule'); + + // Main table. + $this->set_main_table('gradepenalty_duedate_rule', $entitymainalias); + $this->add_entity($entitymain); + + // Base field required for actions. + $this->add_base_fields("{$entitymainalias}.id"); + + // SQL query. + $paramcontextid = database::generate_param_name(); + $context = $this->get_context(); + $this->add_base_condition_sql("{$entitymainalias}.contextid = :$paramcontextid", [$paramcontextid => $context->id]); + + // Fields. + $this->add_base_fields(" + {$entitymainalias}.contextid, + {$entitymainalias}.latefor, + {$entitymainalias}.penalty, + {$entitymainalias}.sortorder" + ); + + // Add content to the report. + $this->add_columns(); + $this->set_default_sort_order(); + $this->add_actions(); + } + + /** + * Add columns to the report. + */ + protected function add_columns(): void { + $columns = [ + 'penalty_rule:latefor', + 'penalty_rule:penalty', + ]; + + $this->add_columns_from_entities($columns); + } + + /** + * Set default sort order. + */ + protected function set_default_sort_order(): void { + // Ascending order, we show rules with higher priority below the rules with lower priority. + $this->set_initial_sort_column('penalty_rule:latefor', SORT_ASC); + } + + /** + * Add actions + */ + protected function add_actions(): void { + // Insert rule above. + $this->add_action((new action( + new moodle_url('/grade/penalty/duedate/edit_penalty_rule.php', [ + 'contextid' => $this->get_context()->id, + 'ruleid' => ':id', + 'action' => GRADEPENALTY_DUEDATE_ACTION_INSERT_ABOVE, + ]), + new pix_icon('t/add', ''), + [], + false, + new lang_string('insert_above', 'gradepenalty_duedate') + )) + ->add_callback(function(stdClass $row): bool { + // Do not show if there is no space to insert above. + return helper::can_insert_rule($row->id, GRADEPENALTY_DUEDATE_ACTION_INSERT_ABOVE); + }) + ); + + // Insert rule below. + $this->add_action((new action( + new moodle_url('/grade/penalty/duedate/edit_penalty_rule.php', [ + 'contextid' => $this->get_context()->id, + 'ruleid' => ':id', + 'action' => GRADEPENALTY_DUEDATE_ACTION_INSERT_BELOW, + ]), + new pix_icon('t/add', ''), + [], + false, + new lang_string('insert_below', 'gradepenalty_duedate') + )) + ->add_callback(function(stdClass $row): bool { + // Do not show if there is no space to insert below. + return helper::can_insert_rule($row->id, GRADEPENALTY_DUEDATE_ACTION_INSERT_BELOW); + }) + ); + + // Edit action. + $this->add_action((new action( + new moodle_url('/grade/penalty/duedate/edit_penalty_rule.php', [ + 'contextid' => $this->get_context()->id, + 'ruleid' => ':id', + 'action' => GRADEPENALTY_DUEDATE_ACTION_UPDATE, + ]), + new pix_icon('t/edit', ''), + [], + false, + new lang_string('edit') + ))); + + // Delete action. + $this->add_action((new action( + new moodle_url('/grade/penalty/duedate/edit_penalty_rule.php', [ + 'contextid' => $this->get_context()->id, + 'ruleid' => ':id', + 'action' => GRADEPENALTY_DUEDATE_ACTION_DELETE, + ]), + new pix_icon('t/delete', ''), + [], + false, + new lang_string('delete') + ))); + } + + /** + * Permission that can view the report. + * + * @return bool + */ + protected function can_view(): bool { + return has_capability('moodle/site:config', context_system::instance()); + } +} diff --git a/grade/penalty/duedate/db/access.php b/grade/penalty/duedate/db/access.php new file mode 100644 index 0000000000000..a7c40abf4c7d8 --- /dev/null +++ b/grade/penalty/duedate/db/access.php @@ -0,0 +1,35 @@ +. + +/** + * Grade penalty duedate caps. + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + 'gradepenalty/duedate:manage' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + ], + ], +]; diff --git a/grade/penalty/duedate/db/install.xml b/grade/penalty/duedate/db/install.xml new file mode 100644 index 0000000000000..b5e80be70c47a --- /dev/null +++ b/grade/penalty/duedate/db/install.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/grade/penalty/duedate/edit_penalty_rule.php b/grade/penalty/duedate/edit_penalty_rule.php new file mode 100644 index 0000000000000..13f7aa94ae301 --- /dev/null +++ b/grade/penalty/duedate/edit_penalty_rule.php @@ -0,0 +1,84 @@ +. + +/** + * Show form to create or update a penalty rule. + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use gradepenalty_duedate\output\form\penalty_rule_form; +use gradepenalty_duedate\penalty_rule; + +require_once(__DIR__ . '/../../../config.php'); +require_once(__DIR__ . '/lib.php'); +require_once("$CFG->libdir/adminlib.php"); + +admin_externalpage_setup('duedaterule'); + +// Page parameters. +$contextid = required_param('contextid', PARAM_INT); +$action = optional_param('action', GRADEPENALTY_DUEDATE_ACTION_CREATE, PARAM_INT); +$ruleid = optional_param('ruleid', null, PARAM_INT); + +// Return URL for redirection. +$returnurl = new moodle_url('/grade/penalty/duedate/manage_penalty_rule.php', ['contextid' => $contextid]); + +// If we are deleting a rule, do so and redirect. +if ($action === GRADEPENALTY_DUEDATE_ACTION_DELETE) { + penalty_rule::delete_rule($ruleid); + redirect($returnurl); +} + +// Form to add or edit a penalty rule. +$mform = new penalty_rule_form('', [ + 'contextid' => $contextid, + 'ruleid' => $ruleid, + 'action' => $action, +]); + +if ($mform->is_cancelled()) { + redirect($returnurl); +} else if ($mform->is_submitted() && $mform->is_validated() && ($data = $mform->get_data())) { + // Save the data. + switch ($action) { + case GRADEPENALTY_DUEDATE_ACTION_CREATE: + penalty_rule::create_first_rule($data); + break; + case GRADEPENALTY_DUEDATE_ACTION_INSERT_ABOVE: + case GRADEPENALTY_DUEDATE_ACTION_INSERT_BELOW: + penalty_rule::insert_rule($ruleid, $data); + break; + case GRADEPENALTY_DUEDATE_ACTION_UPDATE: + penalty_rule::update_rule($ruleid, $data); + break; + default: + throw new coding_exception('Invalid action'); + } + redirect($returnurl); +} + +// Show the page content. +echo $OUTPUT->header(); + +// Add heading with help text. +$title = get_string('editpenaltyrule', 'gradepenalty_duedate'); +echo $OUTPUT->heading_with_help($title, 'editpenaltyrule', 'gradepenalty_duedate'); + +$mform->display(); +echo $OUTPUT->footer(); diff --git a/grade/penalty/duedate/lang/en/gradepenalty_duedate.php b/grade/penalty/duedate/lang/en/gradepenalty_duedate.php new file mode 100644 index 0000000000000..0a53ab1ce0deb --- /dev/null +++ b/grade/penalty/duedate/lang/en/gradepenalty_duedate.php @@ -0,0 +1,48 @@ +. + +/** + * Strings for component 'gradepenalty_duedate', language 'en'. + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['addnewrule'] = 'Click here to add a penalty rule'; +$string['duedaterule'] = 'Set up penalty rules'; +$string['editpenaltyrule'] = 'Edit penalty rule'; +$string['editpenaltyrule_help'] = 'Add new or edit existing penalty rule'; +$string['error_latefor_maxvalue'] = 'Maximum value for late submission is {$a} seconds.'; +$string['error_latefor_minvalue'] = 'Minimum value for late submission is {$a} second(s).'; +$string['error_penalty_maxvalue'] = 'Maximum value for penalty is {$a} percent.'; +$string['error_penalty_minvalue'] = 'Minimum value for penalty is {$a} percent(s).'; +$string['insert_above'] = 'Insert rule above'; +$string['insert_below'] = 'Insert rule below'; +$string['latefor'] = 'Late'; +$string['latefor_help'] = 'Set the time in seconds after the due date that the penalty will be applied.'; +$string['nopenaltyrule'] = 'No penalty rule found. {$a}'; +$string['penalty'] = 'Penalty'; +$string['penalty_help'] = 'Set the penalty in percent that will be applied for late submissions.'; +$string['penaltyrule'] = 'Penalty rule'; +$string['penaltyrule_help'] = 'This page shows a list of penalty rules.'; +$string['pluginname'] = 'Penalty for late submission'; +$string['privacy:metadata'] = 'This plugin does not store any personal data.'; +$string['sortorder'] = 'Priority'; +$string['validation_cannot_insert_new_rule'] = 'Cannot insert new rule.'; +$string['validation_sort_order_is_not_valid'] = 'Sort order is not valid.'; +$string['validation_this_is_not_first_rule'] = 'Cannot create first rule. There is existing rule with the same context.'; + diff --git a/grade/penalty/duedate/lib.php b/grade/penalty/duedate/lib.php new file mode 100644 index 0000000000000..2a03c13719ac1 --- /dev/null +++ b/grade/penalty/duedate/lib.php @@ -0,0 +1,50 @@ +. + +/** + * Strings for component 'gradepenalty_duedate', language 'en'. + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** Add first rule action */ +define('GRADEPENALTY_DUEDATE_ACTION_CREATE', 0); + +/** Edit rule action */ +define('GRADEPENALTY_DUEDATE_ACTION_UPDATE', 1); + +/** Insert rule above action */ +define('GRADEPENALTY_DUEDATE_ACTION_INSERT_ABOVE', 2); + +/** Insert rule below action */ +define('GRADEPENALTY_DUEDATE_ACTION_INSERT_BELOW', 3); + +/** Delete rule action */ +define('GRADEPENALTY_DUEDATE_ACTION_DELETE', 4); + +/** Minimum late for value */ +define('GRADEPENALTY_DUEDATE_MIN_LATEFOR', 1); + +/** Maximum late for value */ +define('GRADEPENALTY_DUEDATE_MAX_LATEFOR', YEARSECS); + +/** Minimum penalty value */ +define('GRADEPENALTY_DUEDATE_MIN_PENALTY', 1); + +/** Maximum penalty value */ +define('GRADEPENALTY_DUEDATE_MAX_PENALTY', 100); diff --git a/grade/penalty/duedate/manage_penalty_rule.php b/grade/penalty/duedate/manage_penalty_rule.php new file mode 100644 index 0000000000000..05c597b1aabcb --- /dev/null +++ b/grade/penalty/duedate/manage_penalty_rule.php @@ -0,0 +1,55 @@ +. + +/** + * Site configuration settings for the gradepenalty_duedate plugin + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_reportbuilder\system_report_factory; +use gradepenalty_duedate\reportbuilder\local\systemreports\penalty_rules; + +require_once(__DIR__ . '/../../../config.php'); +require_once("$CFG->libdir/adminlib.php"); + +admin_externalpage_setup('duedaterule'); + +// Page parameters. +$contextid = required_param('contextid', PARAM_INT); + +// Start output. +echo $OUTPUT->header(); + +// Add heading with help text. +$title = get_string('duedaterule', 'gradepenalty_duedate'); +echo $OUTPUT->heading_with_help($title, 'penaltyrule', 'gradepenalty_duedate'); + +// Report for penalty rules. +$context = context::instance_by_id($contextid); +$report = system_report_factory::create(penalty_rules::class, $context); + +// Link to create a new rule if there is none. +$newruleurl = new moodle_url('/grade/penalty/duedate/edit_penalty_rule.php', ['contextid' => $contextid]); +$report->set_default_no_results_notice(new lang_string('nopenaltyrule', 'gradepenalty_duedate', + $OUTPUT->action_link($newruleurl, get_string('addnewrule', 'gradepenalty_duedate')))); + +// Show report. +echo $report->output(); + +echo $OUTPUT->footer(); diff --git a/grade/penalty/duedate/settings.php b/grade/penalty/duedate/settings.php new file mode 100644 index 0000000000000..80aef775838cf --- /dev/null +++ b/grade/penalty/duedate/settings.php @@ -0,0 +1,41 @@ +. + +/** + * Site configuration settings for the gradepenalty_duedate plugin + * + * @package gradepenalty_duedate + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig) { + // New category for the plugin. + $ADMIN->add('gradepenalty', new admin_category('gradepenalty_duedate', new lang_string('pluginname', 'gradepenalty_duedate'))); + + // External page to manage the duedate rules. + $temp = new admin_externalpage( + 'duedaterule', + get_string('duedaterule', 'gradepenalty_duedate'), + new moodle_url('/grade/penalty/duedate/manage_penalty_rule.php', ['contextid' => context_system::instance()->id]), + 'gradepenalty/duedate:manage' + ); + + // Add the external page to the plugin category. + $ADMIN->add('gradepenalty_duedate', $temp); +} diff --git a/grade/penalty/duedate/version.php b/grade/penalty/duedate/version.php new file mode 100644 index 0000000000000..e5278c6ae0ea2 --- /dev/null +++ b/grade/penalty/duedate/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version details for the duedate penalty + * + * @package gradepenalty_duedate + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2024061203; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2024060600; // Requires this Moodle version. +$plugin->component = 'gradepenalty_duedate'; // Full name of the plugin (used for diagnostics). diff --git a/grade/penalty/manage_penalty_plugins.php b/grade/penalty/manage_penalty_plugins.php new file mode 100644 index 0000000000000..388f55f672bd4 --- /dev/null +++ b/grade/penalty/manage_penalty_plugins.php @@ -0,0 +1,66 @@ +. + +/** + * Manage penalty plugins + * + * @package core_grades + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core\notification; +use core_grades\table\gradepenalty_management_table; + +require_once('../../config.php'); +require_once('../../course/lib.php'); +require_once("$CFG->libdir/adminlib.php"); +require_once("$CFG->libdir/tablelib.php"); + +admin_externalpage_setup('managepenaltyplugins'); + +$plugin = optional_param('plugin', '', PARAM_PLUGIN); +$action = optional_param('action', '', PARAM_ALPHA); + +// If Javascript is disabled, we need to handle the form submission. +if (!empty($action) && !empty($plugin) && confirm_sesskey()) { + $manager = core_plugin_manager::resolve_plugininfo_class('gradepenalty'); + $pluginname = get_string('pluginname', 'gradepenalty_' . $plugin); + + if ($action === 'disable' && $manager::enable_plugin($plugin, 0)) { + notification::add( + get_string('plugin_disabled', 'core_admin', $pluginname), + notification::SUCCESS + ); + admin_get_root(true, false); + } else if ($action === 'enable' && $manager::enable_plugin($plugin, 1)) { + notification::add( + get_string('plugin_enabled', 'core_admin', $pluginname), + notification::SUCCESS + ); + + admin_get_root(true, false); + } + + // Redirect back to the settings page. + redirect(new moodle_url('/grade/penalty/manage_penalty_plugins.php')); +} + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string("gradepenalty", 'core_grades')); +$table = new gradepenalty_management_table(); +$table->out(); +echo $OUTPUT->footer(); diff --git a/grade/penalty/upgrade.txt b/grade/penalty/upgrade.txt new file mode 100644 index 0000000000000..ac60bd9ea8534 --- /dev/null +++ b/grade/penalty/upgrade.txt @@ -0,0 +1,4 @@ +=== 4.5 === + +* New plugin type from Moodle 4.5. Any modules that support the grade penalty feature should declare its support for + FEATURE_GRADE_HAS_PENALTY. \ No newline at end of file diff --git a/lang/en/grades.php b/lang/en/grades.php index ac1620e57c379..485a5d9854c6e 100644 --- a/lang/en/grades.php +++ b/lang/en/grades.php @@ -334,6 +334,12 @@ $string['gradepass'] = 'Grade to pass'; $string['gradepass_help'] = 'This setting determines the minimum grade required to pass. The value is used in activity and course completion, and in the gradebook, where pass grades are highlighted in green and fail grades in red.'; $string['gradepassgreaterthangrade'] = 'The grade to pass can not be greater than the maximum possible grade {$a}'; +$string['gradepenalty'] = 'Grade penalty'; +$string['gradepenalty_enabled'] = 'Grade penalty.'; +$string['gradepenalty_enabled_help'] = 'If enabled, the penalty will be applied to the grades of supported modules.'; +$string['gradepenalty_general_settings'] = 'Common settings'; +$string['gradepenalty_supportedmodules'] = 'Supported modules'; +$string['gradepenalty_supportedmodules_help'] = 'Enable the grade penalty for the selected modules.'; $string['gradepointdefault'] = 'Grade point default'; $string['gradepointdefault_help'] = 'This setting determines the default value for the grade point value available in a grade item.'; $string['gradepointdefault_validateerror'] = 'This setting must be an integer between 1 and the grade point maximum.'; @@ -484,6 +490,7 @@ $string['lowest'] = 'Lowest'; $string['lowgradeletter'] = 'Low'; $string['manageoutcomes'] = 'Manage outcomes'; +$string['managepenaltyplugins'] = 'Manage penalty plugins'; $string['manualitem'] = 'Manual item'; $string['mapfrom'] = 'Map from'; $string['mapfrom_help'] = 'Select the column in the spreadsheet containing data for identifying the user, such as username, user ID or email address.'; diff --git a/lib/classes/plugininfo/gradepenalty.php b/lib/classes/plugininfo/gradepenalty.php new file mode 100644 index 0000000000000..88d74ddc90751 --- /dev/null +++ b/lib/classes/plugininfo/gradepenalty.php @@ -0,0 +1,125 @@ +. + +/** + * Defines classes used for plugin info. + * + * @package core + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\plugininfo; + +use core_component; +use moodle_url; + +/** + * Class for admin tool plugins + */ +class gradepenalty extends base { + + + /** + * Allow the plugin to be uninstalled. + * + * @return true + */ + public function is_uninstall_allowed() { + return true; + } + + /** + * Get the URL to manage the penalty plugin. + * + * @return moodle_url + */ + public static function get_manage_url() { + return new moodle_url('/grade/penalty/manage_penalty_plugins.php'); + } + + /** + * Support disabling the plugin. + * + * @return bool + */ + public static function plugintype_supports_disabling(): bool { + return true; + } + + /** + * Get the enabled plugins. + * + * @return array + */ + public static function get_enabled_plugins() { + // Get all the grade penalty plugins. + $plugins = core_component::get_plugin_list('gradepenalty'); + + // Get the enabled plugins. + $enabledplugins = []; + foreach ($plugins as $pluginname => $plugindata) { + if (get_config('gradepenalty_' . $pluginname, 'enabled')) { + $enabledplugins[$pluginname] = $plugindata; + } + } + + return $enabledplugins; + } + + /** + * Enable or disable a plugin. + * + * @param string $pluginname The name of the plugin. + * @param int $enabled Whether to enable or disable the plugin. + * @return bool + */ + public static function enable_plugin(string $pluginname, int $enabled): bool { + // Disable all other plugins, so we can have only one plugin enabled at a time. + if ($enabled) { + // Get the plugins which are enabled. + $plugins = self::get_enabled_plugins(); + + // Disable those plugins. + foreach ($plugins as $pluginname => $plugindata) { + set_config('enabled', false, 'gradepenalty_' . $pluginname); + } + } + + // Enable or disable the plugin. + set_config('enabled', $enabled, 'gradepenalty_' . $pluginname); + return true; + } + + /** + * Check if the plugin is enabled. + * + * @return bool + * @throws \dml_exception + */ + public function is_enabled(): bool { + return get_config($this->component, 'enabled') ?? false; + } + + /** + * Get the settings section name. + * Required for the settings page. + * + * @return string + */ + public function get_settings_section_name() { + return $this->component; + } +} diff --git a/lib/components.json b/lib/components.json index 651654cc583b4..b387eba0a5351 100644 --- a/lib/components.json +++ b/lib/components.json @@ -21,6 +21,7 @@ "coursereport": "course\/report", "gradeexport": "grade\/export", "gradeimport": "grade\/import", + "gradepenalty": "grade\/penalty", "gradereport": "grade\/report", "gradingform": "grade\/grading\/form", "mlbackend": "lib\/mlbackend", diff --git a/lib/gradelib.php b/lib/gradelib.php index a0e444744d1d9..0d4948d1c65ab 100644 --- a/lib/gradelib.php +++ b/lib/gradelib.php @@ -292,6 +292,12 @@ function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $dategraded, $datesubmitted, $gradegrade, $feedbackfiles, $isbulkupdate)) { $failed = true; } + + // Apply penalty if updating/inserting was successful. + if (!$failed) { + $failed = !\core_grades\local\penalty\manager::apply_penalty($courseid, $itemtype, $itemmodule, + $iteminstance, $itemnumber, $userid); + } } if ($rs) { diff --git a/lib/moodlelib.php b/lib/moodlelib.php index a68bb235f0fc6..a2163463c0a4a 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -416,6 +416,8 @@ /** True if module can provide a grade */ define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade'); +/** True if module can support grade penalty */ +define('FEATURE_GRADE_HAS_PENALTY', 'grade_has_penalty'); /** True if module supports outcomes */ define('FEATURE_GRADE_OUTCOMES', 'outcomes'); /** True if module supports advanced grading methods */