-<?php // $Id$\r
-\r
-///////////////////////////////////////////////////////////////////////////\r
-// //\r
-// NOTICE OF COPYRIGHT //\r
-// //\r
-// Moodle - Modular Object-Oriented Dynamic Learning Environment //\r
-// http://moodle.com //\r
-// //\r
-// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //\r
-// //\r
-// This program is free software; you can redistribute it and/or modify //\r
-// it under the terms of the GNU General Public License as published by //\r
-// the Free Software Foundation; either version 2 of the License, or //\r
-// (at your option) any later version. //\r
-// //\r
-// This program is distributed in the hope that it will be useful, //\r
-// but WITHOUT ANY WARRANTY; without even the implied warranty of //\r
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //\r
-// GNU General Public License for more details: //\r
-// //\r
-// http://www.gnu.org/copyleft/gpl.html //\r
-// //\r
-///////////////////////////////////////////////////////////////////////////\r
-\r
-require_once('grade_object.php');\r
-\r
-/**\r
- * Class representing a grade item. It is responsible for handling its DB representation,\r
- * modifying and returning its metadata.\r
- */\r
-class grade_item extends grade_object {\r
- /**\r
- * DB Table (used by grade_object).\r
- * @var string $table\r
- */\r
- var $table = 'grade_items';\r
-\r
- /**\r
- * Array of required table fields, must start with 'id'.\r
- * @var array $required_fields\r
- */\r
- var $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',\r
- 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',\r
- 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',\r
- 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 'needsupdate', 'timecreated',\r
- 'timemodified');\r
-\r
- /**\r
- * The course this grade_item belongs to.\r
- * @var int $courseid\r
- */\r
- var $courseid;\r
-\r
- /**\r
- * The category this grade_item belongs to (optional).\r
- * @var int $categoryid\r
- */\r
- var $categoryid;\r
-\r
- /**\r
- * The grade_category object referenced $this->iteminstance (itemtype must be == 'category' or == 'course' in that case).\r
- * @var object $item_category\r
- */\r
- var $item_category;\r
-\r
- /**\r
- * The grade_category object referenced by $this->categoryid.\r
- * @var object $parent_category\r
- */\r
- var $parent_category;\r
-\r
-\r
- /**\r
- * The name of this grade_item (pushed by the module).\r
- * @var string $itemname\r
- */\r
- var $itemname;\r
-\r
- /**\r
- * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...\r
- * @var string $itemtype\r
- */\r
- var $itemtype;\r
-\r
- /**\r
- * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).\r
- * @var string $itemmodule\r
- */\r
- var $itemmodule;\r
-\r
- /**\r
- * ID of the item module\r
- * @var int $iteminstance\r
- */\r
- var $iteminstance;\r
-\r
- /**\r
- * Number of the item in a series of multiple grades pushed by an activity.\r
- * @var int $itemnumber\r
- */\r
- var $itemnumber;\r
-\r
- /**\r
- * Info and notes about this item.\r
- * @var string $iteminfo\r
- */\r
- var $iteminfo;\r
-\r
- /**\r
- * Arbitrary idnumber provided by the module responsible.\r
- * @var string $idnumber\r
- */\r
- var $idnumber;\r
-\r
- /**\r
- * Calculation string used for this item.\r
- * @var string $calculation\r
- */\r
- var $calculation;\r
-\r
- /**\r
- * Indicates if we already tried to normalize the grade calculation formula.\r
- * This flag helps to minimize db access when broken formulas used in calculation.\r
- * @var boolean\r
- */\r
- var $calculation_normalized;\r
- /**\r
- * Math evaluation object\r
- */\r
- var $formula;\r
-\r
- /**\r
- * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)\r
- * @var int $gradetype\r
- */\r
- var $gradetype = GRADE_TYPE_VALUE;\r
-\r
- /**\r
- * Maximum allowable grade.\r
- * @var float $grademax\r
- */\r
- var $grademax = 100;\r
-\r
- /**\r
- * Minimum allowable grade.\r
- * @var float $grademin\r
- */\r
- var $grademin = 0;\r
-\r
- /**\r
- * id of the scale, if this grade is based on a scale.\r
- * @var int $scaleid\r
- */\r
- var $scaleid;\r
-\r
- /**\r
- * A grade_scale object (referenced by $this->scaleid).\r
- * @var object $scale\r
- */\r
- var $scale;\r
-\r
- /**\r
- * The id of the optional grade_outcome associated with this grade_item.\r
- * @var int $outcomeid\r
- */\r
- var $outcomeid;\r
-\r
- /**\r
- * The grade_outcome this grade is associated with, if applicable.\r
- * @var object $outcome\r
- */\r
- var $outcome;\r
-\r
- /**\r
- * grade required to pass. (grademin <= gradepass <= grademax)\r
- * @var float $gradepass\r
- */\r
- var $gradepass = 0;\r
-\r
- /**\r
- * Multiply all grades by this number.\r
- * @var float $multfactor\r
- */\r
- var $multfactor = 1.0;\r
-\r
- /**\r
- * Add this to all grades.\r
- * @var float $plusfactor\r
- */\r
- var $plusfactor = 0;\r
-\r
- /**\r
- * Aggregation coeficient used for weighted averages\r
- * @var float $aggregationcoef\r
- */\r
- var $aggregationcoef = 0;\r
-\r
- /**\r
- * Sorting order of the columns.\r
- * @var int $sortorder\r
- */\r
- var $sortorder = 0;\r
-\r
- /**\r
- * Display type of the grades (Real, Percentage, Letter, or default).\r
- * @var int $display\r
- */\r
- var $display = GRADE_DISPLAY_TYPE_DEFAULT;\r
-\r
- /**\r
- * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.\r
- * @var int $decimals\r
- */\r
- var $decimals = null;\r
-\r
- /**\r
- * 0 if visible, 1 always hidden or date not visible until\r
- * @var int $hidden\r
- */\r
- var $hidden = 0;\r
-\r
- /**\r
- * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.\r
- * @var int $locked\r
- */\r
- var $locked = 0;\r
-\r
- /**\r
- * Date after which the grade will be locked. Empty means no automatic locking.\r
- * @var int $locktime\r
- */\r
- var $locktime = 0;\r
-\r
- /**\r
- * If set, the whole column will be recalculated, then this flag will be switched off.\r
- * @var boolean $needsupdate\r
- */\r
- var $needsupdate = 1;\r
-\r
- /**\r
- * Cached dependson array\r
- */\r
- var $dependson_cache = null;\r
-\r
- /**\r
- * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.\r
- * Force regrading if necessary\r
- * @param string $source from where was the object inserted (mod/forum, manual, etc.)\r
- * @return boolean success\r
- */\r
- function update($source=null) {\r
- // reset caches\r
- $this->dependson_cache = null;\r
-\r
- // Retrieve scale and infer grademax/min from it if needed\r
- $this->load_scale();\r
-\r
- // make sure there is not 0 in outcomeid\r
- if (empty($this->outcomeid)) {\r
- $this->outcomeid = null;\r
- }\r
-\r
- if ($this->qualifies_for_regrading()) {\r
- $this->force_regrading();\r
- }\r
-\r
- return parent::update($source);\r
- }\r
-\r
- /**\r
- * Compares the values held by this object with those of the matching record in DB, and returns\r
- * whether or not these differences are sufficient to justify an update of all parent objects.\r
- * This assumes that this object has an id number and a matching record in DB. If not, it will return false.\r
- * @return boolean\r
- */\r
- function qualifies_for_regrading() {\r
- if (empty($this->id)) {\r
- return false;\r
- }\r
-\r
- $db_item = new grade_item(array('id' => $this->id));\r
-\r
- $calculationdiff = $db_item->calculation != $this->calculation;\r
- $categorydiff = $db_item->categoryid != $this->categoryid;\r
- $gradetypediff = $db_item->gradetype != $this->gradetype;\r
- $grademaxdiff = $db_item->grademax != $this->grademax;\r
- $grademindiff = $db_item->grademin != $this->grademin;\r
- $scaleiddiff = $db_item->scaleid != $this->scaleid;\r
- $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;\r
- $multfactordiff = $db_item->multfactor != $this->multfactor;\r
- $plusfactordiff = $db_item->plusfactor != $this->plusfactor;\r
- $locktimediff = $db_item->locktime != $this->locktime;\r
- $acoefdiff = $db_item->aggregationcoef != $this->aggregationcoef;\r
-\r
- $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time\r
- $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking\r
-\r
- return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff\r
- || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff\r
- || $lockeddiff || $acoefdiff || $locktimediff);\r
- }\r
-\r
- /**\r
- * Finds and returns a grade_item instance based on params.\r
- * @static\r
- *\r
- * @param array $params associative arrays varname=>value\r
- * @return object grade_item instance or false if none found.\r
- */\r
- function fetch($params) {\r
- return grade_object::fetch_helper('grade_items', 'grade_item', $params);\r
- }\r
-\r
- /**\r
- * Finds and returns all grade_item instances based on params.\r
- * @static\r
- *\r
- * @param array $params associative arrays varname=>value\r
- * @return array array of grade_item insatnces or false if none found.\r
- */\r
- function fetch_all($params) {\r
- return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);\r
- }\r
-\r
- /**\r
- * Delete all grades and force_regrading of parent category.\r
- * @param string $source from where was the object deleted (mod/forum, manual, etc.)\r
- * @return boolean success\r
- */\r
- function delete($source=null) {\r
- if (!$this->is_course_item()) {\r
- $this->force_regrading();\r
- }\r
-\r
- if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {\r
- foreach ($grades as $grade) {\r
- $grade->delete($source);\r
- }\r
- }\r
-\r
- return parent::delete($source);\r
- }\r
-\r
- /**\r
- * In addition to perform parent::insert(), calls force_regrading() method too.\r
- * @param string $source from where was the object inserted (mod/forum, manual, etc.)\r
- * @return int PK ID if successful, false otherwise\r
- */\r
- function insert($source=null) {\r
- global $CFG;\r
-\r
- if (empty($this->courseid)) {\r
- error('Can not insert grade item without course id!');\r
- }\r
-\r
- // load scale if needed\r
- $this->load_scale();\r
-\r
- // add parent category if needed\r
- if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {\r
- $course_category = grade_category::fetch_course_category($this->courseid);\r
- $this->categoryid = $course_category->id;\r
-\r
- }\r
-\r
- // always place the new items at the end, move them after insert if needed\r
- $last_sortorder = get_field_select('grade_items', 'MAX(sortorder)', "courseid = {$this->courseid}");\r
- if (!empty($last_sortorder)) {\r
- $this->sortorder = $last_sortorder + 1;\r
- } else {\r
- $this->sortorder = 1;\r
- }\r
-\r
- // add proper item numbers to manual items\r
- if ($this->itemtype == 'manual') {\r
- if (empty($this->itemnumber)) {\r
- $this->itemnumber = 0;\r
- }\r
- }\r
-\r
- // make sure there is not 0 in outcomeid\r
- if (empty($this->outcomeid)) {\r
- $this->outcomeid = null;\r
- }\r
-\r
- if (parent::insert($source)) {\r
- // force regrading of items if needed\r
- $this->force_regrading();\r
- return $this->id;\r
-\r
- } else {\r
- debugging("Could not insert this grade_item in the database!");\r
- return false;\r
- }\r
- }\r
-\r
- /**\r
- * Set idnumber of grade item, updates also course_modules table\r
- * @param string $idnumber (without magic quotes)\r
- * @return boolean success\r
- */\r
- function add_idnumber($idnumber) {\r
- if (!empty($this->idnumber)) {\r
- return false;\r
- }\r
-\r
- if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {\r
- if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {\r
- return false;\r
- }\r
- if (!empty($cm->idnumber)) {\r
- return false;\r
- }\r
- if (set_field('course_modules', 'idnumber', addslashes($idnumber), 'id', $cm->id)) {\r
- $this->idnumber = $idnumber;\r
- return $this->update();\r
- }\r
- return false;\r
-\r
- } else {\r
- $this->idnumber = $idnumber;\r
- return $this->update();\r
- }\r
- }\r
-\r
- /**\r
- * Returns the locked state of this grade_item (if the grade_item is locked OR no specific\r
- * $userid is given) or the locked state of a specific grade within this item if a specific\r
- * $userid is given and the grade_item is unlocked.\r
- *\r
- * @param int $userid\r
- * @return boolean Locked state\r
- */\r
- function is_locked($userid=NULL) {\r
- if (!empty($this->locked)) {\r
- return true;\r
- }\r
-\r
- if (!empty($userid)) {\r
- if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {\r
- $grade->grade_item =& $this; // prevent db fetching of cached grade_item\r
- return $grade->is_locked();\r
- }\r
- }\r
-\r
- return false;\r
- }\r
-\r
- /**\r
- * Locks or unlocks this grade_item and (optionally) all its associated final grades.\r
- * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.\r
- * @param boolean $cascade lock/unlock child objects too\r
- * @param boolean $refresh refresh grades when unlocking\r
- * @return boolean true if grade_item all grades updated, false if at least one update fails\r
- */\r
- function set_locked($lockedstate, $cascade=false, $refresh=true) {\r
- if ($lockedstate) {\r
- /// setting lock\r
- if ($this->needsupdate) {\r
- return false; // can not lock grade without first having final grade\r
- }\r
-\r
- $this->locked = time();\r
- $this->update();\r
-\r
- if ($cascade) {\r
- $grades = $this->get_final();\r
- foreach($grades as $g) {\r
- $grade = new grade_grade($g, false);\r
- $grade->grade_item =& $this;\r
- $grade->set_locked(1, null, false);\r
- }\r
- }\r
-\r
- return true;\r
-\r
- } else {\r
- /// removing lock\r
- if (!empty($this->locked) and $this->locktime < time()) {\r
- //we have to reset locktime or else it would lock up again\r
- $this->locktime = 0;\r
- }\r
-\r
- $this->locked = 0;\r
- $this->update();\r
-\r
- if ($cascade) {\r
- if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {\r
- foreach($grades as $grade) {\r
- $grade->grade_item =& $this;\r
- $grade->set_locked(0, null, false);\r
- }\r
- }\r
- }\r
-\r
- if ($refresh) {\r
- //refresh when unlocking\r
- $this->refresh_grades();\r
- }\r
-\r
- return true;\r
- }\r
- }\r
-\r
- /**\r
- * Lock the grade if needed - make sure this is called only when final grades are valid\r
- */\r
- function check_locktime() {\r
- if (!empty($this->locked)) {\r
- return; // already locked\r
- }\r
-\r
- if ($this->locktime and $this->locktime < time()) {\r
- $this->locked = time();\r
- $this->update('locktime');\r
- }\r
- }\r
-\r
- /**\r
- * Set the locktime for this grade item.\r
- *\r
- * @param int $locktime timestamp for lock to activate\r
- * @return void\r
- */\r
- function set_locktime($locktime) {\r
- $this->locktime = $locktime;\r
- $this->update();\r
- }\r
-\r
- /**\r
- * Set the locktime for this grade item.\r
- *\r
- * @return int $locktime timestamp for lock to activate\r
- */\r
- function get_locktime() {\r
- return $this->locktime;\r
- }\r
-\r
- /**\r
- * Returns the hidden state of this grade_item\r
- * @return boolean hidden state\r
- */\r
- function is_hidden() {\r
- return ($this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()));\r
- }\r
-\r
- /**\r
- * Check grade item hidden status.\r
- * @return int 0 means visible, 1 hidden always, timestamp hidden until\r
- */\r
- function get_hidden() {\r
- return $this->hidden;\r
- }\r
-\r
- /**\r
- * Set the hidden status of grade_item and all grades, 0 mean visible, 1 always hidden, number means date to hide until.\r
- * @param int $hidden new hidden status\r
- * @param boolean $cascade apply to child objects too\r
- * @return void\r
- */\r
- function set_hidden($hidden, $cascade=false) {\r
- $this->hidden = $hidden;\r
- $this->update();\r
-\r
- if ($cascade) {\r
- if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {\r
- foreach($grades as $grade) {\r
- $grade->grade_item =& $this;\r
- $grade->set_hidden($hidden, $cascade);\r
- }\r
- }\r
- }\r
- }\r
-\r
- /**\r
- * Returns the number of grades that are hidden.\r
- * @param return int Number of hidden grades\r
- */\r
- function has_hidden_grades($groupsql="", $groupwheresql="") {\r
- global $CFG;\r
- return get_field_sql("SELECT COUNT(*) FROM {$CFG->prefix}grade_grades g LEFT JOIN "\r
- ."{$CFG->prefix}user u ON g.userid = u.id $groupsql WHERE itemid = $this->id AND hidden = 1 $groupwheresql");\r
- }\r
-\r
- /**\r
- * Mark regrading as finished successfully.\r
- */\r
- function regrading_finished() {\r
- $this->needsupdate = 0;\r
- //do not use $this->update() because we do not want this logged in grade_item_history\r
- set_field('grade_items', 'needsupdate', 0, 'id', $this->id);\r
- }\r
-\r
- /**\r
- * Performs the necessary calculations on the grades_final referenced by this grade_item.\r
- * Also resets the needsupdate flag once successfully performed.\r
- *\r
- * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),\r
- * because the regrading must be done in correct order!!\r
- *\r
- * @return boolean true if ok, error string otherwise\r
- */\r
- function regrade_final_grades($userid=null) {\r
- global $CFG;\r
-\r
- // locked grade items already have correct final grades\r
- if ($this->is_locked()) {\r
- return true;\r
- }\r
-\r
- // calculation produces final value using formula from other final values\r
- if ($this->is_calculated()) {\r
- if ($this->compute($userid)) {\r
- return true;\r
- } else {\r
- return "Could not calculate grades for grade item"; // TODO: improve and localize\r
- }\r
-\r
- // noncalculated outcomes already have final values - raw grades not used\r
- } else if ($this->is_outcome_item()) {\r
- return true;\r
-\r
- // aggregate the category grade\r
- } else if ($this->is_category_item() or $this->is_course_item()) {\r
- // aggregate category grade item\r
- $category = $this->get_item_category();\r
- $category->grade_item =& $this;\r
- if ($category->generate_grades($userid)) {\r
- return true;\r
- } else {\r
- return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize\r
- }\r
-\r
- } else if ($this->is_manual_item()) {\r
- // manual items track only final grades, no raw grades\r
- return true;\r
-\r
- } else if (!$this->is_raw_used()) {\r
- // hmm - raw grades are not used- nothing to regrade\r
- return true;\r
- }\r
-\r
- // normal grade item - just new final grades\r
- $result = true;\r
- $grade_inst = new grade_grade();\r
- $fields = implode(',', $grade_inst->required_fields);\r
- if ($userid) {\r
- $rs = get_recordset_select('grade_grades', "itemid={$this->id} AND userid=$userid", '', $fields);\r
- } else {\r
- $rs = get_recordset('grade_grades', 'itemid', $this->id, '', $fields);\r
- }\r
- if ($rs) {\r
- while ($grade_record = rs_fetch_next_record($rs)) {\r
- $grade = new grade_grade($grade_record, false);\r
-\r
- if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {\r
- // this grade is locked - final grade must be ok\r
- continue;\r
- }\r
-\r
- $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);\r
-\r
- if ($grade_record->finalgrade !== $grade->finalgrade) {\r
- if (!$grade->update('system')) {\r
- $result = "Internal error updating final grade";\r
- }\r
- }\r
- }\r
- rs_close($rs);\r
- }\r
-\r
- return $result;\r
- }\r
-\r
- /**\r
- * Given a float grade value or integer grade scale, applies a number of adjustment based on\r
- * grade_item variables and returns the result.\r
- * @param object $rawgrade The raw grade value.\r
- * @return mixed\r
- */\r
- function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {\r
- if (is_null($rawgrade)) {\r
- return null;\r
- }\r
-\r
- if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade\r
-\r
- if ($this->grademax < $this->grademin) {\r
- return null;\r
- }\r
-\r
- if ($this->grademax == $this->grademin) {\r
- return $this->grademax; // no range\r
- }\r
-\r
- // Standardise score to the new grade range\r
- // NOTE: this is not compatible with current assignment grading\r
- if ($rawmin != $this->grademin or $rawmax != $this->grademax) {\r
- $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);\r
- }\r
-\r
- // Apply other grade_item factors\r
- $rawgrade *= $this->multfactor;\r
- $rawgrade += $this->plusfactor;\r
-\r
- return bounded_number($this->grademin, $rawgrade, $this->grademax);\r
-\r
- } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value\r
- if (empty($this->scale)) {\r
- $this->load_scale();\r
- }\r
-\r
- if ($this->grademax < 0) {\r
- return null; // scale not present - no grade\r
- }\r
-\r
- if ($this->grademax == 0) {\r
- return $this->grademax; // only one option\r
- }\r
-\r
- // Convert scale if needed\r
- // NOTE: this is not compatible with current assignment grading\r
- if ($rawmin != $this->grademin or $rawmax != $this->grademax) {\r
- $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);\r
- }\r
-\r
- return (int)bounded_number(0, round($rawgrade+0.00001), $this->grademax);\r
-\r
-\r
- } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value\r
- // somebody changed the grading type when grades already existed\r
- return null;\r
-\r
- } else {\r
- dubugging("Unkown grade type");\r
- return null;;\r
- }\r
- }\r
-\r
- /**\r
- * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.\r
- * @return void\r
- */\r
- function force_regrading() {\r
- $this->needsupdate = 1;\r
- //mark this item and course item only - categories and calculated items are always regraded\r
- $wheresql = "(itemtype='course' OR id={$this->id}) AND courseid={$this->courseid}";\r
- set_field_select('grade_items', 'needsupdate', 1, $wheresql);\r
- }\r
-\r
- /**\r
- * Instantiates a grade_scale object whose data is retrieved from the DB,\r
- * if this item's scaleid variable is set.\r
- * @return object grade_scale or null if no scale used\r
- */\r
- function load_scale() {\r
- if ($this->gradetype != GRADE_TYPE_SCALE) {\r
- $this->scaleid = null;\r
- }\r
-\r
- if (!empty($this->scaleid)) {\r
- //do not load scale if already present\r
- if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {\r
- $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));\r
- $this->scale->load_items();\r
- }\r
-\r
- // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we\r
- // stay with the current min=1 max=count(scaleitems)\r
- $this->grademax = count($this->scale->scale_items);\r
- $this->grademin = 1;\r
-\r
- } else {\r
- $this->scale = null;\r
- }\r
-\r
- return $this->scale;\r
- }\r
-\r
- /**\r
- * Instantiates a grade_outcome object whose data is retrieved from the DB,\r
- * if this item's outcomeid variable is set.\r
- * @return object grade_outcome\r
- */\r
- function load_outcome() {\r
- if (!empty($this->outcomeid)) {\r
- $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));\r
- }\r
- return $this->outcome;\r
- }\r
-\r
- /**\r
- * Returns the grade_category object this grade_item belongs to (referenced by categoryid)\r
- * or category attached to category item.\r
- *\r
- * @return mixed grade_category object if applicable, false if course item\r
- */\r
- function get_parent_category() {\r
- if ($this->is_category_item() or $this->is_course_item()) {\r
- return $this->get_item_category();\r
-\r
- } else {\r
- return grade_category::fetch(array('id'=>$this->categoryid));\r
- }\r
- }\r
-\r
- /**\r
- * Calls upon the get_parent_category method to retrieve the grade_category object\r
- * from the DB and assigns it to $this->parent_category. It also returns the object.\r
- * @return object Grade_category\r
- */\r
- function load_parent_category() {\r
- if (empty($this->parent_category->id)) {\r
- $this->parent_category = $this->get_parent_category();\r
- }\r
- return $this->parent_category;\r
- }\r
-\r
- /**\r
- * Returns the grade_category for category item\r
- *\r
- * @return mixed grade_category object if applicable, false otherwise\r
- */\r
- function get_item_category() {\r
- if (!$this->is_course_item() and !$this->is_category_item()) {\r
- return false;\r
- }\r
- return grade_category::fetch(array('id'=>$this->iteminstance));\r
- }\r
-\r
- /**\r
- * Calls upon the get_item_category method to retrieve the grade_category object\r
- * from the DB and assigns it to $this->item_category. It also returns the object.\r
- * @return object Grade_category\r
- */\r
- function load_item_category() {\r
- if (empty($this->category->id)) {\r
- $this->item_category = $this->get_item_category();\r
- }\r
- return $this->item_category;\r
- }\r
-\r
- /**\r
- * Is the grade item associated with category?\r
- * @return boolean\r
- */\r
- function is_category_item() {\r
- return ($this->itemtype == 'category');\r
- }\r
-\r
- /**\r
- * Is the grade item associated with course?\r
- * @return boolean\r
- */\r
- function is_course_item() {\r
- return ($this->itemtype == 'course');\r
- }\r
-\r
- /**\r
- * Is this a manualy graded item?\r
- * @return boolean\r
- */\r
- function is_manual_item() {\r
- return ($this->itemtype == 'manual');\r
- }\r
-\r
- /**\r
- * Is this an outcome item?\r
- * @return boolean\r
- */\r
- function is_outcome_item() {\r
- return !empty($this->outcomeid);\r
- }\r
-\r
- /**\r
- * Is the grade item normal - associated with module, plugin or something else?\r
- * @return boolean\r
- */\r
- function is_normal_item() {\r
- return ($this->itemtype != 'course' and $this->itemtype != 'category' and $this->itemtype != 'manual');\r
- }\r
-\r
- /**\r
- * Returns true if grade items uses raw grades\r
- * @return boolean\r
- */\r
- function is_raw_used() {\r
- return ($this->is_normal_item() and !$this->is_calculated() and !$this->is_outcome_item());\r
- }\r
-\r
- /**\r
- * Returns grade item associated with the course\r
- * @param int $courseid\r
- * @return course item object\r
- */\r
- function fetch_course_item($courseid) {\r
- if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {\r
- return $course_item;\r
- }\r
-\r
- // first get category - it creates the associated grade item\r
- $course_category = grade_category::fetch_course_category($courseid);\r
-\r
- return grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'));\r
- }\r
-\r
- /**\r
- * Is grading object editable?\r
- * @return boolean\r
- */\r
- function is_editable() {\r
- return true;\r
- }\r
-\r
- /**\r
- * Checks if grade calculated. Returns this object's calculation.\r
- * @return boolean true if grade item calculated.\r
- */\r
- function is_calculated() {\r
- if (empty($this->calculation)) {\r
- return false;\r
- }\r
-\r
- /*\r
- * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),\r
- * we would have to fetch all course grade items to find out the ids.\r
- * Also if user changes the idnumber the formula does not need to be updated.\r
- */\r
-\r
- // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)\r
- if (!$this->calculation_normalized and preg_match('/##gi\d+##/', $this->calculation)) {\r
- $this->set_calculation($this->calculation);\r
- }\r
-\r
- return !empty($this->calculation);\r
- }\r
-\r
- /**\r
- * Returns calculation string if grade calculated.\r
- * @return mixed string if calculation used, null if not\r
- */\r
- function get_calculation() {\r
- if ($this->is_calculated()) {\r
- return grade_item::denormalize_formula($this->calculation, $this->courseid);\r
-\r
- } else {\r
- return NULL;\r
- }\r
- }\r
-\r
- /**\r
- * Sets this item's calculation (creates it) if not yet set, or\r
- * updates it if already set (in the DB). If no calculation is given,\r
- * the calculation is removed.\r
- * @param string $formula string representation of formula used for calculation\r
- * @return boolean success\r
- */\r
- function set_calculation($formula) {\r
- $this->calculation = grade_item::normalize_formula($formula, $this->courseid);\r
- $this->calculation_normalized = true;\r
- return $this->update();\r
- }\r
-\r
- /**\r
- * Denormalizes the calculation formula to [idnumber] form\r
- * @static\r
- * @param string $formula\r
- * @return string denormalized string\r
- */\r
- function denormalize_formula($formula, $courseid) {\r
- if (empty($formula)) {\r
- return '';\r
- }\r
-\r
- // denormalize formula - convert ##giXX## to [[idnumber]]\r
- if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {\r
- foreach ($matches[1] as $id) {\r
- if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {\r
- if (!empty($grade_item->idnumber)) {\r
- $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);\r
- }\r
- }\r
- }\r
- }\r
-\r
- return $formula;\r
-\r
- }\r
-\r
- /**\r
- * Normalizes the calculation formula to [#giXX#] form\r
- * @static\r
- * @param string $formula\r
- * @return string normalized string\r
- */\r
- function normalize_formula($formula, $courseid) {\r
- $formula = trim($formula);\r
-\r
- if (empty($formula)) {\r
- return NULL;\r
-\r
- }\r
-\r
- // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]\r
- if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {\r
- foreach ($grade_items as $grade_item) {\r
- $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);\r
- }\r
- }\r
-\r
- return $formula;\r
- }\r
-\r
- /**\r
- * Returns the final values for this grade item (as imported by module or other source).\r
- * @param int $userid Optional: to retrieve a single final grade\r
- * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.\r
- */\r
- function get_final($userid=NULL) {\r
- if ($userid) {\r
- if ($user = get_record('grade_grades', 'itemid', $this->id, 'userid', $userid)) {\r
- return $user;\r
- }\r
-\r
- } else {\r
- if ($grades = get_records('grade_grades', 'itemid', $this->id)) {\r
- //TODO: speed up with better SQL\r
- $result = array();\r
- foreach ($grades as $grade) {\r
- $result[$grade->userid] = $grade;\r
- }\r
- return $result;\r
- } else {\r
- return array();\r
- }\r
- }\r
- }\r
-\r
- /**\r
- * Get (or create if not exist yet) grade for this user\r
- * @param int $userid\r
- * @return object grade_grade object instance\r
- */\r
- function get_grade($userid, $create=true) {\r
- if (empty($this->id)) {\r
- debugging('Can not use before insert');\r
- return false;\r
- }\r
-\r
- $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));\r
- if (empty($grade->id) and $create) {\r
- $grade->insert();\r
- }\r
-\r
- return $grade;\r
- }\r
-\r
- /**\r
- * Returns the sortorder of this grade_item. This method is also available in\r
- * grade_category, for cases where the object type is not know.\r
- * @return int Sort order\r
- */\r
- function get_sortorder() {\r
- return $this->sortorder;\r
- }\r
-\r
- /**\r
- * Returns the idnumber of this grade_item. This method is also available in\r
- * grade_category, for cases where the object type is not know.\r
- * @return string idnumber\r
- */\r
- function get_idnumber() {\r
- return $this->idnumber;\r
- }\r
-\r
- /**\r
- * Returns this grade_item. This method is also available in\r
- * grade_category, for cases where the object type is not know.\r
- * @return string idnumber\r
- */\r
- function get_grade_item() {\r
- return $this;\r
- }\r
-\r
- /**\r
- * Sets the sortorder of this grade_item. This method is also available in\r
- * grade_category, for cases where the object type is not know.\r
- * @param int $sortorder\r
- * @return void\r
- */\r
- function set_sortorder($sortorder) {\r
- $this->sortorder = $sortorder;\r
- $this->update();\r
- }\r
-\r
- function move_after_sortorder($sortorder) {\r
- global $CFG;\r
-\r
- //make some room first\r
- $sql = "UPDATE {$CFG->prefix}grade_items\r
- SET sortorder = sortorder + 1\r
- WHERE sortorder > $sortorder AND courseid = {$this->courseid}";\r
- execute_sql($sql, false);\r
-\r
- $this->set_sortorder($sortorder + 1);\r
- }\r
-\r
- /**\r
- * Returns the most descriptive field for this object. This is a standard method used\r
- * when we do not know the exact type of an object.\r
- * @return string name\r
- */\r
- function get_name() {\r
- if (!empty($this->itemname)) {\r
- // MDL-10557\r
- return format_string($this->itemname);\r
-\r
- } else if ($this->is_course_item()) {\r
- return get_string('coursetotal', 'grades');\r
-\r
- } else if ($this->is_category_item()) {\r
- return get_string('categorytotal', 'grades');\r
-\r
- } else {\r
- return get_string('grade');\r
- }\r
- }\r
-\r
- /**\r
- * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.\r
- * @param int $parentid\r
- * @return boolean success;\r
- */\r
- function set_parent($parentid) {\r
- if ($this->is_course_item() or $this->is_category_item()) {\r
- error('Can not set parent for category or course item!');\r
- }\r
-\r
- if ($this->categoryid == $parentid) {\r
- return true;\r
- }\r
-\r
- // find parent and check course id\r
- if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {\r
- return false;\r
- }\r
-\r
- $this->force_regrading();\r
-\r
- // set new parent\r
- $this->categoryid = $parent_category->id;\r
- $this->parent_category =& $parent_category;\r
-\r
- return $this->update();\r
- }\r
-\r
- /**\r
- * Finds out on which other items does this depend directly when doing calculation or category agregation\r
- * @param bool $reset_cache\r
- * @return array of grade_item ids this one depends on\r
- */\r
- function depends_on($reset_cache=false) {\r
- global $CFG;\r
-\r
- if ($reset_cache) {\r
- $this->dependson_cache = null;\r
- } else if (isset($this->dependson_cache)) {\r
- return $this->dependson_cache;\r
- }\r
-\r
- if ($this->is_locked()) {\r
- // locked items do not need to be regraded\r
- $this->dependson_cache = array();\r
- return $this->dependson_cache;\r
- }\r
-\r
- if ($this->is_calculated()) {\r
- if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {\r
- $this->dependson_cache = array_unique($matches[1]); // remove duplicates\r
- return $this->dependson_cache;\r
- } else {\r
- $this->dependson_cache = array();\r
- return $this->dependson_cache;\r
- }\r
-\r
- } else if ($grade_category = $this->load_item_category()) {\r
- //only items with numeric or scale values can be aggregated\r
- if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {\r
- $this->dependson_cache = array();\r
- return $this->dependson_cache;\r
- }\r
-\r
- $grade_category->apply_forced_settings();\r
-\r
- if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {\r
- $outcomes_sql = "";\r
- } else {\r
- $outcomes_sql = "AND gi.outcomeid IS NULL";\r
- }\r
-\r
- if ($grade_category->aggregatesubcats) {\r
- // return all children excluding category items\r
- $sql = "SELECT gi.id\r
- FROM {$CFG->prefix}grade_items gi\r
- WHERE (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")\r
- $outcomes_sql\r
- AND gi.categoryid IN (\r
- SELECT gc.id\r
- FROM {$CFG->prefix}grade_categories gc\r
- WHERE gc.path LIKE '%/{$grade_category->id}/%')";\r
-\r
- } else {\r
- $sql = "SELECT gi.id\r
- FROM {$CFG->prefix}grade_items gi\r
- WHERE gi.categoryid = {$grade_category->id}\r
- AND (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")\r
- $outcomes_sql\r
-\r
- UNION\r
-\r
- SELECT gi.id\r
- FROM {$CFG->prefix}grade_items gi, {$CFG->prefix}grade_categories gc\r
- WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id\r
- AND gc.parent = {$grade_category->id}\r
- AND (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")\r
- $outcomes_sql";\r
- }\r
-\r
- if ($children = get_records_sql($sql)) {\r
- $this->dependson_cache = array_keys($children);\r
- return $this->dependson_cache;\r
- } else {\r
- $this->dependson_cache = array();\r
- return $this->dependson_cache;\r
- }\r
-\r
- } else {\r
- $this->dependson_cache = array();\r
- return $this->dependson_cache;\r
- }\r
- }\r
-\r
- /**\r
- * Refetch grades from moudles, plugins.\r
- * @param int $userid optional, one user only\r
- */\r
- function refresh_grades($userid=0) {\r
- if ($this->itemtype == 'mod') {\r
- if ($this->is_outcome_item()) {\r
- //nothing to do\r
- return;\r
- }\r
-\r
- if (!$activity = get_record($this->itemmodule, 'id', $this->iteminstance)) {\r
- debugging('Can not find activity');\r
- return;\r
- }\r
-\r
- if (! $cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {\r
- debuggin('Can not find course module');\r
- return;\r
- }\r
-\r
- $activity->modname = $this->itemmodule;\r
- $activity->cmidnumber = $cm->idnumber;\r
-\r
- grade_update_mod_grades($activity);\r
- }\r
- }\r
-\r
- /**\r
- * Updates final grade value for given user, this is a only way to update final\r
- * grades from gradebook and import because it logs the change in history table\r
- * and deals with overridden flag. This flag is set to prevent later overriding\r
- * from raw grades submitted from modules.\r
- *\r
- * @param int $userid the graded user\r
- * @param mixed $finalgrade float value of final grade - false means do not change\r
- * @param string $howmodified modification source\r
- * @param string $note optional note\r
- * @param mixed $feedback teachers feedback as string - false means do not change\r
- * @param int $feedbackformat\r
- * @return boolean success\r
- */\r
- function update_final_grade($userid, $finalgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {\r
- global $USER, $CFG;\r
-\r
- if (empty($usermodified)) {\r
- $usermodified = $USER->id;\r
- }\r
-\r
- $result = true;\r
-\r
- // no grading used or locked\r
- if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {\r
- return false;\r
- }\r
-\r
- $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));\r
- $grade->grade_item =& $this; // prevent db fetching of this grade_item\r
-\r
- $grade->usermodified = $usermodified;\r
-\r
- if ($grade->is_locked()) {\r
- // do not update locked grades at all\r
- return false;\r
- }\r
-\r
- $locktime = $grade->get_locktime();\r
- if ($locktime and $locktime < time()) {\r
- // do not update grades that should be already locked, force regrade instead\r
- $this->force_regrading();\r
- return false;\r
- }\r
-\r
- $oldgrade = new object();\r
- $oldgrade->finalgrade = $grade->finalgrade;\r
- $oldgrade->overridden = $grade->overridden;\r
- $oldgrade->feedback = $grade->feedback;\r
- $oldgrade->feedbackformat = $grade->feedbackformat;\r
-\r
- if ($finalgrade !== false or $feedback !== false) {\r
- if (($this->is_outcome_item() or $this->is_manual_item()) and !$this->is_calculated()) {\r
- // final grades updated only by user - no need for overriding\r
- $grade->overridden = 0;\r
-\r
- } else {\r
- $grade->overridden = time();\r
- }\r
- }\r
-\r
- if ($finalgrade !== false) {\r
- if (!is_null($finalgrade)) {\r
- $finalgrade = bounded_number($this->grademin, $finalgrade, $this->grademax);\r
- } else {\r
- $finalgrade = $finalgrade;\r
- }\r
- $grade->finalgrade = $finalgrade;\r
- }\r
-\r
- // do we have comment from teacher?\r
- if ($feedback !== false) {\r
- $grade->feedback = $feedback;\r
- $grade->feedbackformat = $feedbackformat;\r
- }\r
-\r
- if (empty($grade->id)) {\r
- $result = (boolean)$grade->insert($source);\r
-\r
- } else if ($grade->finalgrade !== $oldgrade->finalgrade\r
- or $grade->feedback !== $oldgrade->feedback\r
- or $grade->feedbackformat !== $oldgrade->feedbackformat) {\r
- $result = $grade->update($source);\r
- }\r
-\r
- if (!$result) {\r
- // something went wrong - better force final grade recalculation\r
- $this->force_regrading();\r
-\r
- } else if ($this->is_course_item() and !$this->needsupdate) {\r
- if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {\r
- $this->force_regrading();\r
- }\r
-\r
- } else if (!$this->needsupdate) {\r
- $course_item = grade_item::fetch_course_item($this->courseid);\r
- if (!$course_item->needsupdate) {\r
- if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {\r
- $this->force_regrading();\r
- }\r
- } else {\r
- $this->force_regrading();\r
- }\r
- }\r
-\r
- return $result;\r
- }\r
-\r
-\r
- /**\r
- * Updates raw grade value for given user, this is a only way to update raw\r
- * grades from external source (modules, etc.),\r
- * because it logs the change in history table and deals with final grade recalculation.\r
- *\r
- * @param int $userid the graded user\r
- * @param mixed $rawgrade float value of raw grade - false means do not change\r
- * @param string $howmodified modification source\r
- * @param string $note optional note\r
- * @param mixed $feedback teachers feedback as string - false means do not change\r
- * @param int $feedbackformat\r
- * @return boolean success\r
- */\r
- function update_raw_grade($userid, $rawgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {\r
- global $USER;\r
-\r
- if (empty($usermodified)) {\r
- $usermodified = $USER->id;\r
- }\r
-\r
- $result = true;\r
-\r
- // calculated grades can not be updated; course and category can not be updated because they are aggregated\r
- if ($this->is_calculated() or $this->is_outcome_item() or !$this->is_normal_item()\r
- or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {\r
- return false;\r
- }\r
-\r
- $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));\r
- $grade->grade_item =& $this; // prevent db fetching of this grade_item\r
-\r
- $grade->usermodified = $usermodified;\r
-\r
- if ($grade->is_locked()) {\r
- // do not update locked grades at all\r
- return false;\r
- }\r
-\r
- $locktime = $grade->get_locktime();\r
- if ($locktime and $locktime < time()) {\r
- // do not update grades that should be already locked and force regrade\r
- $this->force_regrading();\r
- return false;\r
- }\r
-\r
- $oldgrade = new object();\r
- $oldgrade->finalgrade = $grade->finalgrade;\r
- $oldgrade->rawgrade = $grade->rawgrade;\r
- $oldgrade->rawgrademin = $grade->rawgrademin;\r
- $oldgrade->rawgrademax = $grade->rawgrademax;\r
- $oldgrade->rawscaleid = $grade->rawscaleid;\r
- $oldgrade->feedback = $grade->feedback;\r
- $oldgrade->feedbackformat = $grade->feedbackformat;\r
-\r
- // fist copy current grademin/max and scale\r
- $grade->rawgrademin = $this->grademin;\r
- $grade->rawgrademax = $this->grademax;\r
- $grade->rawscaleid = $this->scaleid;\r
-\r
- // change raw grade?\r
- if ($rawgrade !== false) {\r
- $grade->rawgrade = $rawgrade;\r
- }\r
-\r
- // do we have comment from teacher?\r
- if ($feedback !== false) {\r
- $grade->feedback = $feedback;\r
- $grade->feedbackformat = $feedbackformat;\r
- }\r
-\r
- if (empty($grade->id)) {\r
- $result = (boolean)$grade->insert($source);\r
-\r
- } else if ($grade->finalgrade !== $oldgrade->finalgrade\r
- or $grade->rawgrade !== $oldgrade->rawgrade\r
- or $grade->rawgrademin !== $oldgrade->rawgrademin\r
- or $grade->rawgrademax !== $oldgrade->rawgrademax\r
- or $grade->rawscaleid !== $oldgrade->rawscaleid\r
- or $grade->feedback !== $oldgrade->feedback\r
- or $grade->feedbackformat !== $oldgrade->feedbackformat) {\r
-\r
- $result = $grade->update($source);\r
- }\r
-\r
- if (!$result) {\r
- // something went wrong - better force final grade recalculation\r
- $this->force_regrading();\r
-\r
- } else if (!$this->needsupdate) {\r
- $course_item = grade_item::fetch_course_item($this->courseid);\r
- if (!$course_item->needsupdate) {\r
- if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {\r
- $this->force_regrading();\r
- }\r
- } else {\r
- $this->force_regrading();\r
- }\r
- }\r
-\r
- return $result;\r
- }\r
-\r
- /**\r
- * Calculates final grade values using the formula in calculation property.\r
- * The parameters are taken from final grades of grade items in current course only.\r
- * @return boolean false if error\r
- */\r
- function compute($userid=null) {\r
- global $CFG;\r
-\r
- if (!$this->is_calculated()) {\r
- return false;\r
- }\r
-\r
- require_once($CFG->libdir.'/mathslib.php');\r
-\r
- if ($this->is_locked()) {\r
- return true; // no need to recalculate locked items\r
- }\r
-\r
- // get used items\r
- $useditems = $this->depends_on();\r
-\r
- // prepare formula and init maths library\r
- $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);\r
- $this->formula = new calc_formula($formula);\r
-\r
- // where to look for final grades?\r
- // this itemid is added so that we use only one query for source and final grades\r
- $gis = implode(',', array_merge($useditems, array($this->id)));\r
-\r
- if ($userid) {\r
- $usersql = "AND g.userid=$userid";\r
- } else {\r
- $usersql = "";\r
- }\r
-\r
- $grade_inst = new grade_grade();\r
- $fields = 'g.'.implode(',g.', $grade_inst->required_fields);\r
-\r
- $sql = "SELECT $fields\r
- FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi\r
- WHERE gi.id = g.itemid AND gi.courseid={$this->courseid} AND gi.id IN ($gis) $usersql\r
- ORDER BY g.userid";\r
-\r
- $return = true;\r
-\r
- // group the grades by userid and use formula on the group\r
- if ($rs = get_recordset_sql($sql)) {\r
- $prevuser = 0;\r
- $grade_records = array();\r
- $oldgrade = null;\r
- while ($used = rs_fetch_next_record($rs)) {\r
- if ($used->userid != $prevuser) {\r
- if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {\r
- $return = false;\r
- }\r
- $prevuser = $used->userid;\r
- $grade_records = array();\r
- $oldgrade = null;\r
- }\r
- if ($used->itemid == $this->id) {\r
- $oldgrade = $used;\r
- }\r
- $grade_records['gi'.$used->itemid] = $used->finalgrade;\r
- }\r
- if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {\r
- $return = false;\r
- }\r
- }\r
- rs_close($rs);\r
-\r
- return $return;\r
- }\r
-\r
- /**\r
- * internal function - does the final grade calculation\r
- */\r
- function use_formula($userid, $params, $useditems, $oldgrade) {\r
- if (empty($userid)) {\r
- return true;\r
- }\r
-\r
- // add missing final grade values\r
- // not graded (null) is counted as 0 - the spreadsheet way\r
- foreach($useditems as $gi) {\r
- if (!array_key_exists('gi'.$gi, $params)) {\r
- $params['gi'.$gi] = 0;\r
- } else {\r
- $params['gi'.$gi] = (float)$params['gi'.$gi];\r
- }\r
- }\r
-\r
- // can not use own final grade during calculation\r
- unset($params['gi'.$this->id]);\r
-\r
- // insert final grade - will be needed later anyway\r
- if ($oldgrade) {\r
- $grade = new grade_grade($oldgrade, false); // fetching from db is not needed\r
- $grade->grade_item =& $this;\r
-\r
- } else {\r
- $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);\r
- $grade->insert('system');\r
- $grade->grade_item =& $this;\r
-\r
- $oldgrade = new object();\r
- $oldgrade->finalgrade = $grade->finalgrade;\r
- $oldgrade->rawgrade = $grade->rawgrade;\r
- }\r
-\r
- // no need to recalculate locked or overridden grades\r
- if ($grade->is_locked() or $grade->is_overridden()) {\r
- return true;\r
- }\r
-\r
- // do the calculation\r
- $this->formula->set_params($params);\r
- $result = $this->formula->evaluate();\r
-\r
- // no raw grade for calculated grades - only final\r
- $grade->rawgrade = null;\r
-\r
-\r
- if ($result === false) {\r
- $grade->finalgrade = null;\r
-\r
- } else {\r
- // normalize\r
- $result = bounded_number($this->grademin, $result, $this->grademax);\r
- if ($this->gradetype == GRADE_TYPE_SCALE) {\r
- $result = round($result+0.00001); // round scales upwards\r
- }\r
- $grade->finalgrade = $result;\r
- }\r
-\r
- // update in db if changed\r
- if ( $grade->finalgrade !== $oldgrade->finalgrade\r
- or $grade->rawgrade !== $oldgrade->rawgrade) {\r
-\r
- $grade->update('system');\r
- }\r
-\r
- if ($result !== false) {\r
- //lock grade if needed\r
- }\r
-\r
- if ($result === false) {\r
- return false;\r
- } else {\r
- return true;\r
- }\r
-\r
- }\r
-\r
- /**\r
- * Validate the formula.\r
- * @param string $formula\r
- * @return boolean true if calculation possible, false otherwise\r
- */\r
- function validate_formula($formulastr) {\r
- global $CFG;\r
- require_once($CFG->libdir.'/mathslib.php');\r
-\r
- $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);\r
-\r
- if (empty($formulastr)) {\r
- return true;\r
- }\r
-\r
- if (strpos($formulastr, '=') !== 0) {\r
- return get_string('errorcalculationnoequal', 'grades');\r
- }\r
-\r
- // get used items\r
- if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {\r
- $useditems = array_unique($matches[1]); // remove duplicates\r
- } else {\r
- $useditems = array();\r
- }\r
- \r
- // MDL-11902\r
- // unset the value if formula is trying to reference to itself\r
- // but array keys does not match itemid\r
- if (!empty($this->id)) {\r
- $useditems = array_diff($useditems, array($this->id));\r
- //unset($useditems[$this->id]);\r
- }\r
-\r
- // prepare formula and init maths library\r
- $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);\r
- $formula = new calc_formula($formula);\r
-\r
-\r
- if (empty($useditems)) {\r
- $grade_items = array();\r
-\r
- } else {\r
- $gis = implode(',', $useditems);\r
-\r
- $sql = "SELECT gi.*\r
- FROM {$CFG->prefix}grade_items gi\r
- WHERE gi.id IN ($gis) and gi.courseid={$this->courseid}"; // from the same course only!\r
-\r
- if (!$grade_items = get_records_sql($sql)) {\r
- $grade_items = array();\r
- }\r
- }\r
-\r
- $params = array();\r
- foreach ($useditems as $itemid) {\r
- // make sure all grade items exist in this course\r
- if (!array_key_exists($itemid, $grade_items)) {\r
- return false;\r
- }\r
- // use max grade when testing formula, this should be ok in 99.9%\r
- // division by 0 is one of possible problems\r
- $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;\r
- }\r
-\r
- // do the calculation\r
- $formula->set_params($params);\r
- $result = $formula->evaluate();\r
-\r
- // false as result indicates some problem\r
- if ($result === false) {\r
- // TODO: add more error hints\r
- return get_string('errorcalculationunknown', 'grades');\r
- } else {\r
- return true;\r
- }\r
- }\r
-\r
- /**\r
- * 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.\r
- * @return int Display type\r
- */\r
- function get_displaytype() {\r
- global $CFG;\r
-\r
- if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {\r
- return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);\r
-\r
- } else {\r
- return $this->display;\r
- }\r
- }\r
-\r
- /**\r
- * 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.\r
- * @return int Decimals (0 - 5)\r
- */\r
- function get_decimals() {\r
- global $CFG;\r
-\r
- if (is_null($this->decimals)) {\r
- return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);\r
-\r
- } else {\r
- return $this->decimals;\r
- }\r
- }\r
-}\r
-?>\r
+<?php // $Id$
+
+///////////////////////////////////////////////////////////////////////////
+// //
+// NOTICE OF COPYRIGHT //
+// //
+// Moodle - Modular Object-Oriented Dynamic Learning Environment //
+// http://moodle.com //
+// //
+// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
+// //
+// This program 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 2 of the License, or //
+// (at your option) any later version. //
+// //
+// This program 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: //
+// //
+// http://www.gnu.org/copyleft/gpl.html //
+// //
+///////////////////////////////////////////////////////////////////////////
+
+require_once('grade_object.php');
+
+/**
+ * Class representing a grade item. It is responsible for handling its DB representation,
+ * modifying and returning its metadata.
+ */
+class grade_item extends grade_object {
+ /**
+ * DB Table (used by grade_object).
+ * @var string $table
+ */
+ var $table = 'grade_items';
+
+ /**
+ * Array of required table fields, must start with 'id'.
+ * @var array $required_fields
+ */
+ var $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
+ 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
+ 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
+ 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 'needsupdate', 'timecreated',
+ 'timemodified');
+
+ /**
+ * The course this grade_item belongs to.
+ * @var int $courseid
+ */
+ var $courseid;
+
+ /**
+ * The category this grade_item belongs to (optional).
+ * @var int $categoryid
+ */
+ var $categoryid;
+
+ /**
+ * The grade_category object referenced $this->iteminstance (itemtype must be == 'category' or == 'course' in that case).
+ * @var object $item_category
+ */
+ var $item_category;
+
+ /**
+ * The grade_category object referenced by $this->categoryid.
+ * @var object $parent_category
+ */
+ var $parent_category;
+
+
+ /**
+ * The name of this grade_item (pushed by the module).
+ * @var string $itemname
+ */
+ var $itemname;
+
+ /**
+ * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
+ * @var string $itemtype
+ */
+ var $itemtype;
+
+ /**
+ * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
+ * @var string $itemmodule
+ */
+ var $itemmodule;
+
+ /**
+ * ID of the item module
+ * @var int $iteminstance
+ */
+ var $iteminstance;
+
+ /**
+ * Number of the item in a series of multiple grades pushed by an activity.
+ * @var int $itemnumber
+ */
+ var $itemnumber;
+
+ /**
+ * Info and notes about this item.
+ * @var string $iteminfo
+ */
+ var $iteminfo;
+
+ /**
+ * Arbitrary idnumber provided by the module responsible.
+ * @var string $idnumber
+ */
+ var $idnumber;
+
+ /**
+ * Calculation string used for this item.
+ * @var string $calculation
+ */
+ var $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 boolean
+ */
+ var $calculation_normalized;
+ /**
+ * Math evaluation object
+ */
+ var $formula;
+
+ /**
+ * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
+ * @var int $gradetype
+ */
+ var $gradetype = GRADE_TYPE_VALUE;
+
+ /**
+ * Maximum allowable grade.
+ * @var float $grademax
+ */
+ var $grademax = 100;
+
+ /**
+ * Minimum allowable grade.
+ * @var float $grademin
+ */
+ var $grademin = 0;
+
+ /**
+ * id of the scale, if this grade is based on a scale.
+ * @var int $scaleid
+ */
+ var $scaleid;
+
+ /**
+ * A grade_scale object (referenced by $this->scaleid).
+ * @var object $scale
+ */
+ var $scale;
+
+ /**
+ * The id of the optional grade_outcome associated with this grade_item.
+ * @var int $outcomeid
+ */
+ var $outcomeid;
+
+ /**
+ * The grade_outcome this grade is associated with, if applicable.
+ * @var object $outcome
+ */
+ var $outcome;
+
+ /**
+ * grade required to pass. (grademin <= gradepass <= grademax)
+ * @var float $gradepass
+ */
+ var $gradepass = 0;
+
+ /**
+ * Multiply all grades by this number.
+ * @var float $multfactor
+ */
+ var $multfactor = 1.0;
+
+ /**
+ * Add this to all grades.
+ * @var float $plusfactor
+ */
+ var $plusfactor = 0;
+
+ /**
+ * Aggregation coeficient used for weighted averages
+ * @var float $aggregationcoef
+ */
+ var $aggregationcoef = 0;
+
+ /**
+ * Sorting order of the columns.
+ * @var int $sortorder
+ */
+ var $sortorder = 0;
+
+ /**
+ * Display type of the grades (Real, Percentage, Letter, or default).
+ * @var int $display
+ */
+ var $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
+ */
+ var $decimals = null;
+
+ /**
+ * 0 if visible, 1 always hidden or date not visible until
+ * @var int $hidden
+ */
+ var $hidden = 0;
+
+ /**
+ * 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
+ */
+ var $locked = 0;
+
+ /**
+ * Date after which the grade will be locked. Empty means no automatic locking.
+ * @var int $locktime
+ */
+ var $locktime = 0;
+
+ /**
+ * If set, the whole column will be recalculated, then this flag will be switched off.
+ * @var boolean $needsupdate
+ */
+ var $needsupdate = 1;
+
+ /**
+ * Cached dependson array
+ */
+ var $dependson_cache = null;
+
+ /**
+ * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
+ * Force regrading if necessary
+ * @param string $source from where was the object inserted (mod/forum, manual, etc.)
+ * @return boolean success
+ */
+ 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();
+ }
+
+ return parent::update($source);
+ }
+
+ /**
+ * 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 boolean
+ */
+ 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;
+ $grademaxdiff = $db_item->grademax != $this->grademax;
+ $grademindiff = $db_item->grademin != $this->grademin;
+ $scaleiddiff = $db_item->scaleid != $this->scaleid;
+ $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
+ $multfactordiff = $db_item->multfactor != $this->multfactor;
+ $plusfactordiff = $db_item->plusfactor != $this->plusfactor;
+ $locktimediff = $db_item->locktime != $this->locktime;
+ $acoefdiff = $db_item->aggregationcoef != $this->aggregationcoef;
+
+ $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 || $locktimediff);
+ }
+
+ /**
+ * Finds and returns a grade_item instance based on params.
+ * @static
+ *
+ * @param array $params associative arrays varname=>value
+ * @return object grade_item instance or false if none found.
+ */
+ function fetch($params) {
+ return grade_object::fetch_helper('grade_items', 'grade_item', $params);
+ }
+
+ /**
+ * Finds and returns all grade_item instances based on params.
+ * @static
+ *
+ * @param array $params associative arrays varname=>value
+ * @return array array of grade_item insatnces or false if none found.
+ */
+ 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 boolean success
+ */
+ function delete($source=null) {
+ 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);
+ }
+ }
+
+ return parent::delete($source);
+ }
+
+ /**
+ * 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
+ */
+ function insert($source=null) {
+ global $CFG;
+
+ if (empty($this->courseid)) {
+ error('Can not insert grade item without course id!');
+ }
+
+ // 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 = get_field_select('grade_items', 'MAX(sortorder)', "courseid = {$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;
+ }
+
+ if (parent::insert($source)) {
+ // force regrading of items if needed
+ $this->force_regrading();
+ 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 boolean success
+ */
+ function add_idnumber($idnumber) {
+ if (!empty($this->idnumber)) {
+ return false;
+ }
+
+ if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
+ if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
+ return false;
+ }
+ if (!empty($cm->idnumber)) {
+ return false;
+ }
+ if (set_field('course_modules', 'idnumber', addslashes($idnumber), 'id', $cm->id)) {
+ $this->idnumber = $idnumber;
+ return $this->update();
+ }
+ return false;
+
+ } 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
+ * @return boolean Locked state
+ */
+ function is_locked($userid=NULL) {
+ 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 $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
+ * @param boolean $cascade lock/unlock child objects too
+ * @param boolean $refresh refresh grades when unlocking
+ * @return boolean true if grade_item all grades updated, false if at least one update fails
+ */
+ 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
+ */
+ 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
+ */
+ function set_locktime($locktime) {
+ $this->locktime = $locktime;
+ $this->update();
+ }
+
+ /**
+ * Set the locktime for this grade item.
+ *
+ * @return int $locktime timestamp for lock to activate
+ */
+ function get_locktime() {
+ return $this->locktime;
+ }
+
+ /**
+ * Returns the hidden state of this grade_item
+ * @return boolean hidden state
+ */
+ function is_hidden() {
+ return ($this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()));
+ }
+
+ /**
+ * Check grade item hidden status.
+ * @return int 0 means visible, 1 hidden always, timestamp hidden until
+ */
+ function get_hidden() {
+ return $this->hidden;
+ }
+
+ /**
+ * Set the hidden status of grade_item and all grades, 0 mean visible, 1 always hidden, number means date to hide until.
+ * @param int $hidden new hidden status
+ * @param boolean $cascade apply to child objects too
+ * @return void
+ */
+ function set_hidden($hidden, $cascade=false) {
+ $this->hidden = $hidden;
+ $this->update();
+
+ 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);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the number of grades that are hidden.
+ * @param return int Number of hidden grades
+ */
+ function has_hidden_grades($groupsql="", $groupwheresql="") {
+ global $CFG;
+ return get_field_sql("SELECT COUNT(*) FROM {$CFG->prefix}grade_grades g LEFT JOIN "
+ ."{$CFG->prefix}user u ON g.userid = u.id $groupsql WHERE itemid = $this->id AND hidden = 1 $groupwheresql");
+ }
+
+ /**
+ * Mark regrading as finished successfully.
+ */
+ function regrading_finished() {
+ $this->needsupdate = 0;
+ //do not use $this->update() because we do not want this logged in grade_item_history
+ set_field('grade_items', 'needsupdate', 0, '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!!
+ *
+ * @return boolean true if ok, error string otherwise
+ */
+ function regrade_final_grades($userid=null) {
+ global $CFG;
+
+ // 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->get_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) {
+ $rs = get_recordset_select('grade_grades', "itemid={$this->id} AND userid=$userid", '', $fields);
+ } else {
+ $rs = get_recordset('grade_grades', 'itemid', $this->id, '', $fields);
+ }
+ if ($rs) {
+ while ($grade_record = rs_fetch_next_record($rs)) {
+ $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_record->finalgrade !== $grade->finalgrade) {
+ if (!$grade->update('system')) {
+ $result = "Internal error updating final grade";
+ }
+ }
+ }
+ rs_close($rs);
+ }
+
+ 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 object $rawgrade The raw grade value.
+ * @return mixed
+ */
+ 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: this is not compatible with current assignment grading
+ if ($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 bounded_number($this->grademin, $rawgrade, $this->grademax);
+
+ } 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: this is not compatible with current assignment grading
+ if ($rawmin != $this->grademin or $rawmax != $this->grademax) {
+ $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
+ }
+
+ return (int)bounded_number(0, round($rawgrade+0.00001), $this->grademax);
+
+
+ } 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 {
+ dubugging("Unkown grade type");
+ return null;;
+ }
+ }
+
+ /**
+ * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
+ * @return void
+ */
+ function force_regrading() {
+ $this->needsupdate = 1;
+ //mark this item and course item only - categories and calculated items are always regraded
+ $wheresql = "(itemtype='course' OR id={$this->id}) AND courseid={$this->courseid}";
+ set_field_select('grade_items', 'needsupdate', 1, $wheresql);
+ }
+
+ /**
+ * Instantiates a grade_scale object whose data is retrieved from the DB,
+ * if this item's scaleid variable is set.
+ * @return object grade_scale or null if no scale used
+ */
+ 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));
+ $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 whose data is retrieved from the DB,
+ * if this item's outcomeid variable is set.
+ * @return object grade_outcome
+ */
+ 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 mixed grade_category object if applicable, false if course item
+ */
+ 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 object Grade_category
+ */
+ 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 category item
+ *
+ * @return mixed grade_category object if applicable, false otherwise
+ */
+ 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 object Grade_category
+ */
+ function load_item_category() {
+ if (empty($this->category->id)) {
+ $this->item_category = $this->get_item_category();
+ }
+ return $this->item_category;
+ }
+
+ /**
+ * Is the grade item associated with category?
+ * @return boolean
+ */
+ function is_category_item() {
+ return ($this->itemtype == 'category');
+ }
+
+ /**
+ * Is the grade item associated with course?
+ * @return boolean
+ */
+ function is_course_item() {
+ return ($this->itemtype == 'course');
+ }
+
+ /**
+ * Is this a manualy graded item?
+ * @return boolean
+ */
+ function is_manual_item() {
+ return ($this->itemtype == 'manual');
+ }
+
+ /**
+ * Is this an outcome item?
+ * @return boolean
+ */
+ function is_outcome_item() {
+ return !empty($this->outcomeid);
+ }
+
+ /**
+ * Is the grade item normal - associated with module, plugin or something else?
+ * @return boolean
+ */
+ function is_normal_item() {
+ return ($this->itemtype != 'course' and $this->itemtype != 'category' and $this->itemtype != 'manual');
+ }
+
+ /**
+ * Returns true if grade items uses raw grades
+ * @return boolean
+ */
+ function is_raw_used() {
+ return ($this->is_normal_item() and !$this->is_calculated() and !$this->is_outcome_item());
+ }
+
+ /**
+ * Returns grade item associated with the course
+ * @param int $courseid
+ * @return course item object
+ */
+ 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 grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'));
+ }
+
+ /**
+ * Is grading object editable?
+ * @return boolean
+ */
+ function is_editable() {
+ return true;
+ }
+
+ /**
+ * Checks if grade calculated. Returns this object's calculation.
+ * @return boolean true if grade item calculated.
+ */
+ 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 preg_match('/##gi\d+##/', $this->calculation)) {
+ $this->set_calculation($this->calculation);
+ }
+
+ return !empty($this->calculation);
+ }
+
+ /**
+ * Returns calculation string if grade calculated.
+ * @return mixed string if calculation used, null if not
+ */
+ 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 boolean success
+ */
+ 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
+ * @static
+ * @param string $formula
+ * @return string denormalized string
+ */
+ 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
+ * @static
+ * @param string $formula
+ * @return string normalized string
+ */
+ 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 final grade
+ * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
+ */
+ function get_final($userid=NULL) {
+ if ($userid) {
+ if ($user = get_record('grade_grades', 'itemid', $this->id, 'userid', $userid)) {
+ return $user;
+ }
+
+ } else {
+ if ($grades = get_records('grade_grades', 'itemid', $this->id)) {
+ //TODO: speed up with better SQL
+ $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
+ * @return object grade_grade object instance
+ */
+ 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
+ */
+ 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 idnumber
+ */
+ 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 string idnumber
+ */
+ 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
+ * @return void
+ */
+ function set_sortorder($sortorder) {
+ $this->sortorder = $sortorder;
+ $this->update();
+ }
+
+ function move_after_sortorder($sortorder) {
+ global $CFG;
+
+ //make some room first
+ $sql = "UPDATE {$CFG->prefix}grade_items
+ SET sortorder = sortorder + 1
+ WHERE sortorder > $sortorder AND courseid = {$this->courseid}";
+ execute_sql($sql, false);
+
+ $this->set_sortorder($sortorder + 1);
+ }
+
+ /**
+ * Returns the most descriptive field for this object. This is a standard method used
+ * when we do not know the exact type of an object.
+ * @return string name
+ */
+ function get_name() {
+ if (!empty($this->itemname)) {
+ // MDL-10557
+ return format_string($this->itemname);
+
+ } else if ($this->is_course_item()) {
+ return get_string('coursetotal', 'grades');
+
+ } else if ($this->is_category_item()) {
+ return get_string('categorytotal', 'grades');
+
+ } else {
+ return get_string('grade');
+ }
+ }
+
+ /**
+ * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
+ * @param int $parentid
+ * @return boolean success;
+ */
+ function set_parent($parentid) {
+ if ($this->is_course_item() or $this->is_category_item()) {
+ error('Can not set parent for category or course item!');
+ }
+
+ if ($this->categoryid == $parentid) {
+ return true;
+ }
+
+ // find parent and check course id
+ if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
+ return false;
+ }
+
+ $this->force_regrading();
+
+ // set new parent
+ $this->categoryid = $parent_category->id;
+ $this->parent_category =& $parent_category;
+
+ return $this->update();
+ }
+
+ /**
+ * Finds out on which other items does this depend directly when doing calculation or category agregation
+ * @param bool $reset_cache
+ * @return array of grade_item ids this one depends on
+ */
+ function depends_on($reset_cache=false) {
+ global $CFG;
+
+ if ($reset_cache) {
+ $this->dependson_cache = null;
+ } else if (isset($this->dependson_cache)) {
+ return $this->dependson_cache;
+ }
+
+ if ($this->is_locked()) {
+ // 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 duplicates
+ return $this->dependson_cache;
+ } else {
+ $this->dependson_cache = array();
+ return $this->dependson_cache;
+ }
+
+ } else if ($grade_category = $this->load_item_category()) {
+ //only items with numeric or scale values can be aggregated
+ if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
+ $this->dependson_cache = array();
+ return $this->dependson_cache;
+ }
+
+ // If global aggregateoutcomes is set, override category value
+ if ($CFG->grade_aggregateoutcomes != -1) {
+ $grade_category->aggregateoutcomes = $CFG->grade_aggregateoutcomes;
+ }
+
+ // If global aggregatesubcats is set, override category value
+ if ($CFG->grade_aggregatesubcats != -1) {
+ $grade_category->aggregatesubcats = $CFG->grade_aggregatesubcats;
+ }
+
+ if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
+ $outcomes_sql = "";
+ } else {
+ $outcomes_sql = "AND gi.outcomeid IS NULL";
+ }
+
+ if ($grade_category->aggregatesubcats) {
+ // return all children excluding category items
+ $sql = "SELECT gi.id
+ FROM {$CFG->prefix}grade_items gi
+ WHERE (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")
+ $outcomes_sql
+ AND gi.categoryid IN (
+ SELECT gc.id
+ FROM {$CFG->prefix}grade_categories gc
+ WHERE gc.path LIKE '%/{$grade_category->id}/%')";
+
+ } else {
+ $sql = "SELECT gi.id
+ FROM {$CFG->prefix}grade_items gi
+ WHERE gi.categoryid = {$grade_category->id}
+ AND (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")
+ $outcomes_sql
+
+ UNION
+
+ SELECT gi.id
+ FROM {$CFG->prefix}grade_items gi, {$CFG->prefix}grade_categories gc
+ WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
+ AND gc.parent = {$grade_category->id}
+ AND (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")
+ $outcomes_sql";
+ }
+
+ if ($children = get_records_sql($sql)) {
+ $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 moudles, plugins.
+ * @param int $userid optional, one user only
+ */
+ function refresh_grades($userid=0) {
+ if ($this->itemtype == 'mod') {
+ if ($this->is_outcome_item()) {
+ //nothing to do
+ return;
+ }
+
+ if (!$activity = get_record($this->itemmodule, 'id', $this->iteminstance)) {
+ debugging('Can not find activity');
+ return;
+ }
+
+ if (! $cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
+ debuggin('Can not find course module');
+ return;
+ }
+
+ $activity->modname = $this->itemmodule;
+ $activity->cmidnumber = $cm->idnumber;
+
+ grade_update_mod_grades($activity);
+ }
+ }
+
+ /**
+ * 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 mixed $finalgrade float value of final grade - false means do not change
+ * @param string $howmodified modification source
+ * @param string $note optional note
+ * @param mixed $feedback teachers feedback as string - false means do not change
+ * @param int $feedbackformat
+ * @return boolean success
+ */
+ function update_final_grade($userid, $finalgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
+ global $USER, $CFG;
+
+ if (empty($usermodified)) {
+ $usermodified = $USER->id;
+ }
+
+ $result = true;
+
+ // no grading used or locked
+ if ($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_item
+
+ $grade->usermodified = $usermodified;
+
+ if ($grade->is_locked()) {
+ // do not update locked grades at all
+ return 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 object();
+ $oldgrade->finalgrade = $grade->finalgrade;
+ $oldgrade->overridden = $grade->overridden;
+ $oldgrade->feedback = $grade->feedback;
+ $oldgrade->feedbackformat = $grade->feedbackformat;
+
+ if ($finalgrade !== false or $feedback !== false) {
+ if (($this->is_outcome_item() or $this->is_manual_item()) and !$this->is_calculated()) {
+ // final grades updated only by user - no need for overriding
+ $grade->overridden = 0;
+
+ } else {
+ $grade->overridden = time();
+ }
+ }
+
+ if ($finalgrade !== false) {
+ if (!is_null($finalgrade)) {
+ $finalgrade = bounded_number($this->grademin, $finalgrade, $this->grademax);
+ } else {
+ $finalgrade = $finalgrade;
+ }
+ $grade->finalgrade = $finalgrade;
+ }
+
+ // do we have comment from teacher?
+ if ($feedback !== false) {
+ $grade->feedback = $feedback;
+ $grade->feedbackformat = $feedbackformat;
+ }
+
+ if (empty($grade->id)) {
+ $result = (boolean)$grade->insert($source);
+
+ } else if ($grade->finalgrade !== $oldgrade->finalgrade
+ or $grade->feedback !== $oldgrade->feedback
+ or $grade->feedbackformat !== $oldgrade->feedbackformat) {
+ $result = $grade->update($source);
+ }
+
+ if (!$result) {
+ // something went wrong - better force final grade recalculation
+ $this->force_regrading();
+
+ } else if ($this->is_course_item() and !$this->needsupdate) {
+ if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {
+ $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)) {
+ $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 $howmodified modification source
+ * @param string $note optional note
+ * @param mixed $feedback teachers feedback as string - false means do not change
+ * @param int $feedbackformat
+ * @return boolean success
+ */
+ function update_raw_grade($userid, $rawgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
+ global $USER;
+
+ if (empty($usermodified)) {
+ $usermodified = $USER->id;
+ }
+
+ $result = true;
+
+ // calculated grades can not be updated; course and category can not be updated because they are aggregated
+ if ($this->is_calculated() or $this->is_outcome_item() or !$this->is_normal_item()
+ or $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_item
+
+ $grade->usermodified = $usermodified;
+
+ if ($grade->is_locked()) {
+ // do not update locked grades at all
+ return 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 object();
+ $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;
+
+ // fist copy current grademin/max and scale
+ $grade->rawgrademin = $this->grademin;
+ $grade->rawgrademax = $this->grademax;
+ $grade->rawscaleid = $this->scaleid;
+
+ // change raw grade?
+ if ($rawgrade !== false) {
+ $grade->rawgrade = $rawgrade;
+ }
+
+ // do we have comment from teacher?
+ if ($feedback !== false) {
+ $grade->feedback = $feedback;
+ $grade->feedbackformat = $feedbackformat;
+ }
+
+ if (empty($grade->id)) {
+ $result = (boolean)$grade->insert($source);
+
+ } else if ($grade->finalgrade !== $oldgrade->finalgrade
+ or $grade->rawgrade !== $oldgrade->rawgrade
+ or $grade->rawgrademin !== $oldgrade->rawgrademin
+ or $grade->rawgrademax !== $oldgrade->rawgrademax
+ or $grade->rawscaleid !== $oldgrade->rawscaleid
+ or $grade->feedback !== $oldgrade->feedback
+ or $grade->feedbackformat !== $oldgrade->feedbackformat) {
+
+ $result = $grade->update($source);
+ }
+
+ if (!$result) {
+ // something went wrong - better force final grade recalculation
+ $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)) {
+ $this->force_regrading();
+ }
+ } else {
+ $this->force_regrading();
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Calculates final grade values using the formula in calculation property.
+ * The parameters are taken from final grades of grade items in current course only.
+ * @return boolean false if error
+ */
+ function compute($userid=null) {
+ global $CFG;
+
+ if (!$this->is_calculated()) {
+ return false;
+ }
+
+ require_once($CFG->libdir.'/mathslib.php');
+
+ if ($this->is_locked()) {
+ return true; // no need to recalculate locked items
+ }
+
+ // get used items
+ $useditems = $this->depends_on();
+
+ // prepare formula and init maths library
+ $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
+ $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 = implode(',', array_merge($useditems, array($this->id)));
+
+ if ($userid) {
+ $usersql = "AND g.userid=$userid";
+ } else {
+ $usersql = "";
+ }
+
+ $grade_inst = new grade_grade();
+ $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
+
+ $sql = "SELECT $fields
+ FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi
+ WHERE gi.id = g.itemid AND gi.courseid={$this->courseid} AND gi.id IN ($gis) $usersql
+ ORDER BY g.userid";
+
+ $return = true;
+
+ // group the grades by userid and use formula on the group
+ if ($rs = get_recordset_sql($sql)) {
+ $prevuser = 0;
+ $grade_records = array();
+ $oldgrade = null;
+ while ($used = rs_fetch_next_record($rs)) {
+ 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($rs);
+
+ return $return;
+ }
+
+ /**
+ * internal function - does the final grade calculation
+ */
+ 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
+ foreach($useditems as $gi) {
+ if (!array_key_exists('gi'.$gi, $params)) {
+ $params['gi'.$gi] = 0;
+ } else {
+ $params['gi'.$gi] = (float)$params['gi'.$gi];
+ }
+ }
+
+ // can not use own final grade during calculation
+ unset($params['gi'.$this->id]);
+
+ // insert final grade - will be needed later anyway
+ if ($oldgrade) {
+ $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->insert('system');
+ $grade->grade_item =& $this;
+
+ $oldgrade = new object();
+ $oldgrade->finalgrade = $grade->finalgrade;
+ $oldgrade->rawgrade = $grade->rawgrade;
+ }
+
+ // no need to recalculate locked or overridden grades
+ if ($grade->is_locked() or $grade->is_overridden()) {
+ return true;
+ }
+
+ // do the calculation
+ $this->formula->set_params($params);
+ $result = $this->formula->evaluate();
+
+ // no raw grade for calculated grades - only final
+ $grade->rawgrade = null;
+
+
+ if ($result === false) {
+ $grade->finalgrade = null;
+
+ } else {
+ // normalize
+ $result = bounded_number($this->grademin, $result, $this->grademax);
+ if ($this->gradetype == GRADE_TYPE_SCALE) {
+ $result = round($result+0.00001); // round scales upwards
+ }
+ $grade->finalgrade = $result;
+ }
+
+ // update in db if changed
+ if ( $grade->finalgrade !== $oldgrade->finalgrade
+ or $grade->rawgrade !== $oldgrade->rawgrade) {
+
+ $grade->update('system');
+ }
+
+ if ($result !== false) {
+ //lock grade if needed
+ }
+
+ if ($result === false) {
+ return false;
+ } else {
+ return true;
+ }
+
+ }
+
+ /**
+ * Validate the formula.
+ * @param string $formula
+ * @return boolean true if calculation possible, false otherwise
+ */
+ function validate_formula($formulastr) {
+ global $CFG;
+ 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 items
+ if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
+ $useditems = array_unique($matches[1]); // remove duplicates
+ } else {
+ $useditems = array();
+ }
+
+ if (!empty($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 {
+ $gis = implode(',', $useditems);
+
+ $sql = "SELECT gi.*
+ FROM {$CFG->prefix}grade_items gi
+ WHERE gi.id IN ($gis) and gi.courseid={$this->courseid}"; // from the same course only!
+
+ if (!$grade_items = get_records_sql($sql)) {
+ $grade_items = array();
+ }
+ }
+
+ $params = array();
+ foreach ($useditems as $itemid) {
+ // make sure all grade items exist in this course
+ if (!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 problem
+ if ($result === false) {
+ // TODO: add more error hints
+ return 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
+ */
+ 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)
+ */
+ function get_decimals() {
+ global $CFG;
+
+ if (is_null($this->decimals)) {
+ return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
+
+ } else {
+ return $this->decimals;
+ }
+ }
+}
+?>