moodle/grade/classes/component_gradeitem.php
Eloy Lafuente (stronk7) e2821bf1ce MDL-73824 gradebook: Stricter float check to some gradelib functions
It has been detected that, right now, some localised floats are
being passed to those functions (say comma separator, say thousands)
and that's leading to all sort of problems later when comparing,
processing or storing those "wrong-floats" (user entered).

This just makes all those functions to be stricter, so any attempt
of passing to them a wrong float will fail with a clear TypeError.

Any existing case must be converted to a corrrect (X.Y) format, using
unformat_float() or PARAM_LOCALISEDFLOAT before any processing.

Localised floats cannot be used.

Also, fix all the places where those functions are called from
files having strict_types enabled because, with that, now float-like
strings are not accepted any more. Luckily, there is only case,
within the grade/classes/component_gradeitem.php file, and it has
been fixed by casting the float-like string coming from DB to float.
2022-03-03 12:19:02 +01:00

584 lines
18 KiB
PHP

<?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/>.
/**
* Compontent definition of a gradeitem.
*
* @package core_grades
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types = 1);
namespace core_grades;
use context;
use gradingform_controller;
use gradingform_instance;
use moodle_exception;
use stdClass;
use grade_item as core_gradeitem;
use grading_manager;
/**
* Compontent definition of a gradeitem.
*
* @package core_grades
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class component_gradeitem {
/** @var array The scale data for the current grade item */
protected $scale;
/** @var string The component */
protected $component;
/** @var context The context for this activity */
protected $context;
/** @var string The item name */
protected $itemname;
/** @var int The grade itemnumber */
protected $itemnumber;
/**
* component_gradeitem constructor.
*
* @param string $component
* @param context $context
* @param string $itemname
* @throws \coding_exception
*/
final protected function __construct(string $component, context $context, string $itemname) {
$this->component = $component;
$this->context = $context;
$this->itemname = $itemname;
$this->itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
}
/**
* Fetch an instance of a specific component_gradeitem.
*
* @param string $component
* @param context $context
* @param string $itemname
* @return self
*/
public static function instance(string $component, context $context, string $itemname): self {
$itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
$classname = "{$component}\\grades\\{$itemname}_gradeitem";
if (!class_exists($classname)) {
throw new \coding_exception("Unknown gradeitem {$itemname} for component {$classname}");
}
return $classname::load_from_context($context);
}
/**
* Load an instance of the current component_gradeitem based on context.
*
* @param context $context
* @return self
*/
abstract public static function load_from_context(context $context): self;
/**
* The table name used for grading.
*
* @return string
*/
abstract protected function get_table_name(): string;
/**
* Get the itemid for the current gradeitem.
*
* @return int
*/
public function get_grade_itemid(): int {
return component_gradeitems::get_itemnumber_from_itemname($this->component, $this->itemname);
}
/**
* Whether grading is enabled for this item.
*
* @return bool
*/
abstract public function is_grading_enabled(): bool;
/**
* Get the grade value for this instance.
* The itemname is translated to the relevant grade field for the activity.
*
* @return int
*/
abstract protected function get_gradeitem_value(): ?int;
/**
* Whether the grader can grade the gradee.
*
* @param stdClass $gradeduser The user being graded
* @param stdClass $grader The user who is grading
* @return bool
*/
abstract public function user_can_grade(stdClass $gradeduser, stdClass $grader): bool;
/**
* Require that the user can grade, throwing an exception if not.
*
* @param stdClass $gradeduser The user being graded
* @param stdClass $grader The user who is grading
* @throws \required_capability_exception
*/
abstract public function require_user_can_grade(stdClass $gradeduser, stdClass $grader): void;
/**
* Get the scale if a scale is being used.
*
* @return stdClass
*/
protected function get_scale(): ?stdClass {
global $DB;
$gradetype = $this->get_gradeitem_value();
if ($gradetype > 0) {
return null;
}
// This is a scale.
if (null === $this->scale) {
$this->scale = $DB->get_record('scale', ['id' => -1 * $gradetype]);
}
return $this->scale;
}
/**
* Check whether a scale is being used for this grade item.
*
* @return bool
*/
public function is_using_scale(): bool {
$gradetype = $this->get_gradeitem_value();
return $gradetype < 0;
}
/**
* Whether this grade item is configured to use direct grading.
*
* @return bool
*/
public function is_using_direct_grading(): bool {
if ($this->is_using_scale()) {
return false;
}
if ($this->get_advanced_grading_controller()) {
return false;
}
return true;
}
/**
* Whether this grade item is configured to use advanced grading.
*
* @return bool
*/
public function is_using_advanced_grading(): bool {
if ($this->is_using_scale()) {
return false;
}
if ($this->get_advanced_grading_controller()) {
return true;
}
return false;
}
/**
* Get the name of the advanced grading method.
*
* @return string
*/
public function get_advanced_grading_method(): ?string {
$gradingmanager = $this->get_grading_manager();
if (empty($gradingmanager)) {
return null;
}
return $gradingmanager->get_active_method();
}
/**
* Get the name of the component responsible for grading this gradeitem.
*
* @return string
*/
public function get_grading_component_name(): ?string {
if (!$this->is_grading_enabled()) {
return null;
}
if ($method = $this->get_advanced_grading_method()) {
return "gradingform_{$method}";
}
return 'core_grades';
}
/**
* Get the name of the component subtype responsible for grading this gradeitem.
*
* @return string
*/
public function get_grading_component_subtype(): ?string {
if (!$this->is_grading_enabled()) {
return null;
}
if ($method = $this->get_advanced_grading_method()) {
return null;
}
if ($this->is_using_scale()) {
return 'scale';
}
return 'point';
}
/**
* Whether decimals are allowed.
*
* @return bool
*/
protected function allow_decimals(): bool {
return $this->get_gradeitem_value() > 0;
}
/**
* Get the grading manager for this advanced grading definition.
*
* @return grading_manager
*/
protected function get_grading_manager(): ?grading_manager {
require_once(__DIR__ . '/../grading/lib.php');
return get_grading_manager($this->context, $this->component, $this->itemname);
}
/**
* Get the advanced grading controller if advanced grading is enabled.
*
* @return gradingform_controller
*/
protected function get_advanced_grading_controller(): ?gradingform_controller {
$gradingmanager = $this->get_grading_manager();
if (empty($gradingmanager)) {
return null;
}
if ($gradingmethod = $gradingmanager->get_active_method()) {
return $gradingmanager->get_controller($gradingmethod);
}
return null;
}
/**
* Get the list of available grade items.
*
* @return array
*/
public function get_grade_menu(): array {
return make_grades_menu($this->get_gradeitem_value());
}
/**
* Check whether the supplied grade is valid and throw an exception if not.
*
* @param float $grade The value being checked
* @throws moodle_exception
* @return bool
*/
public function check_grade_validity(?float $grade): bool {
$grade = grade_floatval(unformat_float($grade));
if ($grade) {
if ($this->is_using_scale()) {
// Fetch all options for this scale.
$scaleoptions = make_menu_from_list($this->get_scale()->scale);
if ($grade != -1 && !array_key_exists((int) $grade, $scaleoptions)) {
// The selected option did not exist.
throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
'maxgrade' => count($scaleoptions),
'grade' => $grade,
]);
}
} else if ($grade) {
$maxgrade = $this->get_gradeitem_value();
if ($grade > $maxgrade) {
// The grade is greater than the maximum possible value.
throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
'maxgrade' => $maxgrade,
'grade' => $grade,
]);
} else if ($grade < 0) {
// Negative grades are not supported.
throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
'maxgrade' => $maxgrade,
'grade' => $grade,
]);
}
}
}
return true;
}
/**
* Create an empty row in the grade for the specified user and grader.
*
* @param stdClass $gradeduser The user being graded
* @param stdClass $grader The user who is grading
* @return stdClass The newly created grade record
*/
abstract public function create_empty_grade(stdClass $gradeduser, stdClass $grader): stdClass;
/**
* Get the grade record for the specified grade id.
*
* @param int $gradeid
* @return stdClass
* @throws \dml_exception
*/
public function get_grade(int $gradeid): stdClass {
global $DB;
return $DB->get_record($this->get_table_name(), ['id' => $gradeid]);
}
/**
* Get the grade for the specified user.
*
* @param stdClass $gradeduser The user being graded
* @param stdClass $grader The user who is grading
* @return stdClass The grade value
*/
abstract public function get_grade_for_user(stdClass $gradeduser, stdClass $grader): ?stdClass;
/**
* Returns the grade that should be displayed to the user.
*
* The grade does not necessarily return a float value, this method takes grade settings into considering so
* the correct value be shown, eg. a float vs a letter.
*
* @param stdClass $gradeduser
* @param stdClass $grader
* @return stdClass|null
*/
public function get_formatted_grade_for_user(stdClass $gradeduser, stdClass $grader): ?stdClass {
global $DB;
if ($grade = $this->get_grade_for_user($gradeduser, $grader)) {
$gradeitem = $this->get_grade_item();
if (!$this->is_using_scale()) {
$grade->grade = !is_null($grade->grade) ? (float)$grade->grade : null; // Cast non-null values, keeping nulls.
$grade->usergrade = grade_format_gradevalue($grade->grade, $gradeitem);
$grade->maxgrade = format_float($gradeitem->grademax, $gradeitem->get_decimals());
// If displaying the raw grade, also display the total value.
if ($gradeitem->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
$grade->usergrade .= ' / ' . $grade->maxgrade;
}
} else {
$grade->usergrade = '-';
if ($scale = $DB->get_record('scale', ['id' => $gradeitem->scaleid])) {
$options = make_menu_from_list($scale->scale);
$gradeint = (int) $grade->grade;
if (isset($options[$gradeint])) {
$grade->usergrade = $options[$gradeint];
}
}
$grade->maxgrade = format_float($gradeitem->grademax, $gradeitem->get_decimals());
}
return $grade;
}
return null;
}
/**
* Get the grade status for the specified user.
* If the user has a grade as defined by the implementor return true else return false.
*
* @param stdClass $gradeduser The user being graded
* @return bool The grade status
*/
abstract public function user_has_grade(stdClass $gradeduser): bool;
/**
* Get grades for all users for the specified gradeitem.
*
* @return stdClass[] The grades
*/
abstract public function get_all_grades(): array;
/**
* Get the grade item instance id.
*
* This is typically the cmid in the case of an activity, and relates to the iteminstance field in the grade_items
* table.
*
* @return int
*/
abstract public function get_grade_instance_id(): int;
/**
* Get the core grade item from the current component grade item.
* This is mainly used to access the max grade for a gradeitem
*
* @return \grade_item The grade item
*/
public function get_grade_item(): \grade_item {
global $CFG;
require_once("{$CFG->libdir}/gradelib.php");
[$itemtype, $itemmodule] = \core_component::normalize_component($this->component);
$gradeitem = \grade_item::fetch([
'itemtype' => $itemtype,
'itemmodule' => $itemmodule,
'itemnumber' => $this->itemnumber,
'iteminstance' => $this->get_grade_instance_id(),
]);
return $gradeitem;
}
/**
* Create or update the grade.
*
* @param stdClass $grade
* @return bool Success
*/
abstract protected function store_grade(stdClass $grade): bool;
/**
* Create or update the grade.
*
* @param stdClass $gradeduser The user being graded
* @param stdClass $grader The user who is grading
* @param stdClass $formdata The data submitted
* @return bool Success
*/
public function store_grade_from_formdata(stdClass $gradeduser, stdClass $grader, stdClass $formdata): bool {
// Require gradelib for grade_floatval.
require_once(__DIR__ . '/../../lib/gradelib.php');
$grade = $this->get_grade_for_user($gradeduser, $grader);
if ($this->is_using_advanced_grading()) {
$instanceid = $formdata->instanceid;
$gradinginstance = $this->get_advanced_grading_instance($grader, $grade, (int) $instanceid);
$grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading, $grade->id);
if ($grade->grade == -1) {
// In advanced grading, a value of -1 means no data.
return false;
}
} else {
// Handle the case when grade is set to No Grade.
if (isset($formdata->grade)) {
$grade->grade = grade_floatval(unformat_float($formdata->grade));
}
}
return $this->store_grade($grade);
}
/**
* Get the advanced grading instance for the specified grade entry.
*
* @param stdClass $grader The user who is grading
* @param stdClass $grade The row from the grade table.
* @param int $instanceid The instanceid of the advanced grading form
* @return gradingform_instance
*/
public function get_advanced_grading_instance(stdClass $grader, stdClass $grade, int $instanceid = null): ?gradingform_instance {
$controller = $this->get_advanced_grading_controller($this->itemname);
if (empty($controller)) {
// Advanced grading not enabeld for this item.
return null;
}
if (!$controller->is_form_available()) {
// The form is not available for this item.
return null;
}
// Fetch the instance for the specified graderid/itemid.
$gradinginstance = $controller->fetch_instance(
(int) $grader->id,
(int) $grade->id,
$instanceid
);
// Set the allowed grade range.
$gradinginstance->get_controller()->set_grade_range(
$this->get_grade_menu(),
$this->allow_decimals()
);
return $gradinginstance;
}
/**
* Sends a notification about the item being graded for the student.
*
* @param stdClass $gradeduser The user being graded
* @param stdClass $grader The user who is grading
*/
public function send_student_notification(stdClass $gradeduser, stdClass $grader): void {
$contextname = $this->context->get_context_name();
$eventdata = new \core\message\message();
$eventdata->courseid = $this->context->get_course_context()->instanceid;
$eventdata->component = 'moodle';
$eventdata->name = 'gradenotifications';
$eventdata->userfrom = $grader;
$eventdata->userto = $gradeduser;
$eventdata->subject = get_string('gradenotificationsubject', 'grades');
$eventdata->fullmessage = get_string('gradenotificationmessage', 'grades', $contextname);
$eventdata->contexturl = $this->context->get_url();
$eventdata->contexturlname = $contextname;
$eventdata->fullmessageformat = FORMAT_HTML;
$eventdata->fullmessagehtml = '';
$eventdata->smallmessage = '';
$eventdata->notification = 1;
message_send($eventdata);
}
}