Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?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;/*** @var bool If we regrade this item should we mark it as overridden?*/public $markasoverriddenwhengraded = true;/*** @var int course module ID*/public $cmid;/*** @var string average information.*/public $avg;/*** Category name.* @var string*/public $category;/*** 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.)* @param bool $isbulkupdate If bulk grade update is happening.* @return bool success*/public function update($source = null, $isbulkupdate = false) {// 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 outcomeidif (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, $isbulkupdate);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 unlockingreturn ($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;try {$transaction = $DB->start_delegated_transaction();$this->delete_all_grades($source);$success = parent::delete($source);if ($success) {$event = \core\event\grade_item_deleted::create_from_grade_item($this);$event->trigger();}$transaction->allow_commit();} catch (Exception $e) {$transaction->rollback($e);}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;try {$transaction = $DB->start_delegated_transaction();if (!$this->is_course_item()) {$this->force_regrading();}if ($grades = grade_grade::fetch_all(['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();} catch (Exception $e) {$transaction->rollback($e);}return true;}/*** Duplicate grade item.** @return grade_item The duplicate grade item*/public function duplicate() {// Convert current object to array.$copy = (array) $this;if (empty($copy["id"])) {throw new moodle_exception('invalidgradeitemid');}// Remove fields that will be either unique or automatically filled.$removekeys = array();$removekeys[] = 'id';$removekeys[] = 'idnumber';$removekeys[] = 'timecreated';$removekeys[] = 'sortorder';foreach ($removekeys as $key) {unset($copy[$key]);}// Addendum to name.$copy["itemname"] = get_string('duplicatedgradeitem', 'grades', $copy["itemname"]);// Create new grade item.$gradeitem = new grade_item($copy);// Insert grade item into database.$gradeitem->insert();return $gradeitem;}/*** In addition to perform parent::insert(), calls force_regrading() method too.** @param string $source From where was the object inserted (mod/forum, manual, etc.)* @param string $isbulkupdate If bulk grade update is happening.* @return int PK ID if successful, false otherwise*/public function insert($source = null, $isbulkupdate = false) {global $CFG, $DB;if (empty($this->courseid)) {throw new \moodle_exception('cannotinsertgrade');}// load scale if needed$this->load_scale();// add parent category if neededif (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 itemsif ($this->itemtype == 'manual') {if (empty($this->itemnumber)) {$this->itemnumber = 0;}}// make sure there is not 0 in outcomeidif (empty($this->outcomeid)) {$this->outcomeid = null;}$this->timecreated = $this->timemodified = time();if (parent::insert($source, $isbulkupdate)) {// 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_modulesif (!$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_itemreturn $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 (empty($this->id)) {return false;} else if ($this->needsupdate) {// Can not lock grade without first having final grade,// so we schedule it to be locked as soon as regrading is finished.$this->locktime = time() - 1;} else {$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 lockif (!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-21367if( !$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* @param \core\progress\base|null $progress Optional progress object, will be updated per user* @return bool true if ok, error string otherwise*/public function regrade_final_grades($userid=null, ?\core\progress\base $progress = null) {global $CFG, $DB;// locked grade items already have correct final gradesif ($this->is_locked()) {return true;}// calculation produces final value using formula from other final valuesif ($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, $progress)) {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 gradesreturn true;} else if (!$this->is_raw_used()) {// hmm - raw grades are not used- nothing to regradereturn 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);// Incrementing the progress by nothing causes it to send an update (once per second)// to the web browser so as to prevent the connection timing out.if ($progress) {$progress->increment_progress(0);}if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {// this grade is locked - final grade must be okcontinue;}$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 gradeif ($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 valueif (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 existedreturn 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 presentif (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 + 1WHERE 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.sortorderFROM {grade_items} g1JOIN {grade_items} g2 ON g1.courseid = g2.courseidWHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseidORDER 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 + 1WHERE 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"* @param bool $escape Whether the returned category name is to be HTML escaped or not.* @return string name*/public function get_name($fulltotal=false, $escape = true) {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), 'escape' => $escape];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($escape);return get_string('categorytotalfull', 'grades', $a);} else {return get_string('categorytotal', 'grades');}} else {return get_string('gradenoun');}}/*** A grade item can return a more detailed description which will be added to the header of the column/row in some reports.** @return string description*/public function get_description() {if ($this->is_course_item() || $this->is_category_item()) {$categoryitem = $this->load_item_category();return $categoryitem->get_description();}return '';}/*** Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.** @param int $parentid The ID of the new parent* @param bool $updateaggregationfields Whether or not to convert the aggregation fields when switching between category.* Set this to false when the aggregation fields have been updated in prevision of the new* category, typically when the item is freshly created.* @return bool True if success*/public function set_parent($parentid, $updateaggregationfields = true) {if ($this->is_course_item() or $this->is_category_item()) {throw new \moodle_exception('cannotsetparentforcatoritem');}if ($this->categoryid == $parentid) {return true;}// find parent and check course idif (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {return false;}$currentparent = $this->load_parent_category();if ($updateaggregationfields) {$this->set_aggregation_fields_for_aggregation($currentparent->aggregation, $parent_category->aggregation);}$this->force_regrading();// set new parent$this->categoryid = $parent_category->id;$this->parent_category =& $parent_category;return $this->update();}/*** Update the aggregation fields when the aggregation changed.** This method should always be called when the aggregation has changed, but also when* the item was moved to another category, even it if uses the same aggregation method.** Some values such as the weight only make sense within a category, once moved the* values should be reset to let the user adapt them accordingly.** Note that this method does not save the grade item.* {@link grade_item::update()} has to be called manually after using this method.** @param int $from Aggregation method constant value.* @param int $to Aggregation method constant value.* @return boolean True when at least one field was changed, false otherwise*/public function set_aggregation_fields_for_aggregation($from, $to) {$defaults = grade_category::get_default_aggregation_coefficient_values($to);$origaggregationcoef = $this->aggregationcoef;$origaggregationcoef2 = $this->aggregationcoef2;$origweighoverride = $this->weightoverride;if ($from == GRADE_AGGREGATE_SUM && $to == GRADE_AGGREGATE_SUM && $this->weightoverride) {// Do nothing. We are switching from SUM to SUM and the weight is overriden,// a teacher would not expect any change in this situation.} else if ($from == GRADE_AGGREGATE_WEIGHTED_MEAN && $to == GRADE_AGGREGATE_WEIGHTED_MEAN) {// Do nothing. The weights can be kept in this case.} else if (in_array($from, array(GRADE_AGGREGATE_SUM, GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))&& in_array($to, array(GRADE_AGGREGATE_SUM, GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))) {// Reset all but the the extra credit field.$this->aggregationcoef2 = $defaults['aggregationcoef2'];$this->weightoverride = $defaults['weightoverride'];if ($to != GRADE_AGGREGATE_EXTRACREDIT_MEAN) {// Normalise extra credit, except for 'Mean with extra credit' which supports higher values than 1.$this->aggregationcoef = min(1, $this->aggregationcoef);}} else {// Reset all.$this->aggregationcoef = $defaults['aggregationcoef'];$this->aggregationcoef2 = $defaults['aggregationcoef2'];$this->weightoverride = $defaults['weightoverride'];}$acoefdiff = grade_floats_different($origaggregationcoef, $this->aggregationcoef);$acoefdiff2 = grade_floats_different($origaggregationcoef2, $this->aggregationcoef2);$weightoverride = grade_floats_different($origweighoverride, $this->weightoverride);return $acoefdiff || $acoefdiff2 || $weightoverride;}/*** Makes sure value is a valid grade value.** @param float $gradevalue* @return mixed float or int fixed grade value*/public function bounded_grade($gradevalue) {global $CFG;if (is_null($gradevalue)) {return null;}if ($this->gradetype == GRADE_TYPE_SCALE) {// no >100% grades hack for scale grades!// 1.5 is rounded to 2 ;-)return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);}$grademax = $this->grademax;// NOTE: if you change this value you must manually reset the needsupdate flag in all grade items$maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by defaultif (!empty($CFG->unlimitedgrades)) {// NOTE: if you change this value you must manually reset the needsupdate flag in all grade items$grademax = $grademax * $maxcoef;} else if ($this->is_category_item() or $this->is_course_item()) {$category = $this->load_item_category();if ($category->aggregation >= 100) {// grade >100% hack$grademax = $grademax * $maxcoef;}}return (float)bounded_number($this->grademin, $gradevalue, $grademax);}/*** Finds out on which other items does this depend directly when doing calculation or category aggregation** @param bool $reset_cache* @return array of grade_item IDs this one depends on*/public function depends_on($reset_cache=false) {global $CFG, $DB;if ($reset_cache) {$this->dependson_cache = null;} else if (isset($this->dependson_cache)) {return $this->dependson_cache;}if ($this->is_locked() && !$this->is_category_item()) {// locked items do not need to be regraded$this->dependson_cache = array();return $this->dependson_cache;}if ($this->is_calculated()) {if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {$this->dependson_cache = array_unique($matches[1]); // remove duplicatesreturn $this->dependson_cache;} else {$this->dependson_cache = array();return $this->dependson_cache;}} else if ($grade_category = $this->load_item_category()) {$params = array();//only items with numeric or scale values can be aggregatedif ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {$this->dependson_cache = array();return $this->dependson_cache;}$grade_category->apply_forced_settings();if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {$outcomes_sql = "";} else {$outcomes_sql = "AND gi.outcomeid IS NULL";}if (empty($CFG->grade_includescalesinaggregation)) {$gtypes = "gi.gradetype = ?";$params[] = GRADE_TYPE_VALUE;} else {$gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";$params[] = GRADE_TYPE_VALUE;$params[] = GRADE_TYPE_SCALE;}$params[] = $grade_category->id;$params[] = $this->courseid;$params[] = $grade_category->id;$params[] = $this->courseid;if (empty($CFG->grade_includescalesinaggregation)) {$params[] = GRADE_TYPE_VALUE;} else {$params[] = GRADE_TYPE_VALUE;$params[] = GRADE_TYPE_SCALE;}$sql = "SELECT gi.idFROM {grade_items} giWHERE $gtypesAND gi.categoryid = ?AND gi.courseid = ?$outcomes_sqlUNIONSELECT gi.idFROM {grade_items} gi, {grade_categories} gcWHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.idAND gc.parent = ?AND gi.courseid = ?AND $gtypes$outcomes_sql";if ($children = $DB->get_records_sql($sql, $params)) {$this->dependson_cache = array_keys($children);return $this->dependson_cache;} else {$this->dependson_cache = array();return $this->dependson_cache;}} else {$this->dependson_cache = array();return $this->dependson_cache;}}/*** Refetch grades from modules, plugins.** @param int $userid optional, limit the refetch to a single user* @return bool Returns true on success or if there is nothing to do*/public function refresh_grades($userid=0) {global $DB;if ($this->itemtype == 'mod') {if ($this->is_outcome_item()) {//nothing to doreturn true;}if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {debugging("Can not find $this->itemmodule activity with id $this->iteminstance");return false;}if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {debugging('Can not find course module');return false;}$activity->modname = $this->itemmodule;$activity->cmidnumber = $cm->idnumber;return grade_update_mod_grades($activity, $userid);}return true;}/*** Updates final grade value for given user, this is a only way to update final* grades from gradebook and import because it logs the change in history table* and deals with overridden flag. This flag is set to prevent later overriding* from raw grades submitted from modules.** @param int $userid The graded user* @param float|false $finalgrade The float value of final grade, false means do not change* @param string $source The modification source* @param string $feedback Optional teacher feedback* @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML* @param int $usermodified The ID of the user making the modification* @param int $timemodified Optional parameter to set the time modified, if not present current time.* @param bool $isbulkupdate If bulk grade update is happening.* @return bool success*/public function update_final_grade($userid, $finalgrade = false, $source = null, $feedback = false,$feedbackformat = FORMAT_MOODLE, $usermodified = null, $timemodified = null, $isbulkupdate = false) {global $USER, $CFG;$result = true;// no grading used or lockedif ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {return false;}$grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));$grade->grade_item =& $this; // prevent db fetching of this grade_itemif (empty($usermodified)) {$grade->usermodified = $USER->id;} else {$grade->usermodified = $usermodified;}if ($grade->is_locked()) {// do not update locked grades at allreturn false;}$locktime = $grade->get_locktime();if ($locktime and $locktime < time()) {// do not update grades that should be already locked, force regrade instead$this->force_regrading();return false;}$oldgrade = new stdClass();$oldgrade->finalgrade = $grade->finalgrade;$oldgrade->overridden = $grade->overridden;$oldgrade->feedback = $grade->feedback;$oldgrade->feedbackformat = $grade->feedbackformat;$oldgrade->rawgrademin = $grade->rawgrademin;$oldgrade->rawgrademax = $grade->rawgrademax;// MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.$grade->rawgrademin = $this->grademin;$grade->rawgrademax = $this->grademax;$grade->rawscaleid = $this->scaleid;// changed grade?if ($finalgrade !== false) {if ($this->is_overridable_item() && $this->markasoverriddenwhengraded) {$grade->overridden = time();}$grade->finalgrade = $this->bounded_grade($finalgrade);}// do we have comment from teacher?if ($feedback !== false) {if ($this->is_overridable_item_feedback()) {// external items (modules, plugins) may have own feedback$grade->overridden = time();}$grade->feedback = $feedback;$grade->feedbackformat = $feedbackformat;}$gradechanged = false;if (empty($grade->id)) {$grade->timecreated = null; // Hack alert - date submitted - no submission yet.$grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.$result = (bool)$grade->insert($source, $isbulkupdate);// If the grade insert was successful and the final grade was not null then trigger a user_graded event.if ($result && !is_null($grade->finalgrade)) {\core\event\user_graded::create_from_grade($grade)->trigger();}$gradechanged = true;} else {// Existing grade_grades.if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {$gradechanged = true;}if ($grade->feedback === $oldgrade->feedback and $grade->feedbackformat == $oldgrade->feedbackformat and$gradechanged === false) {// No grade nor feedback changed.return $result;}$grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.$result = $grade->update($source, $isbulkupdate);// If the grade update was successful and the actual grade has changed then trigger a user_graded event.if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {\core\event\user_graded::create_from_grade($grade)->trigger();}}if (!$result) {// Something went wrong - better force final grade recalculation.$this->force_regrading();return $result;}// If we are not updating grades we don't need to recalculate the whole course.if (!$gradechanged) {return $result;}if ($this->is_course_item() and !$this->needsupdate) {if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {$this->force_regrading();}} else if (!$this->needsupdate) {$course_item = grade_item::fetch_course_item($this->courseid);if (!$course_item->needsupdate) {if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {$this->force_regrading();}} else {$this->force_regrading();}}return $result;}/*** Updates raw grade value for given user, this is a only way to update raw* grades from external source (modules, etc.),* because it logs the change in history table and deals with final grade recalculation.** @param int $userid the graded user* @param mixed $rawgrade float value of raw grade - false means do not change* @param string $source modification source* @param string $feedback optional teacher feedback* @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML* @param int $usermodified the ID of the user who did the grading* @param int $dategraded A timestamp of when the student's work was graded* @param int $datesubmitted A timestamp of when the student's work was submitted* @param grade_grade $grade A grade object, useful for bulk upgrades* @param array $feedbackfiles An array identifying the location of files we want to copy to the gradebook feedback area.* Example -* [* 'contextid' => 1,* 'component' => 'mod_xyz',* 'filearea' => 'mod_xyz_feedback',* 'itemid' => 2* ];* @param bool $isbulkupdate If bulk grade update is happening.* @return bool success*/public function update_raw_grade($userid, $rawgrade = false, $source = null, $feedback = false,$feedbackformat = FORMAT_MOODLE, $usermodified = null, $dategraded = null, $datesubmitted=null,$grade = null, array $feedbackfiles = [], $isbulkupdate = false) {global $USER;$result = true;// calculated grades can not be updated; course and category can not be updated because they are aggregatedif (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {return false;}if (is_null($grade)) {//fetch from db$grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));}$grade->grade_item =& $this; // prevent db fetching of this grade_itemif (empty($usermodified)) {$grade->usermodified = $USER->id;} else {$grade->usermodified = $usermodified;}if ($grade->is_locked()) {// do not update locked grades at allreturn false;}$locktime = $grade->get_locktime();if ($locktime and $locktime < time()) {// do not update grades that should be already locked and force regrade$this->force_regrading();return false;}$oldgrade = new stdClass();$oldgrade->finalgrade = $grade->finalgrade;$oldgrade->rawgrade = $grade->rawgrade;$oldgrade->rawgrademin = $grade->rawgrademin;$oldgrade->rawgrademax = $grade->rawgrademax;$oldgrade->rawscaleid = $grade->rawscaleid;$oldgrade->feedback = $grade->feedback;$oldgrade->feedbackformat = $grade->feedbackformat;// use new min and max$grade->rawgrade = $grade->rawgrade;$grade->rawgrademin = $this->grademin;$grade->rawgrademax = $this->grademax;$grade->rawscaleid = $this->scaleid;// change raw grade?if ($rawgrade !== false) {$grade->rawgrade = $rawgrade;}// empty feedback means no feedback at allif ($feedback === '') {$feedback = null;}// do we have comment from teacher?if ($feedback !== false and !$grade->is_overridden()) {$grade->feedback = $feedback;$grade->feedbackformat = $feedbackformat;$grade->feedbackfiles = $feedbackfiles;}// update final grade if possibleif (!$grade->is_locked() and !$grade->is_overridden()) {$grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);}// TODO: hack alert - create new fields for these in 2.0$oldgrade->timecreated = $grade->timecreated;$oldgrade->timemodified = $grade->timemodified;$grade->timecreated = $datesubmitted;if ($grade->is_overridden()) {// keep original graded date - update_final_grade() sets this for overridden grades} else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {// no grade and feedback means no grading yet$grade->timemodified = null;} else if (!empty($dategraded)) {// fine - module sends info when graded (yay!)$grade->timemodified = $dategraded;} else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)or $grade->feedback !== $oldgrade->feedback) {// guess - if either grade or feedback changed set new graded date$grade->timemodified = time();} else {//keep original graded date}// end of hack alert$gradechanged = false;if (empty($grade->id)) {$result = (bool)$grade->insert($source, $isbulkupdate);// If the grade insert was successful and the final grade was not null then trigger a user_graded event.if ($result && !is_null($grade->finalgrade)) {\core\event\user_graded::create_from_grade($grade)->trigger();}$gradechanged = true;} else {// Existing grade_grades.if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)or grade_floats_different($grade->rawgrade, $oldgrade->rawgrade)or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)or $grade->rawscaleid != $oldgrade->rawscaleid) {$gradechanged = true;}// The timecreated and timemodified checking is part of the hack above.if ($gradechanged === false and$grade->feedback === $oldgrade->feedback and$grade->feedbackformat == $oldgrade->feedbackformat and$grade->timecreated == $oldgrade->timecreated and$grade->timemodified == $oldgrade->timemodified) {// No changes.return $result;}$result = $grade->update($source, $isbulkupdate);// If the grade update was successful and the actual grade has changed then trigger a user_graded event.if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {\core\event\user_graded::create_from_grade($grade)->trigger();}}if (!$result) {// Something went wrong - better force final grade recalculation.$this->force_regrading();return $result;}// If we are not updating grades we don't need to recalculate the whole course.if (!$gradechanged) {return $result;}if (!$this->needsupdate) {$course_item = grade_item::fetch_course_item($this->courseid);if (!$course_item->needsupdate) {if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {$this->force_regrading();}}}return $result;}/*** Calculates final grade values using the formula in the calculation property.* The parameters are taken from final grades of grade items in current course only.** @param int $userid Supply a user ID to limit the calculations to the grades of a single user* @return bool false if error*/public function compute($userid=null) {global $CFG, $DB;if (!$this->is_calculated()) {return false;}require_once($CFG->libdir.'/mathslib.php');if ($this->is_locked()) {return true; // no need to recalculate locked items}// Precreate grades - we need them to existif ($userid) {$missing = array();if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {$m = new stdClass();$m->userid = $userid;$missing[] = $m;}} else {// Find any users who have grades for some but not all grade items in this course$params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);$sql = "SELECT gg.useridFROM {grade_grades} ggJOIN {grade_items} giON (gi.id = gg.itemid AND gi.courseid = :gicourseid)GROUP BY gg.useridHAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";$missing = $DB->get_records_sql($sql, $params);}if ($missing) {foreach ($missing as $m) {$grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);$grade->grade_item =& $this;$grade->insert('system');}}// get used items$useditems = $this->depends_on();// prepare formula and init maths library$formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);if (strpos($formula, '[[') !== false) {// missing itemreturn false;}$this->formula = new calc_formula($formula);// where to look for final grades?// this itemid is added so that we use only one query for source and final grades$gis = array_merge($useditems, array($this->id));list($usql, $params) = $DB->get_in_or_equal($gis);if ($userid) {$usersql = "AND g.userid=?";$params[] = $userid;} else {$usersql = "";}$grade_inst = new grade_grade();$fields = 'g.'.implode(',g.', $grade_inst->required_fields);$params[] = $this->courseid;$sql = "SELECT $fieldsFROM {grade_grades} g, {grade_items} giWHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?ORDER BY g.userid";$return = true;// group the grades by userid and use formula on the group$rs = $DB->get_recordset_sql($sql, $params);if ($rs->valid()) {$prevuser = 0;$grade_records = array();$oldgrade = null;foreach ($rs as $used) {if ($used->userid != $prevuser) {if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {$return = false;}$prevuser = $used->userid;$grade_records = array();$oldgrade = null;}if ($used->itemid == $this->id) {$oldgrade = $used;}$grade_records['gi'.$used->itemid] = $used->finalgrade;}if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {$return = false;}}$rs->close();return $return;}/*** Internal function that does the final grade calculation** @param int $userid The user ID* @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade* @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID* @param grade_grade $oldgrade A grade_grade instance containing the old values from the database* @return bool False if an error occurred*/public function use_formula($userid, $params, $useditems, $oldgrade) {if (empty($userid)) {return true;}// add missing final grade values// not graded (null) is counted as 0 - the spreadsheet way$allinputsnull = true;foreach($useditems as $gi) {if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {$params['gi'.$gi] = 0;} else {$params['gi'.$gi] = (float)$params['gi'.$gi];if ($gi != $this->id) {$allinputsnull = false;}}}// can not use own final grade during calculationunset($params['gi'.$this->id]);// Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they// wish to update the grades.$gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);$rawminandmaxchanged = false;// insert final grade - will be needed later anywayif ($oldgrade) {// Only run through this code if the gradebook isn't frozen.if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {// Do nothing.} else {// The grade_grade for a calculated item should have the raw grade maximum and minimum set to the// grade_item grade maximum and minimum respectively.if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) {$rawminandmaxchanged = true;$oldgrade->rawgrademax = $this->grademax;$oldgrade->rawgrademin = $this->grademin;}}$oldfinalgrade = $oldgrade->finalgrade;$grade = new grade_grade($oldgrade, false); // fetching from db is not needed$grade->grade_item =& $this;} else {$grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);$grade->grade_item =& $this;$rawminandmaxchanged = false;if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {// Do nothing.} else {// The grade_grade for a calculated item should have the raw grade maximum and minimum set to the// grade_item grade maximum and minimum respectively.$rawminandmaxchanged = true;$grade->rawgrademax = $this->grademax;$grade->rawgrademin = $this->grademin;}$grade->insert('system');$oldfinalgrade = null;}// no need to recalculate locked or overridden gradesif ($grade->is_locked() or $grade->is_overridden()) {return true;}if ($allinputsnull) {$grade->finalgrade = null;$result = true;} else {// do the calculation$this->formula->set_params($params);$result = $this->formula->evaluate();if ($result === false) {$grade->finalgrade = null;} else {// normalize$grade->finalgrade = $this->bounded_grade($result);}}// Only run through this code if the gradebook isn't frozen.if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {// Update in db if changed.if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {$grade->timemodified = time();$success = $grade->update('compute');// If successful trigger a user_graded event.if ($success) {\core\event\user_graded::create_from_grade($grade)->trigger();}}} else {// Update in db if changed.if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) {$grade->timemodified = time();$success = $grade->update('compute');// If successful trigger a user_graded event.if ($success) {\core\event\user_graded::create_from_grade($grade)->trigger();}}}if ($result !== false) {//lock grade if needed}if ($result === false) {return false;} else {return true;}}/*** Validate the formula.** @param string $formulastr* @return bool true if calculation possible, false otherwise*/public function validate_formula($formulastr) {global $CFG, $DB;require_once($CFG->libdir.'/mathslib.php');$formulastr = grade_item::normalize_formula($formulastr, $this->courseid);if (empty($formulastr)) {return true;}if (strpos($formulastr, '=') !== 0) {return get_string('errorcalculationnoequal', 'grades');}// get used itemsif (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {$useditems = array_unique($matches[1]); // remove duplicates} else {$useditems = array();}// MDL-11902// unset the value if formula is trying to reference to itself// but array keys does not match itemidif (!empty($this->id)) {$useditems = array_diff($useditems, array($this->id));//unset($useditems[$this->id]);}// prepare formula and init maths library$formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);$formula = new calc_formula($formula);if (empty($useditems)) {$grade_items = array();} else {list($usql, $params) = $DB->get_in_or_equal($useditems);$params[] = $this->courseid;$sql = "SELECT gi.*FROM {grade_items} giWHERE gi.id $usql and gi.courseid=?"; // from the same course only!if (!$grade_items = $DB->get_records_sql($sql, $params)) {$grade_items = array();}}$params = array();foreach ($useditems as $itemid) {// make sure all grade items exist in this courseif (!array_key_exists($itemid, $grade_items)) {return false;}// use max grade when testing formula, this should be ok in 99.9%// division by 0 is one of possible problems$params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;}// do the calculation$formula->set_params($params);$result = $formula->evaluate();// false as result indicates some problemif ($result === false) {// TODO: add more error hintsreturn get_string('errorcalculationunknown', 'grades');} else {return true;}}/*** Returns the value of the display type** It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.** @return int Display type*/public function get_displaytype() {global $CFG;if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);} else {return $this->display;}}/*** Returns the value of the decimals field** It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.** @return int Decimals (0 - 5)*/public function get_decimals() {global $CFG;if (is_null($this->decimals)) {return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);} else {return $this->decimals;}}/*** Returns a string representing the range of grademin - grademax for this grade item.** @param int $rangesdisplaytype* @param int $rangesdecimalpoints* @return string*/function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {global $USER;// Determine which display type to use for this averageif (isset($USER->editing) && $USER->editing) {$displaytype = GRADE_DISPLAY_TYPE_REAL;} else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs$displaytype = $this->get_displaytype();} else {$displaytype = $rangesdisplaytype;}// Override grade_item setting if a display preference (not default) was set for the averagesif ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {$decimalpoints = $this->get_decimals();} else {$decimalpoints = $rangesdecimalpoints;}if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {$grademin = "0 %";$grademax = "100 %";} else {$grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);$grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);}return $grademin.'–'. $grademax;}/*** Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.** @return string|false Returns the coefficient string of false is no coefficient is being used*/public function get_coefstring() {$parent_category = $this->load_parent_category();if ($this->is_category_item()) {$parent_category = $parent_category->load_parent_category();}if ($parent_category->is_aggregationcoef_used()) {return $parent_category->get_coefstring();} else {return false;}}/*** Returns whether the grade item can control the visibility of the grades** @return bool*/public function can_control_visibility() {if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);}return parent::can_control_visibility();}/*** Used to notify the completion system (if necessary) that a user's grade* has changed, and clear up a possible score cache.** @param bool $deleted True if grade was actually deleted*/protected function notify_changed($deleted) {global $CFG;// Condition code may cache the grades for conditional availability of// modules or sections. (This code should use a hook for communication// with plugin, but hooks are not implemented at time of writing.)if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {\availability_grade\callbacks::grade_item_changed($this->courseid);}}/*** Helper function to get the accurate context for this grade column.** @return context*/public function get_context() {if ($this->itemtype == 'mod') {$modinfo = get_fast_modinfo($this->courseid);// Sometimes the course module cache is out of date and needs to be rebuilt.if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {rebuild_course_cache($this->courseid, true);$modinfo = get_fast_modinfo($this->courseid);}// Even with a rebuilt cache the module does not exist. This means the// database is in an invalid state - we will log an error and return// the course context but the calling code should be updated.if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {mtrace(get_string('moduleinstancedoesnotexist', 'error'));$context = \context_course::instance($this->courseid);} else {$cm = $modinfo->instances[$this->itemmodule][$this->iteminstance];$context = \context_module::instance($cm->id);}} else {$context = \context_course::instance($this->courseid);}return $context;}}