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 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   * Condition on grades of current user.
  19   *
  20   * @package availability_grade
  21   * @copyright 2014 The Open University
  22   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace availability_grade;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Condition on grades of current user.
  31   *
  32   * @package availability_grade
  33   * @copyright 2014 The Open University
  34   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class condition extends \core_availability\condition {
  37      /** @var int Grade item id */
  38      private $gradeitemid;
  39  
  40      /** @var float|null Min grade (must be >= this) or null if none */
  41      private $min;
  42  
  43      /** @var float|null Max grade (must be < this) or null if none */
  44      private $max;
  45  
  46      /**
  47       * Constructor.
  48       *
  49       * @param \stdClass $structure Data structure from JSON decode
  50       * @throws \coding_exception If invalid data structure.
  51       */
  52      public function __construct($structure) {
  53          // Get grade item id.
  54          if (isset($structure->id) && is_int($structure->id)) {
  55              $this->gradeitemid = $structure->id;
  56          } else {
  57              throw new \coding_exception('Missing or invalid ->id for grade condition');
  58          }
  59  
  60          // Get min and max.
  61          if (!property_exists($structure, 'min')) {
  62              $this->min = null;
  63          } else if (is_float($structure->min) || is_int($structure->min)) {
  64              $this->min = $structure->min;
  65          } else {
  66              throw new \coding_exception('Missing or invalid ->min for grade condition');
  67          }
  68          if (!property_exists($structure, 'max')) {
  69              $this->max = null;
  70          } else if (is_float($structure->max) || is_int($structure->max)) {
  71              $this->max = $structure->max;
  72          } else {
  73              throw new \coding_exception('Missing or invalid ->max for grade condition');
  74          }
  75      }
  76  
  77      public function save() {
  78          $result = (object)array('type' => 'grade', 'id' => $this->gradeitemid);
  79          if (!is_null($this->min)) {
  80              $result->min = $this->min;
  81          }
  82          if (!is_null($this->max)) {
  83              $result->max = $this->max;
  84          }
  85          return $result;
  86      }
  87  
  88      /**
  89       * Returns a JSON object which corresponds to a condition of this type.
  90       *
  91       * Intended for unit testing, as normally the JSON values are constructed
  92       * by JavaScript code.
  93       *
  94       * @param int $gradeitemid Grade item id
  95       * @param number|null $min Min grade (or null if no min)
  96       * @param number|null $max Max grade (or null if no max)
  97       * @return stdClass Object representing condition
  98       */
  99      public static function get_json($gradeitemid, $min = null, $max = null) {
 100          $result = (object)array('type' => 'grade', 'id' => (int)$gradeitemid);
 101          if (!is_null($min)) {
 102              $result->min = $min;
 103          }
 104          if (!is_null($max)) {
 105              $result->max = $max;
 106          }
 107          return $result;
 108      }
 109  
 110      public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
 111          $course = $info->get_course();
 112          $score = $this->get_cached_grade_score($this->gradeitemid, $course->id, $grabthelot, $userid);
 113          $allow = $score !== false &&
 114                  (is_null($this->min) || $score >= $this->min) &&
 115                  (is_null($this->max) || $score < $this->max);
 116          if ($not) {
 117              $allow = !$allow;
 118          }
 119  
 120          return $allow;
 121      }
 122  
 123      public function get_description($full, $not, \core_availability\info $info) {
 124          $course = $info->get_course();
 125          // String depends on type of requirement. We are coy about
 126          // the actual numbers, in case grades aren't released to
 127          // students.
 128          if (is_null($this->min) && is_null($this->max)) {
 129              $string = 'any';
 130          } else if (is_null($this->max)) {
 131              $string = 'min';
 132          } else if (is_null($this->min)) {
 133              $string = 'max';
 134          } else {
 135              $string = 'range';
 136          }
 137          if ($not) {
 138              // The specific strings don't make as much sense with 'not'.
 139              if ($string === 'any') {
 140                  $string = 'notany';
 141              } else {
 142                  $string = 'notgeneral';
 143              }
 144          }
 145          // We cannot get the name at this point because it requires format_string which is not
 146          // allowed here. Instead, get it later with the callback function below.
 147          $name = $this->description_callback([$this->gradeitemid]);
 148          return get_string('requires_' . $string, 'availability_grade', $name);
 149      }
 150  
 151      /**
 152       * Gets the grade name at display time.
 153       *
 154       * @param \course_modinfo $modinfo Modinfo
 155       * @param \context $context Context
 156       * @param string[] $params Parameters (just grade item id)
 157       * @return string Text value
 158       */
 159      public static function get_description_callback_value(
 160              \course_modinfo $modinfo, \context $context, array $params): string {
 161          if (count($params) !== 1 || !is_number($params[0])) {
 162              return '<!-- Invalid grade description callback -->';
 163          }
 164          $gradeitemid = (int)$params[0];
 165          return self::get_cached_grade_name($modinfo->get_course_id(), $gradeitemid);
 166      }
 167  
 168      protected function get_debug_string() {
 169          $out = '#' . $this->gradeitemid;
 170          if (!is_null($this->min)) {
 171              $out .= ' >= ' . sprintf('%.5f', $this->min);
 172          }
 173          if (!is_null($this->max)) {
 174              if (!is_null($this->min)) {
 175                  $out .= ',';
 176              }
 177              $out .= ' < ' . sprintf('%.5f', $this->max);
 178          }
 179          return $out;
 180      }
 181  
 182      /**
 183       * Obtains the name of a grade item, also checking that it exists. Uses a
 184       * cache. The name returned is suitable for display.
 185       *
 186       * @param int $courseid Course id
 187       * @param int $gradeitemid Grade item id
 188       * @return string Grade name or empty string if no grade with that id
 189       */
 190      private static function get_cached_grade_name($courseid, $gradeitemid) {
 191          global $DB, $CFG;
 192          require_once($CFG->libdir . '/gradelib.php');
 193  
 194          // Get all grade item names from cache, or using db query.
 195          $cache = \cache::make('availability_grade', 'items');
 196          if (($cacheditems = $cache->get($courseid)) === false) {
 197              // We cache the whole items table not the name; the format_string
 198              // call for the name might depend on current user (e.g. multilang)
 199              // and this is a shared cache.
 200              $cacheditems = $DB->get_records('grade_items', array('courseid' => $courseid));
 201              $cache->set($courseid, $cacheditems);
 202          }
 203  
 204          // Return name from cached item or a lang string.
 205          if (!array_key_exists($gradeitemid, $cacheditems)) {
 206              return get_string('missing', 'availability_grade');
 207          }
 208          $gradeitemobj = $cacheditems[$gradeitemid];
 209          $item = new \grade_item;
 210          \grade_object::set_properties($item, $gradeitemobj);
 211          return $item->get_name();
 212      }
 213  
 214      /**
 215       * Obtains a grade score. Note that this score should not be displayed to
 216       * the user, because gradebook rules might prohibit that. It may be a
 217       * non-final score subject to adjustment later.
 218       *
 219       * @param int $gradeitemid Grade item ID we're interested in
 220       * @param int $courseid Course id
 221       * @param bool $grabthelot If true, grabs all scores for current user on
 222       *   this course, so that later ones come from cache
 223       * @param int $userid Set if requesting grade for a different user (does
 224       *   not use cache)
 225       * @return float Grade score as a percentage in range 0-100 (e.g. 100.0
 226       *   or 37.21), or false if user does not have a grade yet
 227       */
 228      protected static function get_cached_grade_score($gradeitemid, $courseid,
 229              $grabthelot=false, $userid=0) {
 230          global $USER, $DB;
 231          if (!$userid) {
 232              $userid = $USER->id;
 233          }
 234          $cache = \cache::make('availability_grade', 'scores');
 235          if (($cachedgrades = $cache->get($userid)) === false) {
 236              $cachedgrades = array();
 237          }
 238          if (!array_key_exists($gradeitemid, $cachedgrades)) {
 239              if ($grabthelot) {
 240                  // Get all grades for the current course.
 241                  $rs = $DB->get_recordset_sql('
 242                          SELECT
 243                              gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax
 244                          FROM
 245                              {grade_items} gi
 246                              LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=?
 247                          WHERE
 248                              gi.courseid = ?', array($userid, $courseid));
 249                  foreach ($rs as $record) {
 250                      // This function produces division by zero error warnings when rawgrademax and rawgrademin
 251                      // are equal. Below change does not affect function behavior, just avoids the warning.
 252                      if (is_null($record->finalgrade) || $record->rawgrademax == $record->rawgrademin) {
 253                          // No grade = false.
 254                          $cachedgrades[$record->id] = false;
 255                      } else {
 256                          // Otherwise convert grade to percentage.
 257                          $cachedgrades[$record->id] =
 258                                  (($record->finalgrade - $record->rawgrademin) * 100) /
 259                                  ($record->rawgrademax - $record->rawgrademin);
 260                      }
 261                  }
 262                  $rs->close();
 263                  // And if it's still not set, well it doesn't exist (eg
 264                  // maybe the user set it as a condition, then deleted the
 265                  // grade item) so we call it false.
 266                  if (!array_key_exists($gradeitemid, $cachedgrades)) {
 267                      $cachedgrades[$gradeitemid] = false;
 268                  }
 269              } else {
 270                  // Just get current grade.
 271                  $record = $DB->get_record('grade_grades', array(
 272                      'userid' => $userid, 'itemid' => $gradeitemid));
 273                  // This function produces division by zero error warnings when rawgrademax and rawgrademin
 274                  // are equal. Below change does not affect function behavior, just avoids the warning.
 275                  if ($record && !is_null($record->finalgrade) && $record->rawgrademax != $record->rawgrademin) {
 276                      $score = (($record->finalgrade - $record->rawgrademin) * 100) /
 277                          ($record->rawgrademax - $record->rawgrademin);
 278                  } else {
 279                      // Treat the case where row exists but is null, same as
 280                      // case where row doesn't exist.
 281                      $score = false;
 282                  }
 283                  $cachedgrades[$gradeitemid] = $score;
 284              }
 285              $cache->set($userid, $cachedgrades);
 286          }
 287          return $cachedgrades[$gradeitemid];
 288      }
 289  
 290      public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
 291          global $DB;
 292          $rec = \restore_dbops::get_backup_ids_record($restoreid, 'grade_item', $this->gradeitemid);
 293          if (!$rec || !$rec->newitemid) {
 294              // If we are on the same course (e.g. duplicate) then we can just
 295              // use the existing one.
 296              if ($DB->record_exists('grade_items',
 297                      array('id' => $this->gradeitemid, 'courseid' => $courseid))) {
 298                  return false;
 299              }
 300              // Otherwise it's a warning.
 301              $this->gradeitemid = 0;
 302              $logger->process('Restored item (' . $name .
 303                      ') has availability condition on grade that was not restored',
 304                      \backup::LOG_WARNING);
 305          } else {
 306              $this->gradeitemid = (int)$rec->newitemid;
 307          }
 308          return true;
 309      }
 310  
 311      public function update_dependency_id($table, $oldid, $newid) {
 312          if ($table === 'grade_items' && (int)$this->gradeitemid === (int)$oldid) {
 313              $this->gradeitemid = $newid;
 314              return true;
 315          } else {
 316              return false;
 317          }
 318      }
 319  }