/lib/grade/grade_item.php
PHP | 2543 lines | 1339 code | 376 blank | 828 comment | 388 complexity | 67a8292c4521c85fedbcfcf3cd167f8c MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
Large files files are truncated, but you can click here to view the full file
- <?php
- // This file is part of Moodle - http://moodle.org/
- //
- // Moodle is free software: you can redistribute it and/or modify
- // it under the terms of the GNU General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // Moodle is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU General Public License for more details.
- //
- // You should have received a copy of the GNU General Public License
- // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
- /**
- * Definition of a class to represent a grade item
- *
- * @package core_grades
- * @category grade
- * @copyright 2006 Nicolas Connault
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
- defined('MOODLE_INTERNAL') || die();
- require_once('grade_object.php');
- /**
- * Class representing a grade item.
- *
- * It is responsible for handling its DB representation, modifying and returning its metadata.
- *
- * @package core_grades
- * @category grade
- * @copyright 2006 Nicolas Connault
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
- class grade_item extends grade_object {
- /**
- * DB Table (used by grade_object).
- * @var string $table
- */
- public $table = 'grade_items';
- /**
- * Array of required table fields, must start with 'id'.
- * @var array $required_fields
- */
- public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
- 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
- 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
- 'aggregationcoef2', 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
- 'needsupdate', 'weightoverride', 'timecreated', 'timemodified');
- /**
- * The course this grade_item belongs to.
- * @var int $courseid
- */
- public $courseid;
- /**
- * The category this grade_item belongs to (optional).
- * @var int $categoryid
- */
- public $categoryid;
- /**
- * The grade_category object referenced $this->iteminstance if itemtype == 'category' or == 'course'.
- * @var grade_category $item_category
- */
- public $item_category;
- /**
- * The grade_category object referenced by $this->categoryid.
- * @var grade_category $parent_category
- */
- public $parent_category;
- /**
- * The name of this grade_item (pushed by the module).
- * @var string $itemname
- */
- public $itemname;
- /**
- * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
- * @var string $itemtype
- */
- public $itemtype;
- /**
- * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
- * @var string $itemmodule
- */
- public $itemmodule;
- /**
- * ID of the item module
- * @var int $iteminstance
- */
- public $iteminstance;
- /**
- * Number of the item in a series of multiple grades pushed by an activity.
- * @var int $itemnumber
- */
- public $itemnumber;
- /**
- * Info and notes about this item.
- * @var string $iteminfo
- */
- public $iteminfo;
- /**
- * Arbitrary idnumber provided by the module responsible.
- * @var string $idnumber
- */
- public $idnumber;
- /**
- * Calculation string used for this item.
- * @var string $calculation
- */
- public $calculation;
- /**
- * Indicates if we already tried to normalize the grade calculation formula.
- * This flag helps to minimize db access when broken formulas used in calculation.
- * @var bool
- */
- public $calculation_normalized;
- /**
- * Math evaluation object
- * @var calc_formula A formula object
- */
- public $formula;
- /**
- * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
- * @var int $gradetype
- */
- public $gradetype = GRADE_TYPE_VALUE;
- /**
- * Maximum allowable grade.
- * @var float $grademax
- */
- public $grademax = 100;
- /**
- * Minimum allowable grade.
- * @var float $grademin
- */
- public $grademin = 0;
- /**
- * id of the scale, if this grade is based on a scale.
- * @var int $scaleid
- */
- public $scaleid;
- /**
- * The grade_scale object referenced by $this->scaleid.
- * @var grade_scale $scale
- */
- public $scale;
- /**
- * The id of the optional grade_outcome associated with this grade_item.
- * @var int $outcomeid
- */
- public $outcomeid;
- /**
- * The grade_outcome this grade is associated with, if applicable.
- * @var grade_outcome $outcome
- */
- public $outcome;
- /**
- * grade required to pass. (grademin <= gradepass <= grademax)
- * @var float $gradepass
- */
- public $gradepass = 0;
- /**
- * Multiply all grades by this number.
- * @var float $multfactor
- */
- public $multfactor = 1.0;
- /**
- * Add this to all grades.
- * @var float $plusfactor
- */
- public $plusfactor = 0;
- /**
- * Aggregation coeficient used for weighted averages or extra credit
- * @var float $aggregationcoef
- */
- public $aggregationcoef = 0;
- /**
- * Aggregation coeficient used for weighted averages only
- * @var float $aggregationcoef2
- */
- public $aggregationcoef2 = 0;
- /**
- * Sorting order of the columns.
- * @var int $sortorder
- */
- public $sortorder = 0;
- /**
- * Display type of the grades (Real, Percentage, Letter, or default).
- * @var int $display
- */
- public $display = GRADE_DISPLAY_TYPE_DEFAULT;
- /**
- * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
- * @var int $decimals
- */
- public $decimals = null;
- /**
- * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
- * @var int $locked
- */
- public $locked = 0;
- /**
- * Date after which the grade will be locked. Empty means no automatic locking.
- * @var int $locktime
- */
- public $locktime = 0;
- /**
- * If set, the whole column will be recalculated, then this flag will be switched off.
- * @var bool $needsupdate
- */
- public $needsupdate = 1;
- /**
- * If set, the grade item's weight has been overridden by a user and should not be automatically adjusted.
- */
- public $weightoverride = 0;
- /**
- * Cached dependson array
- * @var array An array of cached grade item dependencies.
- */
- public $dependson_cache = null;
- /**
- * Constructor. Optionally (and by default) attempts to fetch corresponding row from the database
- *
- * @param array $params An array with required parameters for this grade object.
- * @param bool $fetch Whether to fetch corresponding row from the database or not,
- * optional fields might not be defined if false used
- */
- public function __construct($params = null, $fetch = true) {
- global $CFG;
- // Set grademax from $CFG->gradepointdefault .
- self::set_properties($this, array('grademax' => $CFG->gradepointdefault));
- parent::__construct($params, $fetch);
- }
- /**
- * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
- * Force regrading if necessary, rounds the float numbers using php function,
- * the reason is we need to compare the db value with computed number to skip regrading if possible.
- *
- * @param string $source from where was the object inserted (mod/forum, manual, etc.)
- * @return bool success
- */
- public function update($source=null) {
- // reset caches
- $this->dependson_cache = null;
- // Retrieve scale and infer grademax/min from it if needed
- $this->load_scale();
- // make sure there is not 0 in outcomeid
- if (empty($this->outcomeid)) {
- $this->outcomeid = null;
- }
- if ($this->qualifies_for_regrading()) {
- $this->force_regrading();
- }
- $this->timemodified = time();
- $this->grademin = grade_floatval($this->grademin);
- $this->grademax = grade_floatval($this->grademax);
- $this->multfactor = grade_floatval($this->multfactor);
- $this->plusfactor = grade_floatval($this->plusfactor);
- $this->aggregationcoef = grade_floatval($this->aggregationcoef);
- $this->aggregationcoef2 = grade_floatval($this->aggregationcoef2);
- $result = parent::update($source);
- if ($result) {
- $event = \core\event\grade_item_updated::create_from_grade_item($this);
- $event->trigger();
- }
- return $result;
- }
- /**
- * Compares the values held by this object with those of the matching record in DB, and returns
- * whether or not these differences are sufficient to justify an update of all parent objects.
- * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
- *
- * @return bool
- */
- public function qualifies_for_regrading() {
- if (empty($this->id)) {
- return false;
- }
- $db_item = new grade_item(array('id' => $this->id));
- $calculationdiff = $db_item->calculation != $this->calculation;
- $categorydiff = $db_item->categoryid != $this->categoryid;
- $gradetypediff = $db_item->gradetype != $this->gradetype;
- $scaleiddiff = $db_item->scaleid != $this->scaleid;
- $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
- $locktimediff = $db_item->locktime != $this->locktime;
- $grademindiff = grade_floats_different($db_item->grademin, $this->grademin);
- $grademaxdiff = grade_floats_different($db_item->grademax, $this->grademax);
- $multfactordiff = grade_floats_different($db_item->multfactor, $this->multfactor);
- $plusfactordiff = grade_floats_different($db_item->plusfactor, $this->plusfactor);
- $acoefdiff = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
- $acoefdiff2 = grade_floats_different($db_item->aggregationcoef2, $this->aggregationcoef2);
- $weightoverride = grade_floats_different($db_item->weightoverride, $this->weightoverride);
- $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
- $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
- return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
- || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
- || $lockeddiff || $acoefdiff || $acoefdiff2 || $weightoverride || $locktimediff);
- }
- /**
- * Finds and returns a grade_item instance based on params.
- *
- * @static
- * @param array $params associative arrays varname=>value
- * @return grade_item|bool Returns a grade_item instance or false if none found
- */
- public static function fetch($params) {
- return grade_object::fetch_helper('grade_items', 'grade_item', $params);
- }
- /**
- * Check to see if there are any existing grades for this grade_item.
- *
- * @return boolean - true if there are valid grades for this grade_item.
- */
- public function has_grades() {
- global $DB;
- $count = $DB->count_records_select('grade_grades',
- 'itemid = :gradeitemid AND finalgrade IS NOT NULL',
- array('gradeitemid' => $this->id));
- return $count > 0;
- }
- /**
- * Check to see if there are existing overridden grades for this grade_item.
- *
- * @return boolean - true if there are overridden grades for this grade_item.
- */
- public function has_overridden_grades() {
- global $DB;
- $count = $DB->count_records_select('grade_grades',
- 'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
- array('gradeitemid' => $this->id));
- return $count > 0;
- }
- /**
- * Finds and returns all grade_item instances based on params.
- *
- * @static
- * @param array $params associative arrays varname=>value
- * @return array array of grade_item instances or false if none found.
- */
- public static function fetch_all($params) {
- return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
- }
- /**
- * Delete all grades and force_regrading of parent category.
- *
- * @param string $source from where was the object deleted (mod/forum, manual, etc.)
- * @return bool success
- */
- public function delete($source=null) {
- global $DB;
- $transaction = $DB->start_delegated_transaction();
- $this->delete_all_grades($source);
- $success = parent::delete($source);
- $transaction->allow_commit();
- return $success;
- }
- /**
- * Delete all grades
- *
- * @param string $source from where was the object deleted (mod/forum, manual, etc.)
- * @return bool
- */
- public function delete_all_grades($source=null) {
- global $DB;
- $transaction = $DB->start_delegated_transaction();
- if (!$this->is_course_item()) {
- $this->force_regrading();
- }
- if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
- foreach ($grades as $grade) {
- $grade->delete($source);
- }
- }
- // Delete all the historical files.
- // We only support feedback files for modules atm.
- if ($this->is_external_item()) {
- $fs = new file_storage();
- $fs->delete_area_files($this->get_context()->id, GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA);
- }
- $transaction->allow_commit();
- return true;
- }
- /**
- * In addition to perform parent::insert(), calls force_regrading() method too.
- *
- * @param string $source From where was the object inserted (mod/forum, manual, etc.)
- * @return int PK ID if successful, false otherwise
- */
- public function insert($source=null) {
- global $CFG, $DB;
- if (empty($this->courseid)) {
- print_error('cannotinsertgrade');
- }
- // load scale if needed
- $this->load_scale();
- // add parent category if needed
- if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
- $course_category = grade_category::fetch_course_category($this->courseid);
- $this->categoryid = $course_category->id;
- }
- // always place the new items at the end, move them after insert if needed
- $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
- if (!empty($last_sortorder)) {
- $this->sortorder = $last_sortorder + 1;
- } else {
- $this->sortorder = 1;
- }
- // add proper item numbers to manual items
- if ($this->itemtype == 'manual') {
- if (empty($this->itemnumber)) {
- $this->itemnumber = 0;
- }
- }
- // make sure there is not 0 in outcomeid
- if (empty($this->outcomeid)) {
- $this->outcomeid = null;
- }
- $this->timecreated = $this->timemodified = time();
- if (parent::insert($source)) {
- // force regrading of items if needed
- $this->force_regrading();
- $event = \core\event\grade_item_created::create_from_grade_item($this);
- $event->trigger();
- return $this->id;
- } else {
- debugging("Could not insert this grade_item in the database!");
- return false;
- }
- }
- /**
- * Set idnumber of grade item, updates also course_modules table
- *
- * @param string $idnumber (without magic quotes)
- * @return bool success
- */
- public function add_idnumber($idnumber) {
- global $DB;
- if (!empty($this->idnumber)) {
- return false;
- }
- if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
- if ($this->itemnumber == 0) {
- // for activity modules, itemnumber 0 is synced with the course_modules
- if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
- return false;
- }
- if (!empty($cm->idnumber)) {
- return false;
- }
- $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
- $this->idnumber = $idnumber;
- return $this->update();
- } else {
- $this->idnumber = $idnumber;
- return $this->update();
- }
- } else {
- $this->idnumber = $idnumber;
- return $this->update();
- }
- }
- /**
- * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
- * $userid is given) or the locked state of a specific grade within this item if a specific
- * $userid is given and the grade_item is unlocked.
- *
- * @param int $userid The user's ID
- * @return bool Locked state
- */
- public function is_locked($userid=NULL) {
- global $CFG;
- // Override for any grade items belonging to activities which are in the process of being deleted.
- require_once($CFG->dirroot . '/course/lib.php');
- if (course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance)) {
- return true;
- }
- if (!empty($this->locked)) {
- return true;
- }
- if (!empty($userid)) {
- if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
- $grade->grade_item =& $this; // prevent db fetching of cached grade_item
- return $grade->is_locked();
- }
- }
- return false;
- }
- /**
- * Locks or unlocks this grade_item and (optionally) all its associated final grades.
- *
- * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
- * @param bool $cascade Lock/unlock child objects too
- * @param bool $refresh Refresh grades when unlocking
- * @return bool True if grade_item all grades updated, false if at least one update fails
- */
- public function set_locked($lockedstate, $cascade=false, $refresh=true) {
- if ($lockedstate) {
- /// setting lock
- if ($this->needsupdate) {
- return false; // can not lock grade without first having final grade
- }
- $this->locked = time();
- $this->update();
- if ($cascade) {
- $grades = $this->get_final();
- foreach($grades as $g) {
- $grade = new grade_grade($g, false);
- $grade->grade_item =& $this;
- $grade->set_locked(1, null, false);
- }
- }
- return true;
- } else {
- /// removing lock
- if (!empty($this->locked) and $this->locktime < time()) {
- //we have to reset locktime or else it would lock up again
- $this->locktime = 0;
- }
- $this->locked = 0;
- $this->update();
- if ($cascade) {
- if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
- foreach($grades as $grade) {
- $grade->grade_item =& $this;
- $grade->set_locked(0, null, false);
- }
- }
- }
- if ($refresh) {
- //refresh when unlocking
- $this->refresh_grades();
- }
- return true;
- }
- }
- /**
- * Lock the grade if needed. Make sure this is called only when final grades are valid
- */
- public function check_locktime() {
- if (!empty($this->locked)) {
- return; // already locked
- }
- if ($this->locktime and $this->locktime < time()) {
- $this->locked = time();
- $this->update('locktime');
- }
- }
- /**
- * Set the locktime for this grade item.
- *
- * @param int $locktime timestamp for lock to activate
- * @return void
- */
- public function set_locktime($locktime) {
- $this->locktime = $locktime;
- $this->update();
- }
- /**
- * Set the locktime for this grade item.
- *
- * @return int $locktime timestamp for lock to activate
- */
- public function get_locktime() {
- return $this->locktime;
- }
- /**
- * Set the hidden status of grade_item and all grades.
- *
- * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
- *
- * @param int $hidden new hidden status
- * @param bool $cascade apply to child objects too
- */
- public function set_hidden($hidden, $cascade=false) {
- parent::set_hidden($hidden, $cascade);
- if ($cascade) {
- if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
- foreach($grades as $grade) {
- $grade->grade_item =& $this;
- $grade->set_hidden($hidden, $cascade);
- }
- }
- }
- //if marking item visible make sure category is visible MDL-21367
- if( !$hidden ) {
- $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
- if ($category_array && array_key_exists($this->categoryid, $category_array)) {
- $category = $category_array[$this->categoryid];
- //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
- $category->set_hidden($hidden, false);
- }
- }
- }
- /**
- * Returns the number of grades that are hidden
- *
- * @param string $groupsql SQL to limit the query by group
- * @param array $params SQL params for $groupsql
- * @param string $groupwheresql Where conditions for $groupsql
- * @return int The number of hidden grades
- */
- public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
- global $DB;
- $params = (array)$params;
- $params['itemid'] = $this->id;
- return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
- ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
- }
- /**
- * Mark regrading as finished successfully. This will also be called when subsequent regrading will not change any grades.
- * Situations such as an error being found will still result in the regrading being finished.
- */
- public function regrading_finished() {
- global $DB;
- $this->needsupdate = 0;
- //do not use $this->update() because we do not want this logged in grade_item_history
- $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
- }
- /**
- * Performs the necessary calculations on the grades_final referenced by this grade_item.
- * Also resets the needsupdate flag once successfully performed.
- *
- * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
- * because the regrading must be done in correct order!!
- *
- * @param int $userid Supply a user ID to limit the regrading to a single user
- * @return bool true if ok, error string otherwise
- */
- public function regrade_final_grades($userid=null) {
- global $CFG, $DB;
- // locked grade items already have correct final grades
- if ($this->is_locked()) {
- return true;
- }
- // calculation produces final value using formula from other final values
- if ($this->is_calculated()) {
- if ($this->compute($userid)) {
- return true;
- } else {
- return "Could not calculate grades for grade item"; // TODO: improve and localize
- }
- // noncalculated outcomes already have final values - raw grades not used
- } else if ($this->is_outcome_item()) {
- return true;
- // aggregate the category grade
- } else if ($this->is_category_item() or $this->is_course_item()) {
- // aggregate category grade item
- $category = $this->load_item_category();
- $category->grade_item =& $this;
- if ($category->generate_grades($userid)) {
- return true;
- } else {
- return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
- }
- } else if ($this->is_manual_item()) {
- // manual items track only final grades, no raw grades
- return true;
- } else if (!$this->is_raw_used()) {
- // hmm - raw grades are not used- nothing to regrade
- return true;
- }
- // normal grade item - just new final grades
- $result = true;
- $grade_inst = new grade_grade();
- $fields = implode(',', $grade_inst->required_fields);
- if ($userid) {
- $params = array($this->id, $userid);
- $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
- } else {
- $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
- }
- if ($rs) {
- foreach ($rs as $grade_record) {
- $grade = new grade_grade($grade_record, false);
- if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
- // this grade is locked - final grade must be ok
- continue;
- }
- $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
- if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
- $success = $grade->update('system');
- // If successful trigger a user_graded event.
- if ($success) {
- $grade->load_grade_item();
- \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
- } else {
- $result = "Internal error updating final grade";
- }
- }
- }
- $rs->close();
- }
- return $result;
- }
- /**
- * Given a float grade value or integer grade scale, applies a number of adjustment based on
- * grade_item variables and returns the result.
- *
- * @param float $rawgrade The raw grade value
- * @param float $rawmin original rawmin
- * @param float $rawmax original rawmax
- * @return mixed
- */
- public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
- if (is_null($rawgrade)) {
- return null;
- }
- if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
- if ($this->grademax < $this->grademin) {
- return null;
- }
- if ($this->grademax == $this->grademin) {
- return $this->grademax; // no range
- }
- // Standardise score to the new grade range
- // NOTE: skip if the activity provides a manual rescaling option.
- $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
- if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
- $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
- }
- // Apply other grade_item factors
- $rawgrade *= $this->multfactor;
- $rawgrade += $this->plusfactor;
- return $this->bounded_grade($rawgrade);
- } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
- if (empty($this->scale)) {
- $this->load_scale();
- }
- if ($this->grademax < 0) {
- return null; // scale not present - no grade
- }
- if ($this->grademax == 0) {
- return $this->grademax; // only one option
- }
- // Convert scale if needed
- // NOTE: skip if the activity provides a manual rescaling option.
- $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
- if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
- // This should never happen because scales are locked if they are in use.
- $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
- }
- return $this->bounded_grade($rawgrade);
- } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
- // somebody changed the grading type when grades already existed
- return null;
- } else {
- debugging("Unknown grade type");
- return null;
- }
- }
- /**
- * Update the rawgrademax and rawgrademin for all grade_grades records for this item.
- * Scale every rawgrade to maintain the percentage. This function should be called
- * after the gradeitem has been updated to the new min and max values.
- *
- * @param float $oldgrademin The previous grade min value
- * @param float $oldgrademax The previous grade max value
- * @param float $newgrademin The new grade min value
- * @param float $newgrademax The new grade max value
- * @param string $source from where was the object inserted (mod/forum, manual, etc.)
- * @return bool True on success
- */
- public function rescale_grades_keep_percentage($oldgrademin, $oldgrademax, $newgrademin, $newgrademax, $source = null) {
- global $DB;
- if (empty($this->id)) {
- return false;
- }
- if ($oldgrademax <= $oldgrademin) {
- // Grades cannot be scaled.
- return false;
- }
- $scale = ($newgrademax - $newgrademin) / ($oldgrademax - $oldgrademin);
- if (($newgrademax - $newgrademin) <= 1) {
- // We would lose too much precision, lets bail.
- return false;
- }
- $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id));
- foreach ($rs as $graderecord) {
- // For each record, create an object to work on.
- $grade = new grade_grade($graderecord, false);
- // Set this object in the item so it doesn't re-fetch it.
- $grade->grade_item = $this;
- if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) {
- // Updating the raw grade automatically updates the min/max.
- if ($this->is_raw_used()) {
- $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
- $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
- } else {
- $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
- $this->update_final_grade($grade->userid, $finalgrade, $source);
- }
- }
- }
- $rs->close();
- // Mark this item for regrading.
- $this->force_regrading();
- return true;
- }
- /**
- * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
- *
- * @return void
- */
- public function force_regrading() {
- global $DB;
- $this->needsupdate = 1;
- //mark this item and course item only - categories and calculated items are always regraded
- $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
- $params = array($this->id, $this->courseid);
- $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
- }
- /**
- * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
- *
- * @return grade_scale Returns a grade_scale object or null if no scale used
- */
- public function load_scale() {
- if ($this->gradetype != GRADE_TYPE_SCALE) {
- $this->scaleid = null;
- }
- if (!empty($this->scaleid)) {
- //do not load scale if already present
- if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
- $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
- if (!$this->scale) {
- debugging('Incorrect scale id: '.$this->scaleid);
- $this->scale = null;
- return null;
- }
- $this->scale->load_items();
- }
- // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
- // stay with the current min=1 max=count(scaleitems)
- $this->grademax = count($this->scale->scale_items);
- $this->grademin = 1;
- } else {
- $this->scale = null;
- }
- return $this->scale;
- }
- /**
- * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
- *
- * @return grade_outcome This grade item's associated grade_outcome or null
- */
- public function load_outcome() {
- if (!empty($this->outcomeid)) {
- $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
- }
- return $this->outcome;
- }
- /**
- * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
- * or category attached to category item.
- *
- * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
- */
- public function get_parent_category() {
- if ($this->is_category_item() or $this->is_course_item()) {
- return $this->get_item_category();
- } else {
- return grade_category::fetch(array('id'=>$this->categoryid));
- }
- }
- /**
- * Calls upon the get_parent_category method to retrieve the grade_category object
- * from the DB and assigns it to $this->parent_category. It also returns the object.
- *
- * @return grade_category This grade item's parent grade_category.
- */
- public function load_parent_category() {
- if (empty($this->parent_category->id)) {
- $this->parent_category = $this->get_parent_category();
- }
- return $this->parent_category;
- }
- /**
- * Returns the grade_category for a grade category grade item
- *
- * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
- */
- public function get_item_category() {
- if (!$this->is_course_item() and !$this->is_category_item()) {
- return false;
- }
- return grade_category::fetch(array('id'=>$this->iteminstance));
- }
- /**
- * Calls upon the get_item_category method to retrieve the grade_category object
- * from the DB and assigns it to $this->item_category. It also returns the object.
- *
- * @return grade_category
- */
- public function load_item_category() {
- if (empty($this->item_category->id)) {
- $this->item_category = $this->get_item_category();
- }
- return $this->item_category;
- }
- /**
- * Is the grade item associated with category?
- *
- * @return bool
- */
- public function is_category_item() {
- return ($this->itemtype == 'category');
- }
- /**
- * Is the grade item associated with course?
- *
- * @return bool
- */
- public function is_course_item() {
- return ($this->itemtype == 'course');
- }
- /**
- * Is this a manually graded item?
- *
- * @return bool
- */
- public function is_manual_item() {
- return ($this->itemtype == 'manual');
- }
- /**
- * Is this an outcome item?
- *
- * @return bool
- */
- public function is_outcome_item() {
- return !empty($this->outcomeid);
- }
- /**
- * Is the grade item external - associated with module, plugin or something else?
- *
- * @return bool
- */
- public function is_external_item() {
- return ($this->itemtype == 'mod');
- }
- /**
- * Is the grade item overridable
- *
- * @return bool
- */
- public function is_overridable_item() {
- if ($this->is_course_item() or $this->is_category_item()) {
- $overridable = (bool) get_config('moodle', 'grade_overridecat');
- } else {
- $overridable = false;
- }
- return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $overridable);
- }
- /**
- * Is the grade item feedback overridable
- *
- * @return bool
- */
- public function is_overridable_item_feedback() {
- return !$this->is_outcome_item() and $this->is_external_item();
- }
- /**
- * Returns true if grade items uses raw grades
- *
- * @return bool
- */
- public function is_raw_used() {
- return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
- }
- /**
- * Returns true if the grade item is an aggreggated type grade.
- *
- * @since Moodle 2.8.7, 2.9.1
- * @return bool
- */
- public function is_aggregate_item() {
- return ($this->is_category_item() || $this->is_course_item());
- }
- /**
- * Returns the grade item associated with the course
- *
- * @param int $courseid
- * @return grade_item Course level grade item object
- */
- public static function fetch_course_item($courseid) {
- if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
- return $course_item;
- }
- // first get category - it creates the associated grade item
- $course_category = grade_category::fetch_course_category($courseid);
- return $course_category->get_grade_item();
- }
- /**
- * Is grading object editable?
- *
- * @return bool
- */
- public function is_editable() {
- return true;
- }
- /**
- * Checks if grade calculated. Returns this object's calculation.
- *
- * @return bool true if grade item calculated.
- */
- public function is_calculated() {
- if (empty($this->calculation)) {
- return false;
- }
- /*
- * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
- * we would have to fetch all course grade items to find out the ids.
- * Also if user changes the idnumber the formula does not need to be updated.
- */
- // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
- if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
- $this->set_calculation($this->calculation);
- }
- return !empty($this->calculation);
- }
- /**
- * Returns calculation string if grade calculated.
- *
- * @return string Returns the grade item's calculation if calculation is used, null if not
- */
- public function get_calculation() {
- if ($this->is_calculated()) {
- return grade_item::denormalize_formula($this->calculation, $this->courseid);
- } else {
- return NULL;
- }
- }
- /**
- * Sets this item's calculation (creates it) if not yet set, or
- * updates it if already set (in the DB). If no calculation is given,
- * the calculation is removed.
- *
- * @param string $formula string representation of formula used for calculation
- * @return bool success
- */
- public function set_calculation($formula) {
- $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
- $this->calculation_normalized = true;
- return $this->update();
- }
- /**
- * Denormalizes the calculation formula to [idnumber] form
- *
- * @param string $formula A string representation of the formula
- * @param int $courseid The course ID
- * @return string The denormalized formula as a string
- */
- public static function denormalize_formula($formula, $courseid) {
- if (empty($formula)) {
- return '';
- }
- // denormalize formula - convert ##giXX## to [[idnumber]]
- if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
- foreach ($matches[1] as $id) {
- if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
- if (!empty($grade_item->idnumber)) {
- $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
- }
- }
- }
- }
- return $formula;
- }
- /**
- * Normalizes the calculation formula to [#giXX#] form
- *
- * @param string $formula The formula
- * @param int $courseid The course ID
- * @return string The normalized formula as a string
- */
- public static function normalize_formula($formula, $courseid) {
- $formula = trim($formula);
- if (empty($formula)) {
- return NULL;
- }
- // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
- if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
- foreach ($grade_items as $grade_item) {
- $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
- }
- }
- return $formula;
- }
- /**
- * Returns the final values for this grade item (as imported by module or other source).
- *
- * @param int $userid Optional: to retrieve a single user's final grade
- * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
- */
- public function get_final($userid=NULL) {
- global $DB;
- if ($userid) {
- if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
- return $user;
- }
- } else {
- if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
- //TODO: speed up with better SQL (MDL-31380)
- $result = array();
- foreach ($grades as $grade) {
- $result[$grade->userid] = $grade;
- }
- return $result;
- } else {
- return array();
- }
- }
- }
- /**
- * Get (or create if not exist yet) grade for this user
- *
- * @param int $userid The user ID
- * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
- * @return grade_grade The grade_grade instance for the user for this grade item
- */
- public function get_grade($userid, $create=true) {
- if (empty($this->id)) {
- debugging('Can not use before insert');
- return false;
- }
- $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
- if (empty($grade->id) and $create) {
- $grade->insert();
- }
- return $grade;
- }
- /**
- * Returns the sortorder of this grade_item. This method is also available in
- * grade_category, for cases where the object type is not know.
- *
- * @return int Sort order
- */
- public function get_sortorder() {
- return $this->sortorder;
- }
- /**
- * Returns the idnumber of this grade_item. This method is also available in
- * grade_category, for cases where the object type is not know.
- *
- * @return string The grade item idnumber
- */
- public function get_idnumber() {
- return $this->idnumber;
- }
- /**
- * Returns this grade_item. This method is also available in
- * grade_category, for cases where the object type is not know.
- *
- * @return grade_item
- */
- public function get_grade_item() {
- return $this;
- }
- /**
- * Sets the sortorder of this grade_item. This method is also available in
- * grade_category, for cases where the object type is not know.
- *
- * @param int $sortorder
- */
- public function set_sortorder($sortorder) {
- if ($this->sortorder == $sortorder) {
- return;
- }
- $this->sortorder = $sortorder;
- $this->update();
- }
- /**
- * Update this grade item's sortorder so that it will appear after $sortorder
- *
- * @param int $sortorder The sort order to place this grade item after
- */
- public function move_after_sortorder($sortorder) {
- global $CFG, $DB;
- //make some room first
- $params = array($sortorder, $this->courseid);
- $sql = "UPDATE {grade_items}
- SET sortorder = sortorder + 1
- WHERE sortorder > ? AND courseid = ?";
- $DB->execute($sql, $params);
- $this->set_sortorder($sortorder + 1);
- }
- /**
- * Detect duplicate grade item's sortorder and re-sort them.
- * Note: Duplicate sortorder will be introduced while duplicating activities or
- * merging two courses.
- *
- * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
- */
- public static function fix_duplicate_sortorder($courseid) {
- global $DB;
- $transaction = $DB->start_delegated_transaction();
- $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
- FROM {grade_items} g1
- JOIN {grade_items} g2 ON g1.courseid = g2.courseid
- WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
- ORDER BY g1.sortorder DESC, g1.id DESC";
- // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
- // bottom higher end of the sort orders and work down by id.
- $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
- foreach($rs as $duplicate) {
- $DB->execute("UPDATE {grade_items}
- SET sortorder = sortorder + 1
- WHERE courseid = :courseid AND
- (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
- array('courseid' => $duplicate->courseid,
- 'sortorder' => $duplicate->sortorder,
- 'sortorder2' => $duplicate->sortorder,
- 'id' => $duplicate->id));
- }
- $rs->close();
- $transaction->allow_commit();
- }
- /**
- * Returns the most descriptive field for this object.
- *
- * Determines what type of grade item it is then returns the appropriate string
- *
- * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
- * @return string name
- */
- public function get_name($fulltotal=false) {
- global $CFG;
- require_once($CFG->dirroot . '/course/lib.php');
- if (strval($this->itemname) !== '') {
- // MDL-10557
- // Make it obvious to users if the course module to which this grade item relates, is currently being removed.
- $deletionpending = course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance);
- $deletionnotice = get_string('gradesmoduledeletionprefix', 'grades');
- $options = ['context' => context_course::instance($this->courseid)];
- return $deletionpending ?
- format_string($deletionnotice . ' ' . $this->itemname, true, $options) :
- format_string($this->itemname, true, $options);
- } else if ($this->is_course_item()) {
- return get_string('coursetotal', 'grades');
- } else if ($this->is_category_item()) {
- if ($fulltotal) {
- $category = $this->load_parent_category();
- $a = new stdClass();
- $a->category = $category->get_name();
- return get_string('categorytotalfull', 'grades', $a);
- } else {
- return get_string('categorytotal', 'grades');
- }
- } else {
- return get_string('grade');
- }
- }
- /**
- *…
Large files files are truncated, but you can click here to view the full file