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   * 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          $name = self::get_cached_grade_name($course->id, $this->gradeitemid);
 146          return get_string('requires_' . $string, 'availability_grade', $name);
 147      }
 148  
 149      protected function get_debug_string() {
 150          $out = '#' . $this->gradeitemid;
 151          if (!is_null($this->min)) {
 152              $out .= ' >= ' . sprintf('%.5f', $this->min);
 153          }
 154          if (!is_null($this->max)) {
 155              if (!is_null($this->min)) {
 156                  $out .= ',';
 157              }
 158              $out .= ' < ' . sprintf('%.5f', $this->max);
 159          }
 160          return $out;
 161      }
 162  
 163      /**
 164       * Obtains the name of a grade item, also checking that it exists. Uses a
 165       * cache. The name returned is suitable for display.
 166       *
 167       * @param int $courseid Course id
 168       * @param int $gradeitemid Grade item id
 169       * @return string Grade name or empty string if no grade with that id
 170       */
 171      private static function get_cached_grade_name($courseid, $gradeitemid) {
 172          global $DB, $CFG;
 173          require_once($CFG->libdir . '/gradelib.php');
 174  
 175          // Get all grade item names from cache, or using db query.
 176          $cache = \cache::make('availability_grade', 'items');
 177          if (($cacheditems = $cache->get($courseid)) === false) {
 178              // We cache the whole items table not the name; the format_string
 179              // call for the name might depend on current user (e.g. multilang)
 180              // and this is a shared cache.
 181              $cacheditems = $DB->get_records('grade_items', array('courseid' => $courseid));
 182              $cache->set($courseid, $cacheditems);
 183          }
 184  
 185          // Return name from cached item or a lang string.
 186          if (!array_key_exists($gradeitemid, $cacheditems)) {
 187              return get_string('missing', 'availability_grade');
 188          }
 189          $gradeitemobj = $cacheditems[$gradeitemid];
 190          $item = new \grade_item;
 191          \grade_object::set_properties($item, $gradeitemobj);
 192          return $item->get_name();
 193      }
 194  
 195      /**
 196       * Obtains a grade score. Note that this score should not be displayed to
 197       * the user, because gradebook rules might prohibit that. It may be a
 198       * non-final score subject to adjustment later.
 199       *
 200       * @param int $gradeitemid Grade item ID we're interested in
 201       * @param int $courseid Course id
 202       * @param bool $grabthelot If true, grabs all scores for current user on
 203       *   this course, so that later ones come from cache
 204       * @param int $userid Set if requesting grade for a different user (does
 205       *   not use cache)
 206       * @return float Grade score as a percentage in range 0-100 (e.g. 100.0
 207       *   or 37.21), or false if user does not have a grade yet
 208       */
 209      protected static function get_cached_grade_score($gradeitemid, $courseid,
 210              $grabthelot=false, $userid=0) {
 211          global $USER, $DB;
 212          if (!$userid) {
 213              $userid = $USER->id;
 214          }
 215          $cache = \cache::make('availability_grade', 'scores');
 216          if (($cachedgrades = $cache->get($userid)) === false) {
 217              $cachedgrades = array();
 218          }
 219          if (!array_key_exists($gradeitemid, $cachedgrades)) {
 220              if ($grabthelot) {
 221                  // Get all grades for the current course.
 222                  $rs = $DB->get_recordset_sql('
 223                          SELECT
 224                              gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax
 225                          FROM
 226                              {grade_items} gi
 227                              LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=?
 228                          WHERE
 229                              gi.courseid = ?', array($userid, $courseid));
 230                  foreach ($rs as $record) {
 231                      // This function produces division by zero error warnings when rawgrademax and rawgrademin
 232                      // are equal. Below change does not affect function behavior, just avoids the warning.
 233                      if (is_null($record->finalgrade) || $record->rawgrademax == $record->rawgrademin) {
 234                          // No grade = false.
 235                          $cachedgrades[$record->id] = false;
 236                      } else {
 237                          // Otherwise convert grade to percentage.
 238                          $cachedgrades[$record->id] =
 239                                  (($record->finalgrade - $record->rawgrademin) * 100) /
 240                                  ($record->rawgrademax - $record->rawgrademin);
 241                      }
 242                  }
 243                  $rs->close();
 244                  // And if it's still not set, well it doesn't exist (eg
 245                  // maybe the user set it as a condition, then deleted the
 246                  // grade item) so we call it false.
 247                  if (!array_key_exists($gradeitemid, $cachedgrades)) {
 248                      $cachedgrades[$gradeitemid] = false;
 249                  }
 250              } else {
 251                  // Just get current grade.
 252                  $record = $DB->get_record('grade_grades', array(
 253                      'userid' => $userid, 'itemid' => $gradeitemid));
 254                  // This function produces division by zero error warnings when rawgrademax and rawgrademin
 255                  // are equal. Below change does not affect function behavior, just avoids the warning.
 256                  if ($record && !is_null($record->finalgrade) && $record->rawgrademax != $record->rawgrademin) {
 257                      $score = (($record->finalgrade - $record->rawgrademin) * 100) /
 258                          ($record->rawgrademax - $record->rawgrademin);
 259                  } else {
 260                      // Treat the case where row exists but is null, same as
 261                      // case where row doesn't exist.
 262                      $score = false;
 263                  }
 264                  $cachedgrades[$gradeitemid] = $score;
 265              }
 266              $cache->set($userid, $cachedgrades);
 267          }
 268          return $cachedgrades[$gradeitemid];
 269      }
 270  
 271      public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
 272          global $DB;
 273          $rec = \restore_dbops::get_backup_ids_record($restoreid, 'grade_item', $this->gradeitemid);
 274          if (!$rec || !$rec->newitemid) {
 275              // If we are on the same course (e.g. duplicate) then we can just
 276              // use the existing one.
 277              if ($DB->record_exists('grade_items',
 278                      array('id' => $this->gradeitemid, 'courseid' => $courseid))) {
 279                  return false;
 280              }
 281              // Otherwise it's a warning.
 282              $this->gradeitemid = 0;
 283              $logger->process('Restored item (' . $name .
 284                      ') has availability condition on grade that was not restored',
 285                      \backup::LOG_WARNING);
 286          } else {
 287              $this->gradeitemid = (int)$rec->newitemid;
 288          }
 289          return true;
 290      }
 291  
 292      public function update_dependency_id($table, $oldid, $newid) {
 293          if ($table === 'grade_items' && (int)$this->gradeitemid === (int)$oldid) {
 294              $this->gradeitemid = $newid;
 295              return true;
 296          } else {
 297              return false;
 298          }
 299      }
 300  }