Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core_grades;
18
 
19
use core\context;
20
use core\plugininfo\gradepenalty;
21
use core_plugin_manager;
22
use grade_grade;
23
use grade_item;
24
use moodle_url;
25
use navigation_node;
26
use pix_icon;
27
use settings_navigation;
28
use stdClass;
29
 
30
/**
31
 * Manager class for grade penalty.
32
 *
33
 * @package   core_grades
34
 * @copyright 2024 Catalyst IT Australia Pty Ltd
35
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36
 */
37
class penalty_manager {
38
    /**
39
     * List the modules that support the grade penalty feature.
40
     *
41
     * @return array list of supported modules.
42
     */
43
    public static function get_supported_modules(): array {
44
        $plugintype = 'mod';
45
        $mods = \core_component::get_plugin_list($plugintype);
46
        $supported = [];
47
        foreach ($mods as $mod => $plugindir) {
48
            if (plugin_supports($plugintype, $mod, FEATURE_GRADE_HAS_PENALTY)) {
49
                $supported[] = $mod;
50
            }
51
        }
52
        return $supported;
53
    }
54
 
55
    /**
56
     * List the modules that currently have the grade penalty feature enabled.
57
     *
58
     * @return array List of enabled modules.
59
     */
60
    public static function get_enabled_modules(): array {
61
        return array_filter(explode(',', get_config('core', 'gradepenalty_enabledmodules')));
62
    }
63
 
64
    /**
65
     * Enable the grade penalty feature for a module.
66
     *
67
     * @param string $module The module name (e.g. 'assign').
68
     */
69
    public static function enable_module(string $module): void {
70
        self::enable_modules([$module]);
71
    }
72
 
73
    /**
74
     * Enable the grade penalty feature for multiple modules.
75
     *
76
     * @param array $modules List of module names.
77
     */
78
    public static function enable_modules(array $modules): void {
79
        $result = array_unique(array_merge(self::get_enabled_modules(), $modules));
80
        set_config('gradepenalty_enabledmodules', implode(',', $result));
81
    }
82
 
83
    /**
84
     * Disable the grade penalty feature for a module.
85
     *
86
     * @param string $module The module name (e.g. 'assign').
87
     */
88
    public static function disable_module(string $module): void {
89
        self::disable_modules([$module]);
90
    }
91
 
92
    /**
93
     * Disable the grade penalty feature for multiple modules.
94
     *
95
     * @param array $modules List of module names.
96
     */
97
    public static function disable_modules(array $modules): void {
98
        $result = array_diff(self::get_enabled_modules(), $modules);
99
        set_config('gradepenalty_enabledmodules', implode(',', $result));
100
    }
101
 
102
    /**
103
     * Check if the module has the grade penalty feature enabled.
104
     *
105
     * @param string $module The module name (e.g. 'assign').
106
     * @return bool Whether grade penalties are enabled for the module.
107
     */
108
    public static function is_penalty_enabled_for_module(string $module): bool {
109
        return in_array($module, self::get_enabled_modules());
110
    }
111
 
112
    /**
113
     * Whether the grade penalty feature is enabled for a grade.
114
     *
115
     * @param grade_grade $grade
116
     * @return bool
117
     */
118
    private static function is_penalty_enabled_for_grade(grade_grade $grade): bool {
119
        if (empty($grade)) {
120
            return false;
121
        }
122
 
123
        $grademin = $grade->get_grade_min();
124
 
125
        // No penalty for minimum grades.
126
        if ($grade->rawgrade <= $grademin) {
127
            return false;
128
        }
129
 
130
        if ($grade->finalgrade <= $grademin) {
131
            return false;
132
        }
133
 
134
        // No penalty for overridden grades.
135
        // We may need a separate setting to allow grade penalties for overridden grades.
136
        if (!empty($grade->overridden)) {
137
            return false;
138
        }
139
 
140
        // No penalty for locked grades.
141
        if (!empty($grade->locked)) {
142
            return false;
143
        }
144
 
145
        return true;
146
    }
147
 
148
    /**
149
     * Calculate grade penalties for a user and their grade via the enabled penalty plugins.
150
     *
151
     * @param penalty_container $container The penalty container.
152
     * @return penalty_container The penalty container with the calculated penalties.
153
     */
154
    private static function calculate_penalties(penalty_container $container): penalty_container {
155
        // Iterate through all the penalty plugins to calculate the total penalty.
156
        foreach (core_plugin_manager::instance()->get_plugins_of_type('gradepenalty') as $pluginname => $plugin) {
157
            if (gradepenalty::is_plugin_enabled($pluginname)) {
158
                $classname = "\\gradepenalty_{$pluginname}\\penalty_calculator";
159
                if (class_exists($classname)) {
160
                    $classname::calculate_penalty($container);
161
                }
162
            }
163
        }
164
        // Returning the container is not strictly necessary but makes it clear the container is being modified.
165
        return $container;
166
    }
167
 
168
    /**
169
     * Apply grade penalties to a user.
170
     *
171
     * Grade penalties are determined by the enabled penalty plugin.
172
     * This function should be called each time a module creates or updates a grade item for a user.
173
     *
174
     * @param int $userid The user ID
175
     * @param grade_item $gradeitem grade item
176
     * @param int $submissiondate submission date
177
     * @param int $duedate due date
178
     * @param bool $previewonly do not update the grade if true, only return the penalty
179
     * @return penalty_container Information about the applied penalty.
180
     */
181
    public static function apply_grade_penalty_to_user(
182
        int $userid,
183
        grade_item $gradeitem,
184
        int $submissiondate,
185
        int $duedate,
186
        bool $previewonly = false
187
    ): penalty_container {
188
 
189
        try {
190
            $container = self::apply_penalty($userid, $gradeitem, $submissiondate, $duedate, $previewonly);
191
        } catch (\core\exception\moodle_exception $e) {
192
            debugging($e->getMessage(), DEBUG_DEVELOPER);
193
        }
194
        return $container;
195
    }
196
 
197
    /**
198
     * Fetch the penalty for a user based on the submission date and due date and deduct marks from the grade item accordingly.
199
     *
200
     * @param int $userid The user ID.
201
     * @param grade_item $gradeitem The grade item.
202
     * @param int $submissiondate The date and time of the user submission.
203
     * @param int $duedate The date and time the submission is due.
204
     * @param bool $previewonly If true, the grade will not be updated.
205
     * @return penalty_container The penalty container containing information about the applied penalty.
206
     */
207
    private static function apply_penalty(
208
        int $userid,
209
        grade_item $gradeitem,
210
        int $submissiondate,
211
        int $duedate,
212
        bool $previewonly = false
213
    ): penalty_container {
214
 
215
        // Get the grade and create a penalty container.
216
        $grade = $gradeitem->get_grade($userid);
217
        $container = new penalty_container($gradeitem, $grade, $submissiondate, $duedate);
218
 
219
        // Do not apply penalties if the module is disabled.
220
        if (!self::is_penalty_enabled_for_module($gradeitem->itemmodule)) {
221
            return $container;
222
        }
223
 
224
        // Do not apply penalties if the grade is not eligible.
225
        if (!self::is_penalty_enabled_for_grade($grade)) {
226
            return $container;
227
        }
228
 
229
        // Call all penalty plugins to calculate the penalty.
230
        $container = self::calculate_penalties($container);
231
 
232
        // Update the grade if not in preview mode.
233
        if (!$previewonly) {
234
            // Update the raw grade and store the deducted mark.
235
            $gradeitem->update_raw_grade($userid, $container->get_grade_after_penalties(), 'gradepenalty');
236
            $gradeitem->update_deducted_mark($userid, $container->get_penalty());
237
        }
238
 
239
        return $container;
240
    }
241
 
242
    /**
243
     * Returns the penalty indicator HTML code if a penalty is applied to the grade.
244
     * Otherwise, returns an empty string.
245
     *
246
     * @param grade_grade $grade Grade object
247
     * @return string HTML code for penalty indicator
248
     */
249
    public static function show_penalty_indicator(grade_grade $grade): string {
250
        global $PAGE;
251
 
252
        // Show penalty indicator if penalty is greater than 0.
253
        if ($grade->is_penalty_applied_to_final_grade()) {
254
            $indicator = new \core_grades\output\penalty_indicator(2, $grade);
255
            $renderer = $PAGE->get_renderer('core_grades');
256
            return $renderer->render_penalty_indicator($indicator);
257
        }
258
 
259
        return '';
260
    }
261
 
262
    /**
263
     * Allow penalty plugin to extend course navigation.
264
     *
265
     * @param navigation_node $navigation The navigation node
266
     * @param stdClass $course The course object
267
     * @param context $coursecontext The course context
268
     */
269
    public static function extend_navigation_course(navigation_node $navigation,
270
                                                    stdClass $course,
271
                                                    context $coursecontext): void {
272
        // Create new navigation node for grade penalty.
273
        $penaltynav = $navigation->add(get_string('gradepenalty', 'core_grades'),
274
            new moodle_url('/grade/penalty/view.php', ['contextid' => $coursecontext->id]),
275
            navigation_node::TYPE_CONTAINER, null, 'gradepenalty', new pix_icon('i/grades', ''));
276
 
277
        // Allow plugins to extend the navigation.
278
        $pluginfunctions = get_plugin_list_with_function('gradepenalty', 'extend_navigation_course');
279
        foreach ($pluginfunctions as $plugin => $function) {
280
            if (gradepenalty::is_plugin_enabled($plugin)) {
281
                $function($penaltynav, $course, $coursecontext);
282
            }
283
        }
284
 
285
        // Do not display the node if there are no children.
286
        if (!$penaltynav->has_children()) {
287
            $penaltynav->remove();
288
        }
289
    }
290
 
291
    /**
292
     * Allow penalty plugin to extend navigation module.
293
     *
294
     * @param settings_navigation $settings The settings navigation object
295
     * @param navigation_node $navref The navigation node
296
     * @return void
297
     */
298
    public static function extend_navigation_module(settings_navigation $settings, navigation_node $navref): void {
299
        $context = $settings->get_page()->context;
300
        $cm = $settings->get_page()->cm;
301
 
302
        // Create new navigation node for grade penalty.
303
        $penaltynav = $navref->add(get_string('gradepenalty', 'core_grades'),
304
            new moodle_url('/grade/penalty/view.php', ['contextid' => $context->id, 'cm' => $cm->id]),
305
            navigation_node::TYPE_CONTAINER, null, 'gradepenalty', new pix_icon('i/grades', ''));
306
 
307
        // Allow plugins to extend the navigation.
308
        $pluginfunctions = get_plugin_list_with_function('gradepenalty', 'extend_navigation_module');
309
        foreach ($pluginfunctions as $plugin => $function) {
310
            if (gradepenalty::is_plugin_enabled($plugin) && self::is_penalty_enabled_for_module($cm->modname)) {
311
                $function($penaltynav, $cm);
312
            }
313
        }
314
 
315
        // Do not display the node if there are no children.
316
        if (!$penaltynav->has_children()) {
317
            $penaltynav->remove();
318
        }
319
    }
320
 
321
    /**
322
     * Recalculate grade penalties
323
     *
324
     * @param context $context The context
325
     * @param int $usermodified The user who triggered the recalculation
326
     * return void
327
     */
328
    public static function recalculate_penalty(context $context, int $usermodified = 0): void {
329
        if ($usermodified == 0) {
330
            global $USER;
331
            $usermodified = $USER->id;
332
        }
333
 
334
        // Get enabled modules.
335
        $enabledmodules = self::get_enabled_modules();
336
 
337
        foreach ($enabledmodules as $module) {
338
            // If it is in a module context, make sure the module is the same as the enabled module.
339
            if ($context->contextlevel == CONTEXT_MODULE) {
340
                $cmid = $context->instanceid;
341
                $cm = get_coursemodule_from_id($module, $cmid);
342
                if (empty($cm)) {
343
                    continue;
344
                }
345
            }
346
 
347
            // Check if the module supports has penalty recalculator class.
348
            $classname = "\\mod_{$module}\\penalty_recalculator";
349
            if (class_exists($classname)) {
350
                $classname::recalculate_penalty($context, $usermodified);
351
            }
352
        }
353
    }
354
}