AutorÃa | Ultima modificación | Ver Log |
<?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 core_grades;
use core\context;
use core\plugininfo\gradepenalty;
use core_plugin_manager;
use grade_grade;
use grade_item;
use moodle_url;
use navigation_node;
use pix_icon;
use settings_navigation;
use stdClass;
/**
* Manager class for grade penalty.
*
* @package core_grades
* @copyright 2024 Catalyst IT Australia Pty Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class penalty_manager {
/**
* List the modules that support the grade penalty feature.
*
* @return array list of supported modules.
*/
public static function get_supported_modules(): array {
$plugintype = 'mod';
$mods = \core_component::get_plugin_list($plugintype);
$supported = [];
foreach ($mods as $mod => $plugindir) {
if (plugin_supports($plugintype, $mod, FEATURE_GRADE_HAS_PENALTY)) {
$supported[] = $mod;
}
}
return $supported;
}
/**
* List the modules that currently have the grade penalty feature enabled.
*
* @return array List of enabled modules.
*/
public static function get_enabled_modules(): array {
return array_filter(explode(',', get_config('core', 'gradepenalty_enabledmodules')));
}
/**
* Enable the grade penalty feature for a module.
*
* @param string $module The module name (e.g. 'assign').
*/
public static function enable_module(string $module): void {
self::enable_modules([$module]);
}
/**
* Enable the grade penalty feature for multiple modules.
*
* @param array $modules List of module names.
*/
public static function enable_modules(array $modules): void {
$result = array_unique(array_merge(self::get_enabled_modules(), $modules));
set_config('gradepenalty_enabledmodules', implode(',', $result));
}
/**
* Disable the grade penalty feature for a module.
*
* @param string $module The module name (e.g. 'assign').
*/
public static function disable_module(string $module): void {
self::disable_modules([$module]);
}
/**
* Disable the grade penalty feature for multiple modules.
*
* @param array $modules List of module names.
*/
public static function disable_modules(array $modules): void {
$result = array_diff(self::get_enabled_modules(), $modules);
set_config('gradepenalty_enabledmodules', implode(',', $result));
}
/**
* Check if the module has the grade penalty feature enabled.
*
* @param string $module The module name (e.g. 'assign').
* @return bool Whether grade penalties are enabled for the module.
*/
public static function is_penalty_enabled_for_module(string $module): bool {
return in_array($module, self::get_enabled_modules());
}
/**
* Whether the grade penalty feature is enabled for a grade.
*
* @param grade_grade $grade
* @return bool
*/
private static function is_penalty_enabled_for_grade(grade_grade $grade): bool {
if (empty($grade)) {
return false;
}
$grademin = $grade->get_grade_min();
// No penalty for minimum grades.
if ($grade->rawgrade <= $grademin) {
return false;
}
if ($grade->finalgrade <= $grademin) {
return false;
}
// No penalty for overridden grades.
// We may need a separate setting to allow grade penalties for overridden grades.
if (!empty($grade->overridden)) {
return false;
}
// No penalty for locked grades.
if (!empty($grade->locked)) {
return false;
}
return true;
}
/**
* Calculate grade penalties for a user and their grade via the enabled penalty plugins.
*
* @param penalty_container $container The penalty container.
* @return penalty_container The penalty container with the calculated penalties.
*/
private static function calculate_penalties(penalty_container $container): penalty_container {
// Iterate through all the penalty plugins to calculate the total penalty.
foreach (core_plugin_manager::instance()->get_plugins_of_type('gradepenalty') as $pluginname => $plugin) {
if (gradepenalty::is_plugin_enabled($pluginname)) {
$classname = "\\gradepenalty_{$pluginname}\\penalty_calculator";
if (class_exists($classname)) {
$classname::calculate_penalty($container);
}
}
}
// Returning the container is not strictly necessary but makes it clear the container is being modified.
return $container;
}
/**
* Apply grade penalties to a user.
*
* Grade penalties are determined by the enabled penalty plugin.
* This function should be called each time a module creates or updates a grade item for a user.
*
* @param int $userid The user ID
* @param grade_item $gradeitem grade item
* @param int $submissiondate submission date
* @param int $duedate due date
* @param bool $previewonly do not update the grade if true, only return the penalty
* @return penalty_container Information about the applied penalty.
*/
public static function apply_grade_penalty_to_user(
int $userid,
grade_item $gradeitem,
int $submissiondate,
int $duedate,
bool $previewonly = false
): penalty_container {
try {
$container = self::apply_penalty($userid, $gradeitem, $submissiondate, $duedate, $previewonly);
} catch (\core\exception\moodle_exception $e) {
debugging($e->getMessage(), DEBUG_DEVELOPER);
}
return $container;
}
/**
* Fetch the penalty for a user based on the submission date and due date and deduct marks from the grade item accordingly.
*
* @param int $userid The user ID.
* @param grade_item $gradeitem The grade item.
* @param int $submissiondate The date and time of the user submission.
* @param int $duedate The date and time the submission is due.
* @param bool $previewonly If true, the grade will not be updated.
* @return penalty_container The penalty container containing information about the applied penalty.
*/
private static function apply_penalty(
int $userid,
grade_item $gradeitem,
int $submissiondate,
int $duedate,
bool $previewonly = false
): penalty_container {
// Get the grade and create a penalty container.
$grade = $gradeitem->get_grade($userid);
$container = new penalty_container($gradeitem, $grade, $submissiondate, $duedate);
// Do not apply penalties if the module is disabled.
if (!self::is_penalty_enabled_for_module($gradeitem->itemmodule)) {
return $container;
}
// Do not apply penalties if the grade is not eligible.
if (!self::is_penalty_enabled_for_grade($grade)) {
return $container;
}
// Call all penalty plugins to calculate the penalty.
$container = self::calculate_penalties($container);
// Update the grade if not in preview mode.
if (!$previewonly) {
// Update the raw grade and store the deducted mark.
$gradeitem->update_raw_grade($userid, $container->get_grade_after_penalties(), 'gradepenalty');
$gradeitem->update_deducted_mark($userid, $container->get_penalty());
}
return $container;
}
/**
* Returns the penalty indicator HTML code if a penalty is applied to the grade.
* Otherwise, returns an empty string.
*
* @param grade_grade $grade Grade object
* @return string HTML code for penalty indicator
*/
public static function show_penalty_indicator(grade_grade $grade): string {
global $PAGE;
// Show penalty indicator if penalty is greater than 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);
}
return '';
}
/**
* Allow penalty plugin to extend course navigation.
*
* @param navigation_node $navigation The navigation node
* @param stdClass $course The course object
* @param context $coursecontext The course context
*/
public static function extend_navigation_course(navigation_node $navigation,
stdClass $course,
context $coursecontext): void {
// Create new navigation node for grade penalty.
$penaltynav = $navigation->add(get_string('gradepenalty', 'core_grades'),
new moodle_url('/grade/penalty/view.php', ['contextid' => $coursecontext->id]),
navigation_node::TYPE_CONTAINER, null, 'gradepenalty', new pix_icon('i/grades', ''));
// Allow plugins to extend the navigation.
$pluginfunctions = get_plugin_list_with_function('gradepenalty', 'extend_navigation_course');
foreach ($pluginfunctions as $plugin => $function) {
if (gradepenalty::is_plugin_enabled($plugin)) {
$function($penaltynav, $course, $coursecontext);
}
}
// Do not display the node if there are no children.
if (!$penaltynav->has_children()) {
$penaltynav->remove();
}
}
/**
* Allow penalty plugin to extend navigation module.
*
* @param settings_navigation $settings The settings navigation object
* @param navigation_node $navref The navigation node
* @return void
*/
public static function extend_navigation_module(settings_navigation $settings, navigation_node $navref): void {
$context = $settings->get_page()->context;
$cm = $settings->get_page()->cm;
// Create new navigation node for grade penalty.
$penaltynav = $navref->add(get_string('gradepenalty', 'core_grades'),
new moodle_url('/grade/penalty/view.php', ['contextid' => $context->id, 'cm' => $cm->id]),
navigation_node::TYPE_CONTAINER, null, 'gradepenalty', new pix_icon('i/grades', ''));
// Allow plugins to extend the navigation.
$pluginfunctions = get_plugin_list_with_function('gradepenalty', 'extend_navigation_module');
foreach ($pluginfunctions as $plugin => $function) {
if (gradepenalty::is_plugin_enabled($plugin) && self::is_penalty_enabled_for_module($cm->modname)) {
$function($penaltynav, $cm);
}
}
// Do not display the node if there are no children.
if (!$penaltynav->has_children()) {
$penaltynav->remove();
}
}
/**
* Recalculate grade penalties
*
* @param context $context The context
* @param int $usermodified The user who triggered the recalculation
* return void
*/
public static function recalculate_penalty(context $context, int $usermodified = 0): void {
if ($usermodified == 0) {
global $USER;
$usermodified = $USER->id;
}
// Get enabled modules.
$enabledmodules = self::get_enabled_modules();
foreach ($enabledmodules as $module) {
// If it is in a module context, make sure the module is the same as the enabled module.
if ($context->contextlevel == CONTEXT_MODULE) {
$cmid = $context->instanceid;
$cm = get_coursemodule_from_id($module, $cmid);
if (empty($cm)) {
continue;
}
}
// Check if the module supports has penalty recalculator class.
$classname = "\\mod_{$module}\\penalty_recalculator";
if (class_exists($classname)) {
$classname::recalculate_penalty($context, $usermodified);
}
}
}
}