Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   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  /**
  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   */
  24  
  25  declare(strict_types = 1);
  26  
  27  namespace core_grades;
  28  
  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;
  36  
  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 {
  45  
  46      /** @var array The scale data for the current grade item */
  47      protected $scale;
  48  
  49      /** @var string The component */
  50      protected $component;
  51  
  52      /** @var context The context for this activity */
  53      protected $context;
  54  
  55      /** @var string The item name */
  56      protected $itemname;
  57  
  58      /** @var int The grade itemnumber */
  59      protected $itemnumber;
  60  
  61      /**
  62       * component_gradeitem constructor.
  63       *
  64       * @param string $component
  65       * @param context $context
  66       * @param string $itemname
  67       * @throws \coding_exception
  68       */
  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);
  74      }
  75  
  76      /**
  77       * Fetch an instance of a specific component_gradeitem.
  78       *
  79       * @param string $component
  80       * @param context $context
  81       * @param string $itemname
  82       * @return self
  83       */
  84      public static function instance(string $component, context $context, string $itemname): self {
  85          $itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
  86  
  87          $classname = "{$component}\\grades\\{$itemname}_gradeitem";
  88          if (!class_exists($classname)) {
  89              throw new \coding_exception("Unknown gradeitem {$itemname} for component {$classname}");
  90          }
  91  
  92          return $classname::load_from_context($context);
  93      }
  94  
  95      /**
  96       * Load an instance of the current component_gradeitem based on context.
  97       *
  98       * @param context $context
  99       * @return self
 100       */
 101      abstract public static function load_from_context(context $context): self;
 102  
 103      /**
 104       * The table name used for grading.
 105       *
 106       * @return string
 107       */
 108      abstract protected function get_table_name(): string;
 109  
 110      /**
 111       * Get the itemid for the current gradeitem.
 112       *
 113       * @return int
 114       */
 115      public function get_grade_itemid(): int {
 116          return component_gradeitems::get_itemnumber_from_itemname($this->component, $this->itemname);
 117      }
 118  
 119      /**
 120       * Whether grading is enabled for this item.
 121       *
 122       * @return bool
 123       */
 124      abstract public function is_grading_enabled(): bool;
 125  
 126      /**
 127       * Get the grade value for this instance.
 128       * The itemname is translated to the relevant grade field for the activity.
 129       *
 130       * @return int
 131       */
 132      abstract protected function get_gradeitem_value(): ?int;
 133  
 134      /**
 135       * Whether the grader can grade the gradee.
 136       *
 137       * @param stdClass $gradeduser The user being graded
 138       * @param stdClass $grader The user who is grading
 139       * @return bool
 140       */
 141      abstract public function user_can_grade(stdClass $gradeduser, stdClass $grader): bool;
 142  
 143      /**
 144       * Require that the user can grade, throwing an exception if not.
 145       *
 146       * @param stdClass $gradeduser The user being graded
 147       * @param stdClass $grader The user who is grading
 148       * @throws \required_capability_exception
 149       */
 150      abstract public function require_user_can_grade(stdClass $gradeduser, stdClass $grader): void;
 151  
 152      /**
 153       * Get the scale if a scale is being used.
 154       *
 155       * @return stdClass
 156       */
 157      protected function get_scale(): ?stdClass {
 158          global $DB;
 159  
 160          $gradetype = $this->get_gradeitem_value();
 161          if ($gradetype > 0) {
 162              return null;
 163          }
 164  
 165          // This is a scale.
 166          if (null === $this->scale) {
 167              $this->scale = $DB->get_record('scale', ['id' => -1 * $gradetype]);
 168          }
 169  
 170          return $this->scale;
 171      }
 172  
 173      /**
 174       * Check whether a scale is being used for this grade item.
 175       *
 176       * @return bool
 177       */
 178      public function is_using_scale(): bool {
 179          $gradetype = $this->get_gradeitem_value();
 180  
 181          return $gradetype < 0;
 182      }
 183  
 184      /**
 185       * Whether this grade item is configured to use direct grading.
 186       *
 187       * @return bool
 188       */
 189      public function is_using_direct_grading(): bool {
 190          if ($this->is_using_scale()) {
 191              return false;
 192          }
 193  
 194          if ($this->get_advanced_grading_controller()) {
 195              return false;
 196          }
 197  
 198          return true;
 199      }
 200  
 201      /**
 202       * Whether this grade item is configured to use advanced grading.
 203       *
 204       * @return bool
 205       */
 206      public function is_using_advanced_grading(): bool {
 207          if ($this->is_using_scale()) {
 208              return false;
 209          }
 210  
 211          if ($this->get_advanced_grading_controller()) {
 212              return true;
 213          }
 214  
 215          return false;
 216      }
 217  
 218      /**
 219       * Get the name of the advanced grading method.
 220       *
 221       * @return string
 222       */
 223      public function get_advanced_grading_method(): ?string {
 224          $gradingmanager = $this->get_grading_manager();
 225  
 226          if (empty($gradingmanager)) {
 227              return null;
 228          }
 229  
 230          return $gradingmanager->get_active_method();
 231      }
 232  
 233      /**
 234       * Get the name of the component responsible for grading this gradeitem.
 235       *
 236       * @return string
 237       */
 238      public function get_grading_component_name(): ?string {
 239          if (!$this->is_grading_enabled()) {
 240              return null;
 241          }
 242  
 243          if ($method = $this->get_advanced_grading_method()) {
 244              return "gradingform_{$method}";
 245          }
 246  
 247          return 'core_grades';
 248      }
 249  
 250      /**
 251       * Get the name of the component subtype responsible for grading this gradeitem.
 252       *
 253       * @return string
 254       */
 255      public function get_grading_component_subtype(): ?string {
 256          if (!$this->is_grading_enabled()) {
 257              return null;
 258          }
 259  
 260          if ($method = $this->get_advanced_grading_method()) {
 261              return null;
 262          }
 263  
 264          if ($this->is_using_scale()) {
 265              return 'scale';
 266          }
 267  
 268          return 'point';
 269      }
 270  
 271      /**
 272       * Whether decimals are allowed.
 273       *
 274       * @return bool
 275       */
 276      protected function allow_decimals(): bool {
 277          return $this->get_gradeitem_value() > 0;
 278      }
 279  
 280      /**
 281       * Get the grading manager for this advanced grading definition.
 282       *
 283       * @return grading_manager
 284       */
 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);
 288  
 289      }
 290  
 291      /**
 292       * Get the advanced grading controller if advanced grading is enabled.
 293       *
 294       * @return gradingform_controller
 295       */
 296      protected function get_advanced_grading_controller(): ?gradingform_controller {
 297          $gradingmanager = $this->get_grading_manager();
 298  
 299          if (empty($gradingmanager)) {
 300              return null;
 301          }
 302  
 303          if ($gradingmethod = $gradingmanager->get_active_method()) {
 304              return $gradingmanager->get_controller($gradingmethod);
 305          }
 306  
 307          return null;
 308      }
 309  
 310      /**
 311       * Get the list of available grade items.
 312       *
 313       * @return array
 314       */
 315      public function get_grade_menu(): array {
 316          return make_grades_menu($this->get_gradeitem_value());
 317      }
 318  
 319      /**
 320       * Check whether the supplied grade is valid and throw an exception if not.
 321       *
 322       * @param float $grade The value being checked
 323       * @throws moodle_exception
 324       * @return bool
 325       */
 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);
 332  
 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,
 338                      ]);
 339                  }
 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,
 347                      ]);
 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,
 353                      ]);
 354                  }
 355              }
 356          }
 357  
 358          return true;
 359      }
 360  
 361      /**
 362       * Create an empty row in the grade for the specified user and grader.
 363       *
 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
 367       */
 368      abstract public function create_empty_grade(stdClass $gradeduser, stdClass $grader): stdClass;
 369  
 370      /**
 371       * Get the grade record for the specified grade id.
 372       *
 373       * @param int $gradeid
 374       * @return stdClass
 375       * @throws \dml_exception
 376       */
 377      public function get_grade(int $gradeid): stdClass {
 378          global $DB;
 379  
 380          return $DB->get_record($this->get_table_name(), ['id' => $gradeid]);
 381      }
 382  
 383      /**
 384       * Get the grade for the specified user.
 385       *
 386       * @param stdClass $gradeduser The user being graded
 387       * @param stdClass $grader The user who is grading
 388       * @return stdClass The grade value
 389       */
 390      abstract public function get_grade_for_user(stdClass $gradeduser, stdClass $grader): ?stdClass;
 391  
 392      /**
 393       * Returns the grade that should be displayed to the user.
 394       *
 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.
 397       *
 398       * @param stdClass $gradeduser
 399       * @param stdClass $grader
 400       * @return stdClass|null
 401       */
 402      public function get_formatted_grade_for_user(stdClass $gradeduser, stdClass $grader): ?stdClass {
 403          global $DB;
 404  
 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;
 414                  }
 415              } else {
 416                  $grade->usergrade = '-';
 417                  if ($scale = $DB->get_record('scale', ['id' => $gradeitem->scaleid])) {
 418                      $options = make_menu_from_list($scale->scale);
 419  
 420                      $gradeint = (int) $grade->grade;
 421                      if (isset($options[$gradeint])) {
 422                          $grade->usergrade = $options[$gradeint];
 423                      }
 424                  }
 425  
 426                  $grade->maxgrade = format_float($gradeitem->grademax, $gradeitem->get_decimals());
 427              }
 428  
 429              return $grade;
 430          }
 431  
 432          return null;
 433      }
 434  
 435      /**
 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.
 438       *
 439       * @param stdClass $gradeduser The user being graded
 440       * @return bool The grade status
 441       */
 442      abstract public function user_has_grade(stdClass $gradeduser): bool;
 443  
 444      /**
 445       * Get grades for all users for the specified gradeitem.
 446       *
 447       * @return stdClass[] The grades
 448       */
 449      abstract public function get_all_grades(): array;
 450  
 451      /**
 452       * Get the grade item instance id.
 453       *
 454       * This is typically the cmid in the case of an activity, and relates to the iteminstance field in the grade_items
 455       * table.
 456       *
 457       * @return int
 458       */
 459      abstract public function get_grade_instance_id(): int;
 460  
 461      /**
 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
 464       *
 465       * @return \grade_item The grade item
 466       */
 467      public function get_grade_item(): \grade_item {
 468          global $CFG;
 469          require_once("{$CFG->libdir}/gradelib.php");
 470  
 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(),
 477          ]);
 478  
 479          return $gradeitem;
 480      }
 481  
 482      /**
 483       * Create or update the grade.
 484       *
 485       * @param stdClass $grade
 486       * @return bool Success
 487       */
 488      abstract protected function store_grade(stdClass $grade): bool;
 489  
 490      /**
 491       * Create or update the grade.
 492       *
 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
 497       */
 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);
 502  
 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);
 507  
 508              if ($grade->grade == -1) {
 509                  // In advanced grading, a value of -1 means no data.
 510                  return false;
 511              }
 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));
 516              }
 517          }
 518  
 519          return $this->store_grade($grade);
 520      }
 521  
 522      /**
 523       * Get the advanced grading instance for the specified grade entry.
 524       *
 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
 529       */
 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);
 532  
 533          if (empty($controller)) {
 534              // Advanced grading not enabeld for this item.
 535              return null;
 536          }
 537  
 538          if (!$controller->is_form_available()) {
 539              // The form is not available for this item.
 540              return null;
 541          }
 542  
 543          // Fetch the instance for the specified graderid/itemid.
 544          $gradinginstance = $controller->fetch_instance(
 545              (int) $grader->id,
 546              (int) $grade->id,
 547              $instanceid
 548          );
 549  
 550          // Set the allowed grade range.
 551          $gradinginstance->get_controller()->set_grade_range(
 552              $this->get_grade_menu(),
 553              $this->allow_decimals()
 554          );
 555  
 556          return $gradinginstance;
 557      }
 558  
 559      /**
 560       * Sends a notification about the item being graded for the student.
 561       *
 562       * @param stdClass $gradeduser The user being graded
 563       * @param stdClass $grader The user who is grading
 564       */
 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);
 582      }
 583  }