MDL-66809 core_grades: Implement scale-based marking
[moodle.git] / grade / classes / component_gradeitem.php
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/>.
17 /**
18  * Compontent definition of a gradeitem.
19  *
20  * @package   core_grades
21  * @copyright Andrew Nicols <andrew@nicols.co.uk>
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 declare(strict_types = 1);
27 namespace core_grades;
29 use context;
30 use gradingform_controller;
31 use gradingform_instance;
32 use moodle_exception;
33 use stdClass;
34 use grade_item as core_gradeitem;
35 use grading_manager;
37 /**
38  * Compontent definition of a gradeitem.
39  *
40  * @package   core_grades
41  * @copyright Andrew Nicols <andrew@nicols.co.uk>
42  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  */
44 abstract class component_gradeitem {
46     /** @var array The scale data for the current grade item */
47     protected $scale;
49     /** @var string The component */
50     protected $component;
52     /** @var context The context for this activity */
53     protected $context;
55     /** @var string The item name */
56     protected $itemname;
58     /** @var int The grade itemnumber */
59     protected $itemnumber;
61     final protected function __construct(string $component, context $context, string $itemname) {
62         $this->component = $component;
63         $this->context = $context;
64         $this->itemname = $itemname;
65         $this->itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
66     }
68     /**
69      * Fetch an instance of a specific component_gradeitem.
70      *
71      * @param string $component
72      * @param context $context
73      * @param string $itemname
74      * @return self
75      */
76     public static function instance(string $component, context $context, string $itemname): self {
77         $itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
79         $classname = "{$component}\\grades\\{$itemname}_gradeitem";
80         if (!class_exists($classname)) {
81             throw new coding_exception("Unknown gradeitem {$itemname} for component {$classname}");
82         }
84         return $classname::load_from_context($context);
85     }
87     /**
88      * Load an instance of the current component_gradeitem based on context.
89      *
90      * @param context $context
91      * @return self
92      */
93     abstract public static function load_from_context(context $context): self;
95     /**
96      * The table name used for grading.
97      *
98      * @return string
99      */
100     abstract protected function get_table_name(): string;
102     /**
103      * Get the itemid for the current gradeitem.
104      *
105      * @return int
106      */
107     public function get_grade_itemid(): int {
108         return component_gradeitems::get_itemnumber_from_itemname($this->component, $this->itemname);
109     }
111     /**
112      * Whether grading is enabled for this item.
113      *
114      * @return bool
115      */
116     abstract public function is_grading_enabled(): bool;
118     /**
119      * Get the grade value for this instance.
120      * The itemname is translated to the relevant grade field for the activity.
121      *
122      * @return int
123      */
124     abstract protected function get_gradeitem_value(): ?int;
126     /**
127      * Whether the grader can grade the gradee.
128      *
129      * @param stdClass $gradeduser The user being graded
130      * @param stdClass $grader The user who is grading
131      * @return bool
132      */
133     abstract public function user_can_grade(stdClass $gradeduser, stdClass $grader): bool;
135     /**
136      * Require that the user can grade, throwing an exception if not.
137      *
138      * @param stdClass $gradeduser The user being graded
139      * @param stdClass $grader The user who is grading
140      * @throws required_capability_exception
141      */
142     abstract public function require_user_can_grade(stdClass $gradeduser, stdClass $grader): void;
144     /**
145      * Get the scale if a scale is being used.
146      *
147      * @return stdClass
148      */
149     protected function get_scale(): ?stdClass {
150         global $DB;
152         $gradetype = $this->get_gradeitem_value();
153         if ($gradetype > 0) {
154             return null;
155         }
157         // This is a scale.
158         if (null === $this->scale) {
159             $this->scale = $DB->get_record('scale', ['id' => -1 * $gradetype]);
160         }
162         return $this->scale;
163     }
165     /**
166      * Check whether a scale is being used for this grade item.
167      *
168      * @return bool
169      */
170     public function is_using_scale(): bool {
171         $gradetype = $this->get_gradeitem_value();
173         return $gradetype < 0;
174     }
176     /**
177      * Whether this grade item is configured to use direct grading.
178      *
179      * @return bool
180      */
181     public function is_using_direct_grading(): bool {
182         if ($this->is_using_scale()) {
183             return false;
184         }
186         if ($this->get_advanced_grading_controller()) {
187             return false;
188         }
190         return true;
191     }
193     /**
194      * Whether this grade item is configured to use advanced grading.
195      *
196      * @return bool
197      */
198     public function is_using_advanced_grading(): bool {
199         if ($this->is_using_scale()) {
200             return false;
201         }
203         if ($this->get_advanced_grading_controller()) {
204             return true;
205         }
207         return false;
208     }
210     /**
211      * Get the name of the advanced grading method.
212      *
213      * @return string
214      */
215     public function get_advanced_grading_method(): ?string {
216         $gradingmanager = $this->get_grading_manager();
218         if (empty($gradingmanager)) {
219             return null;
220         }
222         return $gradingmanager->get_active_method();
223     }
225     /**
226      * Get the name of the component responsible for grading this gradeitem.
227      *
228      * @return string
229      */
230     public function get_grading_component_name(): ?string {
231         if (!$this->is_grading_enabled()) {
232             return null;
233         }
235         if ($method = $this->get_advanced_grading_method()) {
236             return "gradingform_{$method}";
237         }
239         return 'core_grades';
240     }
242     /**
243      * Get the name of the component subtype responsible for grading this gradeitem.
244      *
245      * @return string
246      */
247     public function get_grading_component_subtype(): ?string {
248         if (!$this->is_grading_enabled()) {
249             return null;
250         }
252         if ($method = $this->get_advanced_grading_method()) {
253             return null;
254         }
256         if ($this->is_using_scale()) {
257             return 'scale';
258         }
260         return 'point';
261     }
263     /**
264      * Whether decimals are allowed.
265      *
266      * @return bool
267      */
268     protected function allow_decimals(): bool {
269         return $this->get_gradeitem_value() > 0;
270     }
272     /**
273      * Get the grading manager for this advanced grading definition.
274      *
275      * @return grading_manager
276      */
277     protected function get_grading_manager(): ?grading_manager {
278         require_once(__DIR__ . '/../grading/lib.php');
279         return get_grading_manager($this->context, $this->component, $this->itemname);
281     }
283     /**
284      * Get the advanced grading controller if advanced grading is enabled.
285      *
286      * @return gradingform_controller
287      */
288     protected function get_advanced_grading_controller(): ?gradingform_controller {
289         $gradingmanager = $this->get_grading_manager();
291         if (empty($gradingmanager)) {
292             return null;
293         }
295         if ($gradingmethod = $gradingmanager->get_active_method()) {
296             return $gradingmanager->get_controller($gradingmethod);
297         }
299         return null;
300     }
302     /**
303      * Get the list of available grade items.
304      *
305      * @return array
306      */
307     public function get_grade_menu(): array {
308         return make_grades_menu($this->get_gradeitem_value());
309     }
311     /**
312      * Check whether the supplied grade is valid and throw an exception if not.
313      *
314      * @param float $grade The value being checked
315      * @throws moodle_exception
316      * @return bool
317      */
318     public function check_grade_validity(?float $grade): bool {
319         $grade = grade_floatval(unformat_float($grade));
320         if ($grade) {
321             if ($this->is_using_scale()) {
322                 // Fetch all options for this scale.
323                 $scaleoptions = make_menu_from_list($this->get_scale()->scale);
325                 if ($grade != -1 && !array_key_exists((int) $grade, $scaleoptions)) {
326                     // The selected option did not exist.
327                     throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
328                         'maxgrade' => count($scaleoptions),
329                         'grade' => $grade,
330                     ]);
331                 }
332             } else if ($grade) {
333                 $maxgrade = $this->get_gradeitem_value();
334                 if ($grade > $maxgrade) {
335                     // The grade is greater than the maximum possible value.
336                     throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
337                         'maxgrade' => $maxgrade,
338                         'grade' => $grade,
339                     ]);
340                 } else if ($grade < 0) {
341                     // Negative grades are not supported.
342                     throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
343                         'maxgrade' => $maxgrade,
344                         'grade' => $grade,
345                     ]);
346                 }
347             }
348         }
350         return true;
351     }
353     /**
354      * Create an empty row in the grade for the specified user and grader.
355      *
356      * @param stdClass $gradeduser The user being graded
357      * @param stdClass $grader The user who is grading
358      * @return stdClass The newly created grade record
359      */
360     abstract public function create_empty_grade(stdClass $gradeduser, stdClass $grader): stdClass;
362     /**
363      * Get the grade record for the specified grade id.
364      *
365      * @param int $gradeid
366      * @return stdClass
367      */
368     public function get_grade(int $gradeid): stdClass {
369         global $DB;
371         $grade = $DB->get_record($this->get_table_name(), ['id' => $gradeid]);
373         return $grade ?: null;
374     }
376     /**
377      * Get the grade for the specified user.
378      *
379      * @param stdClass $gradeduser The user being graded
380      * @param stdClass $grader The user who is grading
381      * @return stdClass The grade value
382      */
383     abstract public function get_grade_for_user(stdClass $gradeduser, stdClass $grader): ?stdClass;
385     /**
386      * Get grades for all users for the specified gradeitem.
387      *
388      * @param int $itemnumber The specific grade item to fetch for the user
389      * @return stdClass[] The grades
390      */
391     abstract public function get_all_grades(): array;
393     /**
394      * Create or update the grade.
395      *
396      * @param stdClass $grade
397      * @return bool Success
398      */
399     abstract protected function store_grade(stdClass $grade): bool;
401     /**
402      * Create or update the grade.
403      *
404      * @param stdClass $gradeduser The user being graded
405      * @param stdClass $grader The user who is grading
406      * @param stdClass $formdata The data submitted
407      * @return bool Success
408      */
409     public function store_grade_from_formdata(stdClass $gradeduser, stdClass $grader, stdClass $formdata): bool {
410         // Require gradelib for grade_floatval.
411         require_once(__DIR__ . '/../../lib/gradelib.php');
412         $grade = $this->get_grade_for_user($gradeduser, $grader);
414         if ($this->is_using_advanced_grading()) {
415             $instanceid = $formdata->instanceid;
416             $gradinginstance = $this->get_advanced_grading_instance($grader, $grade, (int) $instanceid);
417             $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading, $grade->id);
418         } else {
419             // Handle the case when grade is set to No Grade.
420             if (isset($formdata->grade)) {
421                 $grade->grade = grade_floatval(unformat_float($formdata->grade));
422             }
423         }
425         return $this->store_grade($grade);
426     }
428     /**
429      * Get the advanced grading instance for the specified grade entry.
430      *
431      * @param stdClass $grader The user who is grading
432      * @param stdClass $grade The row from the grade table.
433      * @param int $instanceid The instanceid of the advanced grading form
434      * @return gradingform_instance
435      */
436     public function get_advanced_grading_instance(stdClass $grader, stdClass $grade, int $instanceid = null): ?gradingform_instance {
437         $controller = $this->get_advanced_grading_controller($this->itemname);
439         if (empty($controller)) {
440             // Advanced grading not enabeld for this item.
441             return null;
442         }
444         if (!$controller->is_form_available()) {
445             // The form is not available for this item.
446             return null;
447         }
449         // Fetch the instance for the specified graderid/itemid.
450         $gradinginstance = $controller->fetch_instance(
451             (int) $grader->id,
452             (int) $grade->id,
453             $instanceid
454         );
456         // Set the allowed grade range.
457         $gradinginstance->get_controller()->set_grade_range(
458             $this->get_grade_menu(),
459             $this->allow_decimals()
460         );
462         return $gradinginstance;
463     }