. /** * Condition on grades of current user. * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_grade; defined('MOODLE_INTERNAL') || die(); /** * Condition on grades of current user. * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class condition extends \core_availability\condition { /** @var int Grade item id */ private $gradeitemid; /** @var float|null Min grade (must be >= this) or null if none */ private $min; /** @var float|null Max grade (must be < this) or null if none */ private $max; /** * Constructor. * * @param \stdClass $structure Data structure from JSON decode * @throws \coding_exception If invalid data structure. */ public function __construct($structure) { // Get grade item id. if (isset($structure->id) && is_int($structure->id)) { $this->gradeitemid = $structure->id; } else { throw new \coding_exception('Missing or invalid ->id for grade condition'); } // Get min and max. if (!property_exists($structure, 'min')) { $this->min = null; } else if (is_float($structure->min) || is_int($structure->min)) { $this->min = $structure->min; } else { throw new \coding_exception('Missing or invalid ->min for grade condition'); } if (!property_exists($structure, 'max')) { $this->max = null; } else if (is_float($structure->max) || is_int($structure->max)) { $this->max = $structure->max; } else { throw new \coding_exception('Missing or invalid ->max for grade condition'); } } public function save() { $result = (object)array('type' => 'grade', 'id' => $this->gradeitemid); if (!is_null($this->min)) { $result->min = $this->min; } if (!is_null($this->max)) { $result->max = $this->max; } return $result; } /** * Returns a JSON object which corresponds to a condition of this type. * * Intended for unit testing, as normally the JSON values are constructed * by JavaScript code. * * @param int $gradeitemid Grade item id * @param number|null $min Min grade (or null if no min) * @param number|null $max Max grade (or null if no max) * @return stdClass Object representing condition */ public static function get_json($gradeitemid, $min = null, $max = null) { $result = (object)array('type' => 'grade', 'id' => (int)$gradeitemid); if (!is_null($min)) { $result->min = $min; } if (!is_null($max)) { $result->max = $max; } return $result; } public function is_available($not, \core_availability\info $info, $grabthelot, $userid) { $course = $info->get_course(); $score = $this->get_cached_grade_score($this->gradeitemid, $course->id, $grabthelot, $userid); $allow = $score !== false && (is_null($this->min) || $score >= $this->min) && (is_null($this->max) || $score < $this->max); if ($not) { $allow = !$allow; } return $allow; } public function get_description($full, $not, \core_availability\info $info) { $course = $info->get_course(); // String depends on type of requirement. We are coy about // the actual numbers, in case grades aren't released to // students. if (is_null($this->min) && is_null($this->max)) { $string = 'any'; } else if (is_null($this->max)) { $string = 'min'; } else if (is_null($this->min)) { $string = 'max'; } else { $string = 'range'; } if ($not) { // The specific strings don't make as much sense with 'not'. if ($string === 'any') { $string = 'notany'; } else { $string = 'notgeneral'; } } $name = self::get_cached_grade_name($course->id, $this->gradeitemid); return get_string('requires_' . $string, 'availability_grade', $name); } protected function get_debug_string() { $out = '#' . $this->gradeitemid; if (!is_null($this->min)) { $out .= ' >= ' . sprintf('%.5f', $this->min); } if (!is_null($this->max)) { if (!is_null($this->min)) { $out .= ','; } $out .= ' < ' . sprintf('%.5f', $this->max); } return $out; } /** * Obtains the name of a grade item, also checking that it exists. Uses a * cache. The name returned is suitable for display. * * @param int $courseid Course id * @param int $gradeitemid Grade item id * @return string Grade name or empty string if no grade with that id */ private static function get_cached_grade_name($courseid, $gradeitemid) { global $DB, $CFG; require_once($CFG->libdir . '/gradelib.php'); // Get all grade item names from cache, or using db query. $cache = \cache::make('availability_grade', 'items'); if (($cacheditems = $cache->get($courseid)) === false) { // We cache the whole items table not the name; the format_string // call for the name might depend on current user (e.g. multilang) // and this is a shared cache. $cacheditems = $DB->get_records('grade_items', array('courseid' => $courseid)); $cache->set($courseid, $cacheditems); } // Return name from cached item or a lang string. if (!array_key_exists($gradeitemid, $cacheditems)) { return get_string('missing', 'availability_grade'); } $gradeitemobj = $cacheditems[$gradeitemid]; $item = new \grade_item; \grade_object::set_properties($item, $gradeitemobj); return $item->get_name(); } /** * Obtains a grade score. Note that this score should not be displayed to * the user, because gradebook rules might prohibit that. It may be a * non-final score subject to adjustment later. * * @param int $gradeitemid Grade item ID we're interested in * @param int $courseid Course id * @param bool $grabthelot If true, grabs all scores for current user on * this course, so that later ones come from cache * @param int $userid Set if requesting grade for a different user (does * not use cache) * @return float Grade score as a percentage in range 0-100 (e.g. 100.0 * or 37.21), or false if user does not have a grade yet */ protected static function get_cached_grade_score($gradeitemid, $courseid, $grabthelot=false, $userid=0) { global $USER, $DB; if (!$userid) { $userid = $USER->id; } $cache = \cache::make('availability_grade', 'scores'); if (($cachedgrades = $cache->get($userid)) === false) { $cachedgrades = array(); } if (!array_key_exists($gradeitemid, $cachedgrades)) { if ($grabthelot) { // Get all grades for the current course. $rs = $DB->get_recordset_sql(' SELECT gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax FROM {grade_items} gi LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=? WHERE gi.courseid = ?', array($userid, $courseid)); foreach ($rs as $record) { // This function produces division by zero error warnings when rawgrademax and rawgrademin // are equal. Below change does not affect function behavior, just avoids the warning. if (is_null($record->finalgrade) || $record->rawgrademax == $record->rawgrademin) { // No grade = false. $cachedgrades[$record->id] = false; } else { // Otherwise convert grade to percentage. $cachedgrades[$record->id] = (($record->finalgrade - $record->rawgrademin) * 100) / ($record->rawgrademax - $record->rawgrademin); } } $rs->close(); // And if it's still not set, well it doesn't exist (eg // maybe the user set it as a condition, then deleted the // grade item) so we call it false. if (!array_key_exists($gradeitemid, $cachedgrades)) { $cachedgrades[$gradeitemid] = false; } } else { // Just get current grade. $record = $DB->get_record('grade_grades', array( 'userid' => $userid, 'itemid' => $gradeitemid)); // This function produces division by zero error warnings when rawgrademax and rawgrademin // are equal. Below change does not affect function behavior, just avoids the warning. if ($record && !is_null($record->finalgrade) && $record->rawgrademax != $record->rawgrademin) { $score = (($record->finalgrade - $record->rawgrademin) * 100) / ($record->rawgrademax - $record->rawgrademin); } else { // Treat the case where row exists but is null, same as // case where row doesn't exist. $score = false; } $cachedgrades[$gradeitemid] = $score; } $cache->set($userid, $cachedgrades); } return $cachedgrades[$gradeitemid]; } public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) { global $DB; $rec = \restore_dbops::get_backup_ids_record($restoreid, 'grade_item', $this->gradeitemid); if (!$rec || !$rec->newitemid) { // If we are on the same course (e.g. duplicate) then we can just // use the existing one. if ($DB->record_exists('grade_items', array('id' => $this->gradeitemid, 'courseid' => $courseid))) { return false; } // Otherwise it's a warning. $this->gradeitemid = 0; $logger->process('Restored item (' . $name . ') has availability condition on grade that was not restored', \backup::LOG_WARNING); } else { $this->gradeitemid = (int)$rec->newitemid; } return true; } public function update_dependency_id($table, $oldid, $newid) { if ($table === 'grade_items' && (int)$this->gradeitemid === (int)$oldid) { $this->gradeitemid = $newid; return true; } else { return false; } } }