Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

   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          $grade = $DB->get_record($this->get_table_name(), ['id' => $gradeid]);
 381  
 382          return $grade ?: null;
 383      }
 384  
 385      /**
 386       * Get the grade for the specified user.
 387       *
 388       * @param stdClass $gradeduser The user being graded
 389       * @param stdClass $grader The user who is grading
 390       * @return stdClass The grade value
 391       */
 392      abstract public function get_grade_for_user(stdClass $gradeduser, stdClass $grader): ?stdClass;
 393  
 394      /**
 395       * Get the grade status for the specified user.
 396       * If the user has a grade as defined by the implementor return true else return false.
 397       *
 398       * @param stdClass $gradeduser The user being graded
 399       * @return bool The grade status
 400       */
 401      abstract public function user_has_grade(stdClass $gradeduser): bool;
 402  
 403      /**
 404       * Get grades for all users for the specified gradeitem.
 405       *
 406       * @return stdClass[] The grades
 407       */
 408      abstract public function get_all_grades(): array;
 409  
 410      /**
 411       * Get the grade item instance id.
 412       *
 413       * This is typically the cmid in the case of an activity, and relates to the iteminstance field in the grade_items
 414       * table.
 415       *
 416       * @return int
 417       */
 418      abstract public function get_grade_instance_id(): int;
 419  
 420      /**
 421       * Get the core grade item from the current component grade item.
 422       * This is mainly used to access the max grade for a gradeitem
 423       *
 424       * @return \grade_item The grade item
 425       */
 426      public function get_grade_item(): \grade_item {
 427          global $CFG;
 428          require_once("{$CFG->libdir}/gradelib.php");
 429  
 430          [$itemtype, $itemmodule] = \core_component::normalize_component($this->component);
 431          $gradeitem = \grade_item::fetch([
 432              'itemtype' => $itemtype,
 433              'itemmodule' => $itemmodule,
 434              'itemnumber' => $this->itemnumber,
 435              'iteminstance' => $this->get_grade_instance_id(),
 436          ]);
 437  
 438          return $gradeitem;
 439      }
 440  
 441      /**
 442       * Create or update the grade.
 443       *
 444       * @param stdClass $grade
 445       * @return bool Success
 446       */
 447      abstract protected function store_grade(stdClass $grade): bool;
 448  
 449      /**
 450       * Create or update the grade.
 451       *
 452       * @param stdClass $gradeduser The user being graded
 453       * @param stdClass $grader The user who is grading
 454       * @param stdClass $formdata The data submitted
 455       * @return bool Success
 456       */
 457      public function store_grade_from_formdata(stdClass $gradeduser, stdClass $grader, stdClass $formdata): bool {
 458          // Require gradelib for grade_floatval.
 459          require_once (__DIR__ . '/../../lib/gradelib.php');
 460          $grade = $this->get_grade_for_user($gradeduser, $grader);
 461  
 462          if ($this->is_using_advanced_grading()) {
 463              $instanceid = $formdata->instanceid;
 464              $gradinginstance = $this->get_advanced_grading_instance($grader, $grade, (int) $instanceid);
 465              $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading, $grade->id);
 466  
 467              if ($grade->grade == -1) {
 468                  // In advanced grading, a value of -1 means no data.
 469                  return false;
 470              }
 471          } else {
 472              // Handle the case when grade is set to No Grade.
 473              if (isset($formdata->grade)) {
 474                  $grade->grade = grade_floatval(unformat_float($formdata->grade));
 475              }
 476          }
 477  
 478          return $this->store_grade($grade);
 479      }
 480  
 481      /**
 482       * Get the advanced grading instance for the specified grade entry.
 483       *
 484       * @param stdClass $grader The user who is grading
 485       * @param stdClass $grade The row from the grade table.
 486       * @param int $instanceid The instanceid of the advanced grading form
 487       * @return gradingform_instance
 488       */
 489      public function get_advanced_grading_instance(stdClass $grader, stdClass $grade, int $instanceid = null): ?gradingform_instance {
 490          $controller = $this->get_advanced_grading_controller($this->itemname);
 491  
 492          if (empty($controller)) {
 493              // Advanced grading not enabeld for this item.
 494              return null;
 495          }
 496  
 497          if (!$controller->is_form_available()) {
 498              // The form is not available for this item.
 499              return null;
 500          }
 501  
 502          // Fetch the instance for the specified graderid/itemid.
 503          $gradinginstance = $controller->fetch_instance(
 504              (int) $grader->id,
 505              (int) $grade->id,
 506              $instanceid
 507          );
 508  
 509          // Set the allowed grade range.
 510          $gradinginstance->get_controller()->set_grade_range(
 511              $this->get_grade_menu(),
 512              $this->allow_decimals()
 513          );
 514  
 515          return $gradinginstance;
 516      }
 517  
 518      /**
 519       * Sends a notification about the item being graded for the student.
 520       *
 521       * @param stdClass $gradeduser The user being graded
 522       * @param stdClass $grader The user who is grading
 523       */
 524      public function send_student_notification(stdClass $gradeduser, stdClass $grader): void {
 525          $contextname = $this->context->get_context_name();
 526          $eventdata = new \core\message\message();
 527          $eventdata->courseid          = $this->context->get_course_context()->instanceid;
 528          $eventdata->component         = 'moodle';
 529          $eventdata->name              = 'gradenotifications';
 530          $eventdata->userfrom          = $grader;
 531          $eventdata->userto            = $gradeduser;
 532          $eventdata->subject           = get_string('gradenotificationsubject', 'grades');
 533          $eventdata->fullmessage       = get_string('gradenotificationmessage', 'grades', $contextname);
 534          $eventdata->contexturl        = $this->context->get_url();
 535          $eventdata->contexturlname    = $contextname;
 536          $eventdata->fullmessageformat = FORMAT_HTML;
 537          $eventdata->fullmessagehtml   = '';
 538          $eventdata->smallmessage      = '';
 539          $eventdata->notification      = 1;
 540          message_send($eventdata);
 541      }
 542  }