Merge branch 'master_mdl-73498' of https://github.com/daniil-berg/moodle
[moodle.git] / grade / classes / component_gradeitem.php
blob8aea26bf02aab12ce2adcfc6e6aa0a2672c12eb3
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.
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.
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
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.
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
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 /**
62 * component_gradeitem constructor.
64 * @param string $component
65 * @param context $context
66 * @param string $itemname
67 * @throws \coding_exception
69 final protected function __construct(string $component, context $context, string $itemname) {
70 $this->component = $component;
71 $this->context = $context;
72 $this->itemname = $itemname;
73 $this->itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
76 /**
77 * Fetch an instance of a specific component_gradeitem.
79 * @param string $component
80 * @param context $context
81 * @param string $itemname
82 * @return self
84 public static function instance(string $component, context $context, string $itemname): self {
85 $itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
87 $classname = "{$component}\\grades\\{$itemname}_gradeitem";
88 if (!class_exists($classname)) {
89 throw new \coding_exception("Unknown gradeitem {$itemname} for component {$classname}");
92 return $classname::load_from_context($context);
95 /**
96 * Load an instance of the current component_gradeitem based on context.
98 * @param context $context
99 * @return self
101 abstract public static function load_from_context(context $context): self;
104 * The table name used for grading.
106 * @return string
108 abstract protected function get_table_name(): string;
111 * Get the itemid for the current gradeitem.
113 * @return int
115 public function get_grade_itemid(): int {
116 return component_gradeitems::get_itemnumber_from_itemname($this->component, $this->itemname);
120 * Whether grading is enabled for this item.
122 * @return bool
124 abstract public function is_grading_enabled(): bool;
127 * Get the grade value for this instance.
128 * The itemname is translated to the relevant grade field for the activity.
130 * @return int
132 abstract protected function get_gradeitem_value(): ?int;
135 * Whether the grader can grade the gradee.
137 * @param stdClass $gradeduser The user being graded
138 * @param stdClass $grader The user who is grading
139 * @return bool
141 abstract public function user_can_grade(stdClass $gradeduser, stdClass $grader): bool;
144 * Require that the user can grade, throwing an exception if not.
146 * @param stdClass $gradeduser The user being graded
147 * @param stdClass $grader The user who is grading
148 * @throws \required_capability_exception
150 abstract public function require_user_can_grade(stdClass $gradeduser, stdClass $grader): void;
153 * Get the scale if a scale is being used.
155 * @return stdClass
157 protected function get_scale(): ?stdClass {
158 global $DB;
160 $gradetype = $this->get_gradeitem_value();
161 if ($gradetype > 0) {
162 return null;
165 // This is a scale.
166 if (null === $this->scale) {
167 $this->scale = $DB->get_record('scale', ['id' => -1 * $gradetype]);
170 return $this->scale;
174 * Check whether a scale is being used for this grade item.
176 * @return bool
178 public function is_using_scale(): bool {
179 $gradetype = $this->get_gradeitem_value();
181 return $gradetype < 0;
185 * Whether this grade item is configured to use direct grading.
187 * @return bool
189 public function is_using_direct_grading(): bool {
190 if ($this->is_using_scale()) {
191 return false;
194 if ($this->get_advanced_grading_controller()) {
195 return false;
198 return true;
202 * Whether this grade item is configured to use advanced grading.
204 * @return bool
206 public function is_using_advanced_grading(): bool {
207 if ($this->is_using_scale()) {
208 return false;
211 if ($this->get_advanced_grading_controller()) {
212 return true;
215 return false;
219 * Get the name of the advanced grading method.
221 * @return string
223 public function get_advanced_grading_method(): ?string {
224 $gradingmanager = $this->get_grading_manager();
226 if (empty($gradingmanager)) {
227 return null;
230 return $gradingmanager->get_active_method();
234 * Get the name of the component responsible for grading this gradeitem.
236 * @return string
238 public function get_grading_component_name(): ?string {
239 if (!$this->is_grading_enabled()) {
240 return null;
243 if ($method = $this->get_advanced_grading_method()) {
244 return "gradingform_{$method}";
247 return 'core_grades';
251 * Get the name of the component subtype responsible for grading this gradeitem.
253 * @return string
255 public function get_grading_component_subtype(): ?string {
256 if (!$this->is_grading_enabled()) {
257 return null;
260 if ($method = $this->get_advanced_grading_method()) {
261 return null;
264 if ($this->is_using_scale()) {
265 return 'scale';
268 return 'point';
272 * Whether decimals are allowed.
274 * @return bool
276 protected function allow_decimals(): bool {
277 return $this->get_gradeitem_value() > 0;
281 * Get the grading manager for this advanced grading definition.
283 * @return grading_manager
285 protected function get_grading_manager(): ?grading_manager {
286 require_once(__DIR__ . '/../grading/lib.php');
287 return get_grading_manager($this->context, $this->component, $this->itemname);
292 * Get the advanced grading controller if advanced grading is enabled.
294 * @return gradingform_controller
296 protected function get_advanced_grading_controller(): ?gradingform_controller {
297 $gradingmanager = $this->get_grading_manager();
299 if (empty($gradingmanager)) {
300 return null;
303 if ($gradingmethod = $gradingmanager->get_active_method()) {
304 return $gradingmanager->get_controller($gradingmethod);
307 return null;
311 * Get the list of available grade items.
313 * @return array
315 public function get_grade_menu(): array {
316 return make_grades_menu($this->get_gradeitem_value());
320 * Check whether the supplied grade is valid and throw an exception if not.
322 * @param float $grade The value being checked
323 * @throws moodle_exception
324 * @return bool
326 public function check_grade_validity(?float $grade): bool {
327 $grade = grade_floatval(unformat_float($grade));
328 if ($grade) {
329 if ($this->is_using_scale()) {
330 // Fetch all options for this scale.
331 $scaleoptions = make_menu_from_list($this->get_scale()->scale);
333 if ($grade != -1 && !array_key_exists((int) $grade, $scaleoptions)) {
334 // The selected option did not exist.
335 throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
336 'maxgrade' => count($scaleoptions),
337 'grade' => $grade,
340 } else if ($grade) {
341 $maxgrade = $this->get_gradeitem_value();
342 if ($grade > $maxgrade) {
343 // The grade is greater than the maximum possible value.
344 throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
345 'maxgrade' => $maxgrade,
346 'grade' => $grade,
348 } else if ($grade < 0) {
349 // Negative grades are not supported.
350 throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
351 'maxgrade' => $maxgrade,
352 'grade' => $grade,
358 return true;
362 * Create an empty row in the grade for the specified user and grader.
364 * @param stdClass $gradeduser The user being graded
365 * @param stdClass $grader The user who is grading
366 * @return stdClass The newly created grade record
368 abstract public function create_empty_grade(stdClass $gradeduser, stdClass $grader): stdClass;
371 * Get the grade record for the specified grade id.
373 * @param int $gradeid
374 * @return stdClass
375 * @throws \dml_exception
377 public function get_grade(int $gradeid): stdClass {
378 global $DB;
380 return $DB->get_record($this->get_table_name(), ['id' => $gradeid]);
384 * Get the grade for the specified user.
386 * @param stdClass $gradeduser The user being graded
387 * @param stdClass $grader The user who is grading
388 * @return stdClass The grade value
390 abstract public function get_grade_for_user(stdClass $gradeduser, stdClass $grader): ?stdClass;
393 * Returns the grade that should be displayed to the user.
395 * The grade does not necessarily return a float value, this method takes grade settings into considering so
396 * the correct value be shown, eg. a float vs a letter.
398 * @param stdClass $gradeduser
399 * @param stdClass $grader
400 * @return stdClass|null
402 public function get_formatted_grade_for_user(stdClass $gradeduser, stdClass $grader): ?stdClass {
403 global $DB;
405 if ($grade = $this->get_grade_for_user($gradeduser, $grader)) {
406 $gradeitem = $this->get_grade_item();
407 if (!$this->is_using_scale()) {
408 $grade->grade = !is_null($grade->grade) ? (float)$grade->grade : null; // Cast non-null values, keeping nulls.
409 $grade->usergrade = grade_format_gradevalue($grade->grade, $gradeitem);
410 $grade->maxgrade = format_float($gradeitem->grademax, $gradeitem->get_decimals());
411 // If displaying the raw grade, also display the total value.
412 if ($gradeitem->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
413 $grade->usergrade .= ' / ' . $grade->maxgrade;
415 } else {
416 $grade->usergrade = '-';
417 if ($scale = $DB->get_record('scale', ['id' => $gradeitem->scaleid])) {
418 $options = make_menu_from_list($scale->scale);
420 $gradeint = (int) $grade->grade;
421 if (isset($options[$gradeint])) {
422 $grade->usergrade = $options[$gradeint];
426 $grade->maxgrade = format_float($gradeitem->grademax, $gradeitem->get_decimals());
429 return $grade;
432 return null;
436 * Get the grade status for the specified user.
437 * If the user has a grade as defined by the implementor return true else return false.
439 * @param stdClass $gradeduser The user being graded
440 * @return bool The grade status
442 abstract public function user_has_grade(stdClass $gradeduser): bool;
445 * Get grades for all users for the specified gradeitem.
447 * @return stdClass[] The grades
449 abstract public function get_all_grades(): array;
452 * Get the grade item instance id.
454 * This is typically the cmid in the case of an activity, and relates to the iteminstance field in the grade_items
455 * table.
457 * @return int
459 abstract public function get_grade_instance_id(): int;
462 * Get the core grade item from the current component grade item.
463 * This is mainly used to access the max grade for a gradeitem
465 * @return \grade_item The grade item
467 public function get_grade_item(): \grade_item {
468 global $CFG;
469 require_once("{$CFG->libdir}/gradelib.php");
471 [$itemtype, $itemmodule] = \core_component::normalize_component($this->component);
472 $gradeitem = \grade_item::fetch([
473 'itemtype' => $itemtype,
474 'itemmodule' => $itemmodule,
475 'itemnumber' => $this->itemnumber,
476 'iteminstance' => $this->get_grade_instance_id(),
479 return $gradeitem;
483 * Create or update the grade.
485 * @param stdClass $grade
486 * @return bool Success
488 abstract protected function store_grade(stdClass $grade): bool;
491 * Create or update the grade.
493 * @param stdClass $gradeduser The user being graded
494 * @param stdClass $grader The user who is grading
495 * @param stdClass $formdata The data submitted
496 * @return bool Success
498 public function store_grade_from_formdata(stdClass $gradeduser, stdClass $grader, stdClass $formdata): bool {
499 // Require gradelib for grade_floatval.
500 require_once(__DIR__ . '/../../lib/gradelib.php');
501 $grade = $this->get_grade_for_user($gradeduser, $grader);
503 if ($this->is_using_advanced_grading()) {
504 $instanceid = $formdata->instanceid;
505 $gradinginstance = $this->get_advanced_grading_instance($grader, $grade, (int) $instanceid);
506 $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading, $grade->id);
508 if ($grade->grade == -1) {
509 // In advanced grading, a value of -1 means no data.
510 return false;
512 } else {
513 // Handle the case when grade is set to No Grade.
514 if (isset($formdata->grade)) {
515 $grade->grade = grade_floatval(unformat_float($formdata->grade));
519 return $this->store_grade($grade);
523 * Get the advanced grading instance for the specified grade entry.
525 * @param stdClass $grader The user who is grading
526 * @param stdClass $grade The row from the grade table.
527 * @param int $instanceid The instanceid of the advanced grading form
528 * @return gradingform_instance
530 public function get_advanced_grading_instance(stdClass $grader, stdClass $grade, int $instanceid = null): ?gradingform_instance {
531 $controller = $this->get_advanced_grading_controller($this->itemname);
533 if (empty($controller)) {
534 // Advanced grading not enabeld for this item.
535 return null;
538 if (!$controller->is_form_available()) {
539 // The form is not available for this item.
540 return null;
543 // Fetch the instance for the specified graderid/itemid.
544 $gradinginstance = $controller->fetch_instance(
545 (int) $grader->id,
546 (int) $grade->id,
547 $instanceid
550 // Set the allowed grade range.
551 $gradinginstance->get_controller()->set_grade_range(
552 $this->get_grade_menu(),
553 $this->allow_decimals()
556 return $gradinginstance;
560 * Sends a notification about the item being graded for the student.
562 * @param stdClass $gradeduser The user being graded
563 * @param stdClass $grader The user who is grading
565 public function send_student_notification(stdClass $gradeduser, stdClass $grader): void {
566 $contextname = $this->context->get_context_name();
567 $eventdata = new \core\message\message();
568 $eventdata->courseid = $this->context->get_course_context()->instanceid;
569 $eventdata->component = 'moodle';
570 $eventdata->name = 'gradenotifications';
571 $eventdata->userfrom = $grader;
572 $eventdata->userto = $gradeduser;
573 $eventdata->subject = get_string('gradenotificationsubject', 'grades');
574 $eventdata->fullmessage = get_string('gradenotificationmessage', 'grades', $contextname);
575 $eventdata->contexturl = $this->context->get_url();
576 $eventdata->contexturlname = $contextname;
577 $eventdata->fullmessageformat = FORMAT_HTML;
578 $eventdata->fullmessagehtml = '';
579 $eventdata->smallmessage = '';
580 $eventdata->notification = 1;
581 message_send($eventdata);