Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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->usergrade = grade_format_gradevalue($grade->grade, $gradeitem);
 409                  $grade->maxgrade = format_float($gradeitem->grademax, $gradeitem->get_decimals());
 410                  // If displaying the raw grade, also display the total value.
 411                  if ($gradeitem->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
 412                      $grade->usergrade .= ' / ' . $grade->maxgrade;
 413                  }
 414              } else {
 415                  $grade->usergrade = '-';
 416                  if ($scale = $DB->get_record('scale', ['id' => $gradeitem->scaleid])) {
 417                      $options = make_menu_from_list($scale->scale);
 418  
 419                      $gradeint = (int) $grade->grade;
 420                      if (isset($options[$gradeint])) {
 421                          $grade->usergrade = $options[$gradeint];
 422                      }
 423                  }
 424  
 425                  $grade->maxgrade = format_float($gradeitem->grademax, $gradeitem->get_decimals());
 426              }
 427  
 428              return $grade;
 429          }
 430  
 431          return null;
 432      }
 433  
 434      /**
 435       * Get the grade status for the specified user.
 436       * If the user has a grade as defined by the implementor return true else return false.
 437       *
 438       * @param stdClass $gradeduser The user being graded
 439       * @return bool The grade status
 440       */
 441      abstract public function user_has_grade(stdClass $gradeduser): bool;
 442  
 443      /**
 444       * Get grades for all users for the specified gradeitem.
 445       *
 446       * @return stdClass[] The grades
 447       */
 448      abstract public function get_all_grades(): array;
 449  
 450      /**
 451       * Get the grade item instance id.
 452       *
 453       * This is typically the cmid in the case of an activity, and relates to the iteminstance field in the grade_items
 454       * table.
 455       *
 456       * @return int
 457       */
 458      abstract public function get_grade_instance_id(): int;
 459  
 460      /**
 461       * Get the core grade item from the current component grade item.
 462       * This is mainly used to access the max grade for a gradeitem
 463       *
 464       * @return \grade_item The grade item
 465       */
 466      public function get_grade_item(): \grade_item {
 467          global $CFG;
 468          require_once("{$CFG->libdir}/gradelib.php");
 469  
 470          [$itemtype, $itemmodule] = \core_component::normalize_component($this->component);
 471          $gradeitem = \grade_item::fetch([
 472              'itemtype' => $itemtype,
 473              'itemmodule' => $itemmodule,
 474              'itemnumber' => $this->itemnumber,
 475              'iteminstance' => $this->get_grade_instance_id(),
 476          ]);
 477  
 478          return $gradeitem;
 479      }
 480  
 481      /**
 482       * Create or update the grade.
 483       *
 484       * @param stdClass $grade
 485       * @return bool Success
 486       */
 487      abstract protected function store_grade(stdClass $grade): bool;
 488  
 489      /**
 490       * Create or update the grade.
 491       *
 492       * @param stdClass $gradeduser The user being graded
 493       * @param stdClass $grader The user who is grading
 494       * @param stdClass $formdata The data submitted
 495       * @return bool Success
 496       */
 497      public function store_grade_from_formdata(stdClass $gradeduser, stdClass $grader, stdClass $formdata): bool {
 498          // Require gradelib for grade_floatval.
 499          require_once (__DIR__ . '/../../lib/gradelib.php');
 500          $grade = $this->get_grade_for_user($gradeduser, $grader);
 501  
 502          if ($this->is_using_advanced_grading()) {
 503              $instanceid = $formdata->instanceid;
 504              $gradinginstance = $this->get_advanced_grading_instance($grader, $grade, (int) $instanceid);
 505              $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading, $grade->id);
 506  
 507              if ($grade->grade == -1) {
 508                  // In advanced grading, a value of -1 means no data.
 509                  return false;
 510              }
 511          } else {
 512              // Handle the case when grade is set to No Grade.
 513              if (isset($formdata->grade)) {
 514                  $grade->grade = grade_floatval(unformat_float($formdata->grade));
 515              }
 516          }
 517  
 518          return $this->store_grade($grade);
 519      }
 520  
 521      /**
 522       * Get the advanced grading instance for the specified grade entry.
 523       *
 524       * @param stdClass $grader The user who is grading
 525       * @param stdClass $grade The row from the grade table.
 526       * @param int $instanceid The instanceid of the advanced grading form
 527       * @return gradingform_instance
 528       */
 529      public function get_advanced_grading_instance(stdClass $grader, stdClass $grade, int $instanceid = null): ?gradingform_instance {
 530          $controller = $this->get_advanced_grading_controller($this->itemname);
 531  
 532          if (empty($controller)) {
 533              // Advanced grading not enabeld for this item.
 534              return null;
 535          }
 536  
 537          if (!$controller->is_form_available()) {
 538              // The form is not available for this item.
 539              return null;
 540          }
 541  
 542          // Fetch the instance for the specified graderid/itemid.
 543          $gradinginstance = $controller->fetch_instance(
 544              (int) $grader->id,
 545              (int) $grade->id,
 546              $instanceid
 547          );
 548  
 549          // Set the allowed grade range.
 550          $gradinginstance->get_controller()->set_grade_range(
 551              $this->get_grade_menu(),
 552              $this->allow_decimals()
 553          );
 554  
 555          return $gradinginstance;
 556      }
 557  
 558      /**
 559       * Sends a notification about the item being graded for the student.
 560       *
 561       * @param stdClass $gradeduser The user being graded
 562       * @param stdClass $grader The user who is grading
 563       */
 564      public function send_student_notification(stdClass $gradeduser, stdClass $grader): void {
 565          $contextname = $this->context->get_context_name();
 566          $eventdata = new \core\message\message();
 567          $eventdata->courseid          = $this->context->get_course_context()->instanceid;
 568          $eventdata->component         = 'moodle';
 569          $eventdata->name              = 'gradenotifications';
 570          $eventdata->userfrom          = $grader;
 571          $eventdata->userto            = $gradeduser;
 572          $eventdata->subject           = get_string('gradenotificationsubject', 'grades');
 573          $eventdata->fullmessage       = get_string('gradenotificationmessage', 'grades', $contextname);
 574          $eventdata->contexturl        = $this->context->get_url();
 575          $eventdata->contexturlname    = $contextname;
 576          $eventdata->fullmessageformat = FORMAT_HTML;
 577          $eventdata->fullmessagehtml   = '';
 578          $eventdata->smallmessage      = '';
 579          $eventdata->notification      = 1;
 580          message_send($eventdata);
 581      }
 582  }