From b8ff92b66b6dc498cac94ebcfd88b571b431611a Mon Sep 17 00:00:00 2001 From: skodak Date: Mon, 18 Jun 2007 13:43:40 +0000 Subject: [PATCH] MDL-9137 various grading improvements 1/ initial support for migration of old grade_items and categories (not tested) 2/ rewritten grade update and calculation logic 3/ initial support for calculation formulas 4/ minor API refactoring and cleanup 5/ various small bugfixes 6/ fixed use of grademax with scales 7/ fixed some unit tests TODO: * implement proper locking of grades - needs discussion * force recalculation of all formulas after adding/removing/changing of grade items * better delete flag support * support for NULLs n backup - Eloy already proposed a solution * support for NULLs in set_field() * speedup * more unit tests nd functional tests --- grade/export/lib.php | 5 +- lib/evalmath/evalmath.class.php | 16 +- lib/grade/grade_calculation.php | 138 +++- lib/grade/grade_category.php | 322 +++++---- lib/grade/grade_grades_raw.php | 4 +- lib/grade/grade_item.php | 610 +++++++++--------- lib/gradelib.php | 194 +++++- lib/simpletest/fixtures/gradetest.php | 191 ++---- .../grade/simpletest/testgradecategory.php | 58 +- .../grade/simpletest/testgradeitem.php | 85 +-- .../grade/simpletest/testgraderaw.php | 2 +- .../grade/simpletest/testgradetree.php | 2 +- lib/simpletest/testgradelib.php | 4 +- lib/simpletest/testmathslib.php | 9 + 14 files changed, 879 insertions(+), 761 deletions(-) diff --git a/grade/export/lib.php b/grade/export/lib.php index ed8b8387d6..791b4f3dbc 100755 --- a/grade/export/lib.php +++ b/grade/export/lib.php @@ -82,6 +82,10 @@ class grade_export { $this->id = $id; $this->course = $course; + // first make sure we have all final grades + // TODO: check that no grade_item has needsupdate set + grade_update_final_grades(); + /// Check to see if groups are being used in this course if ($groupmode = groupmode($course)) { // Groups are being used @@ -129,7 +133,6 @@ class grade_export { if ($gradeitems) { foreach ($gradeitems as $gradeitem) { - $gradeitem -> generate_final(); // load as an array of grade_final objects if ($itemgrades = $gradeitem -> load_final()) { diff --git a/lib/evalmath/evalmath.class.php b/lib/evalmath/evalmath.class.php index 37a419715c..197bae4170 100644 --- a/lib/evalmath/evalmath.class.php +++ b/lib/evalmath/evalmath.class.php @@ -124,7 +124,7 @@ class EvalMath { if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end //=============== // is it a variable assignment? - if (preg_match('/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches)) { + if (preg_match('/^\s*([a-z][a-z0-9]*)\s*=\s*(.+)$/', $expr, $matches)) { if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant return $this->trigger("cannot assign to constant '$matches[1]'"); } @@ -133,7 +133,7 @@ class EvalMath { return $this->v[$matches[1]]; // and return the resulting value //=============== // is it a function assignment? - } elseif (preg_match('/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) { + } elseif (preg_match('/^\s*([a-z][a-z0-9]*)\s*\(\s*([a-z][a-z0-9]*(?:\s*,\s*[a-z][a-z0-9]*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) { $fnn = $matches[1]; // get the function name if (in_array($matches[1], $this->fb)) { // make sure it isn't built in return $this->trigger("cannot redefine built-in function '$matches[1]()'"); @@ -142,7 +142,7 @@ class EvalMath { if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix for ($i = 0; $iv)) { $stack[$i] = $this->v[$token]; } else { @@ -193,7 +193,7 @@ class EvalMath { while(1) { // 1 Infinite Loop ;) $op = substr($expr, $index, 1); // get the first character at the current index // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand - $ex = preg_match('/^([a-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr($expr, $index), $match); + $ex = preg_match('/^([a-z][a-z0-9]*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr($expr, $index), $match); //=============== if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus? $stack->push('_'); // put a negation on the stack @@ -220,7 +220,7 @@ class EvalMath { if (is_null($o2)) return $this->trigger("unexpected ')'"); else $output[] = $o2; } - if (preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches)) { // did we just close a function? + if (preg_match("/^([a-z][a-z0-9]*)\($/", $stack->last(2), $matches)) { // did we just close a function? $fnn = $matches[1]; // get the function name $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you) $fn = $stack->pop(); @@ -248,7 +248,7 @@ class EvalMath { else $output[] = $o2; // pop the argument expression stuff and push onto the output } // make sure there was a function - if (!preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches)) + if (!preg_match("/^([a-z][a-z0-9]*)\($/", $stack->last(2), $matches)) return $this->trigger("unexpected ','"); $stack->push($stack->pop()+1); // increment the argument count $stack->push('('); // put the ( back on, we'll need to pop back to it again @@ -263,7 +263,7 @@ class EvalMath { } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number? $expecting_op = true; $val = $match[1]; - if (preg_match("/^([a-z]\w*)\($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses... + if (preg_match("/^([a-z][a-z0-9]*)\($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses... if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f) or array_key_exists($matches[1], $this->fc)) { // it's a func $stack->push($val); $stack->push(1); @@ -281,7 +281,7 @@ class EvalMath { } elseif ($op == ')') { //it could be only custom function with no params or general error if ($stack->last() != '(' or $stack->last(2) != 1) return $this->trigger("unexpected ')'"); - if (preg_match("/^([a-z]\w*)\($/", $stack->last(3), $matches)) { // did we just close a function? + if (preg_match("/^([a-z][a-z0-9]*)\($/", $stack->last(3), $matches)) { // did we just close a function? $stack->pop();// ( $stack->pop();// 1 $fn = $stack->pop(); diff --git a/lib/grade/grade_calculation.php b/lib/grade/grade_calculation.php index e3b75fb1b4..b9a6b17e94 100644 --- a/lib/grade/grade_calculation.php +++ b/lib/grade/grade_calculation.php @@ -40,8 +40,8 @@ class grade_calculation extends grade_object { * Array of class variables that are not part of the DB table fields * @var array $nonfields */ - var $nonfields = array('table', 'nonfields'); - + var $nonfields = array('table', 'nonfields', 'formula', 'useditems', 'grade_item'); + /** * A reference to the grade_item this calculation belongs to. * @var int $itemid @@ -59,21 +59,133 @@ class grade_calculation extends grade_object { * @var int $usermodified */ var $usermodified; - + + /** + * Calculation formula object + */ + var $formula; + + /** + * List of other items this calculation depends on + */ + var $useditems; + + /** + * Grade item object + */ + var $grade_item; + /** - * A formula parser object. - * @var object $parser - * @TODO implement parsing of formula and calculation MDL-9643 + * Applies the formula represented by this object. The parameteres are taken from final + * grades of grade items in current course only. + * @return boolean false if error */ - var $parser; + function compute() { + global $CFG; + require_once($CFG->libdir.'/mathslib.php'); + + if (empty($this->id) or empty($this->itemid)) { + debugging('Can not initialize calculation!'); + return false; + } + + // init grade_item + $this->grade_item = grade_item::fetch('id', $this->itemid); + + //init used items + $this->useditems = $this->dependson(); + + // init maths library + $this->formula = new calc_formula($this->calculation); + + // where to look for final grades + $gis = implode(',', array_merge($this->useditems, array($this->itemid))); + + $sql = "SELECT f.* + FROM {$CFG->prefix}grade_grades_final f, {$CFG->prefix}grade_items gi + WHERE gi.id = f.itemid AND gi.courseid={$this->grade_item->courseid} AND gi.id IN ($gis) + ORDER BY f.userid"; + + $return = true; + + if ($rs = get_recordset_sql($sql)) { + if ($rs->RecordCount() > 0) { + $prevuser = 0; + $grades = array(); + $final = null; + while ($subfinal = rs_fetch_next_record($rs)) { + if ($subfinal->userid != $prevuser) { + if (!$this->_use_formula($prevuser, $grades, $final)) { + $return = false; + } + $prevuser = $subfinal->userid; + $grades = array(); + $final = null; + } + if ($subfinal->itemid == $this->grade_item->id) { + $final = grade_grades_final::fetch('id', $subfinal->id); + } + $grades['gi'.$subfinal->itemid] = $subfinal->gradevalue; + } + if (!$this->_use_formula($prevuser, $grades, $final)) { + $return = false; + } + } + } + + return $return; + } + + /** + * internal function - does the final grade calculation + */ + function _use_formula($userid, $params, $final) { + if (empty($userid)) { + return true; + } + + // add missing final grade values - use 0 + foreach($this->useditems as $gi) { + if (!array_key_exists('gi'.$gi, $params)) { + $params['gi'.$gi] = 0; + } else { + $params['gi'.$gi] = (float)$params[$gi]; + } + } + + // can not use own final grade during calculation + unset($params['gi'.$this->grade_item->id]); + + // do the calculation + $this->formula->set_params($params); + $result = $this->formula->evaluate(); + + // insert final grade if needed + if (empty($final)) { + $this->grade_item->grade_grades_final[$userid] = new grade_grades_final(array('itemid'=>$this->grade_item->id, 'userid'=>$userid)); + $this->grade_item->grade_grades_final[$userid]->insert(); + } + + // store the result + if ($result === false) { + $final->grade_value = null; + $final->update(); + return false; + + } else { + $final = $result; + $this->grade_item->grade_grades_final[$userid]->update(); + return true; + } + } /** - * Applies the formula represented by this object to the value given, and returns the result. - * @param float $oldvalue - * @return float result + * Finds out on which other items does this calculation depend + * @return array of grade_item ids this one depends on */ - function compute($oldvalue) { - return $oldvalue; // TODO implement computation using parser + function dependson() { + preg_match_all('/gi([0-9]+)/i', $this->calculation, $matches); + return ($matches[1]); } /** @@ -89,7 +201,7 @@ class grade_calculation extends grade_object { * @param string $fields * @return object grade_calculation object or false if none found. */ - function fetch($field1, $value1, $field2='', $value2='', $field3='', $value3='', $fields="*") { + function fetch($field1, $value1, $field2='', $value2='', $field3='', $value3='', $fields="*") { if ($grade_calculation = get_record('grade_calculations', $field1, $value1, $field2, $value2, $field3, $value3, $fields)) { if (isset($this) && get_class($this) == 'grade_calculation') { print_object($this); diff --git a/lib/grade/grade_category.php b/lib/grade/grade_category.php index 15f4dd91ba..94f27c1ea4 100644 --- a/lib/grade/grade_category.php +++ b/lib/grade/grade_category.php @@ -125,16 +125,8 @@ class grade_category extends grade_object { * @param boolean $fetch Whether or not to fetch the corresponding row from the DB. * @param object $grade_item The associated grade_item object can be passed during construction. */ - function grade_category($params=NULL, $fetch=true, $grade_item=NULL) { + function grade_category($params=NULL, $fetch=true) { $this->grade_object($params, $fetch); - if (!empty($grade_item) && $grade_item->itemtype == 'category') { - $this->grade_item = $grade_item; - if (empty($this->grade_item->iteminstance)) { - $this->grade_item->iteminstance = $this->id; - $this->grade_item->update(); - } - } - $this->path = grade_category::build_path($this); } @@ -242,7 +234,10 @@ class grade_category extends grade_object { * This method also creates an associated grade_item if this wasn't done during construction. */ function insert() { - $result = parent::insert(); + if (!parent::insert()) { + debugging("Could not insert this category: " . print_r($this, true)); + return false; + } $this->path = grade_category::build_path($this); @@ -255,30 +250,19 @@ class grade_category extends grade_object { $this->update(); - if (empty($this->grade_item)) { - $grade_item = new grade_item(); - $grade_item->iteminstance = $this->id; - $grade_item->itemtype = 'category'; + // initialize grade_item for this category + $this->grade_item = $this->get_grade_item(); - if (!$grade_item->insert()) { - debugging("Could not insert this grade_item in the database: " . print_r($grade_item, true)); + // Notify parent category of need to update. + $this->load_parent_category(); + if (!empty($this->parent_category)) { + if (!$this->parent_category->flag_for_update()) { + debugging("Could not notify parent category of the need to update its final grades."); return false; } - - $this->grade_item = $grade_item; } - // Notify parent category of need to update. - if ($result) { - $this->load_parent_category(); - if (!empty($this->parent_category)) { - if (!$this->parent_category->flag_for_update()) { - debugging("Could not notify parent category of the need to update its final grades."); - return false; - } - } - } - return $result; + return true; } /** @@ -315,15 +299,15 @@ class grade_category extends grade_object { * @return boolean Success or failure */ function flag_for_update() { + if (empty($this->id)) { + debugging("Needsupdate requested before insering grade category."); + return true; + } + $result = true; $this->load_grade_item(); - if (empty($this->grade_item)) { - die("Associated grade_item object does not exist for this grade_category!" . print_object($this)); - // TODO Send error message, this is a critical error: each category MUST have a matching grade_item object and load_grade_item() is supposed to create one! - } - $paths = explode('/', $this->path); // Remove the first index, which is always empty @@ -348,60 +332,155 @@ class grade_category extends grade_object { * raw and final grades, which means that ultimately we must get grade_items as children. The category's aggregation * method is used to generate these raw grades, which can then be used by the category's associated grade_item * to apply calculations to and generate final grades. + * + * This function must be use ONLY from grade_item::update_final_grade(), + * because the calculation must be done in correct order!! + * Steps to follow: - * 1. If the children are categories, AND their grade_item's needsupdate is true call generate_grades() on each of them (recursion) - * 2. Get final grades from immediate children (if the children are categories, get the final grades from their grade_item) + * 1. Get final grades from immediate children (if the children are categories, get the final grades from their grade_item) * 3. Aggregate these grades * 4. Save them under $this->grade_item->grade_grades_raw - * 5. Use the grade_item's methods for generating the final grades. */ function generate_grades() { - // 1. Get immediate children - $children = $this->get_children(1, 'flat'); + global $CFG; - if (empty($children)) { - debugging("Could not generate grades for this category, it has no children."); - return false; + $grade_item = $this->get_grade_item(); + if ($grade_item->gradetype == GRADE_TYPE_SCALE) { + $grade_item->load_scale(); } - // This assumes that all immediate children are of the same type (category OR item) - $childrentype = get_class(current($children)); + $dependson = $grade_item->dependson(); + $items = array(); + + foreach($dependson as $dep) { + $items[$dep] = grade_item::fetch('id', $dep); + } - $final_grades_for_aggregation = array(); + // where to look for final grades + $gis = implode(',', array_merge($dependson, array($grade_item->id))); - // 2. Get final grades from immediate children, after generating them if needed. - // NOTE: Make sure that the arrays of final grades are indexed by userid. The resulting arrays are unlikely to match in sizes. - if ($childrentype == 'grade_category') { - foreach ($children as $id => $category) { - $category->load_grade_item(); + $sql = "SELECT f.* + FROM {$CFG->prefix}grade_grades_final f, {$CFG->prefix}grade_items gi + WHERE gi.id = f.itemid AND gi.courseid={$grade_item->courseid} AND gi.id IN ($gis) + ORDER BY f.userid"; - if ($category->grade_item->needsupdate) { - $category->generate_grades(); + if ($rs = get_recordset_sql($sql)) { + if ($rs->RecordCount() > 0) { + $prevuser = 0; + $grades = array(); + while ($subfinal = rs_fetch_next_record($rs)) { + if ($subfinal->userid != $prevuser) { + $this->aggregate_grades($prevuser, $items, $grades, $grade_item, $dependson); + $prevuser = $subfinal->userid; + $grades = array(); + } + $grades[$subfinal->itemid] = $subfinal->gradevalue; } + $this->aggregate_grades($prevuser, $items, $grades, $grade_item, $dependson); + } + } + + //TODO: set grade to null for raw grades that do not have corresponding final grade + // using left join + + return true; + } + + /** + * internal function for vategory grades aggregation + */ + function aggregate_grades($userid, $items, $grades, &$grade_item, $dependson) { + if (empty($userid)) { + return; + } + + // remove the final item we used to get all existing final grades of this category + unset($grades[$grade_item->id]); - $final_grades_for_aggregation[] = $category->grade_item->get_standardised_final(); + if (empty($grades) or empty($items) or ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE)) { + // no grading + if ($raw = grade_grades_raw::fetch('itemid', $grade_item->id, 'userid', $userid)) { + $raw->gradevalue = null; + $raw->update(); } - } elseif ($childrentype == 'grade_item') { - foreach ($children as $id => $item) { - if ($item->needsupdate) { - $item->generate_final(); - } + return; + } - $final_grades_for_aggregation[] = $item->get_standardised_final(); + // normalize the grades first - all will have value 0...1 + foreach ($grades as $k=>$v) { + if (is_null($v)) { + // null means no grade + unset($grades[$k]); + continue; } + $grades[$k] = standardise_score($v, $items[$k]->grademin, $items[$k]->grademax, 0, 1); } - // 3. Aggregate the grades - $aggregated_grades = $this->aggregate_grades($final_grades_for_aggregation); + //sort and limit + $this->apply_limit_rules($grades); + sort($grades, SORT_NUMERIC); - // 4. Save the resulting array of grades as raw grades - $this->load_grade_item(); - $this->grade_item->save_raw($aggregated_grades); + if (count($grades) == 0) { + // no grading yet + if ($raw = grade_grades_raw::fetch('itemid', $grade_item->id, 'userid', $userid)) { + $raw->gradevalue = null; + $raw->update(); + } + return; + } - // 5. Use the grade_item's generate_final method - $this->grade_item->generate_final(); + switch ($this->aggregation) { + case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies + $num = count($grades); + $halfpoint = intval($num / 2); + + if($num % 2 == 0) { + $gradevalue = ($grades[ceil($halfpoint)] + $grades[floor($halfpoint)]) / 2; + } else { + $gradevalue = $grades[$halfpoint]; + } + break; + case GRADE_AGGREGATE_MIN: + $gradevalue = reset($grades); + break; + + case GRADE_AGGREGATE_MAX: + $gradevalue = array_pop($grades); + break; + + case GRADE_AGGREGATE_MEAN_ALL: // Arithmetic average of all grade items including event NULLs; NULL grade caunted as minimum + $num = count($dependson); // you can calculate sum from this one if you multiply it with count($this->dependson();-) + $sum = array_sum($grades); + $gradevalue = $sum / $num; + break; + + case GRADE_AGGREGATE_MEAN_GRADED: // Arithmetic average of all final grades, unfinished not calculated + $num = count($grades); + $sum = array_sum($grades); + $gradevalue = $sum / $num; + default: + break; + } + + $raw = new grade_grades_raw(array('itemid'=>$grade_item->id, 'userid'=>$userid)); + $raw->gradevalue = $gradevalue; + $raw->gradetype = $grade_item->gradetype; + $raw->scaleid = $grade_item->scaleid; + $raw->grademin = 0; + $raw->grademax = 1; + + // recalculate the gradevalue bvack to requested range + $raw->gradevalue = $grade_item->adjust_grade($raw); + + $raw->grademin = $grade_item->grademin; + $raw->grademax = $grade_item->grademax; + + if ($raw->id) { + $raw->update(); + } else { + $raw->insert(); + } - return true; } /** @@ -410,7 +489,7 @@ class grade_category extends grade_object { * @param array $grades * @return array Limited grades. */ - function apply_limit_rules($grades) { + function apply_limit_rules(&$grades) { rsort($grades, SORT_NUMERIC); if (!empty($this->droplow)) { for ($i = 0; $i < $this->droplow; $i++) { @@ -421,89 +500,6 @@ class grade_category extends grade_object { array_pop($grades); } } - sort($grades, SORT_NUMERIC); - return $grades; - } - - /** - * Given an array of arrays of values, standardised from 0 to 1 and indexed by userid, - * uses this category's aggregation method to - * compute and return a single array of grade_raw objects with the aggregated gradevalue. - * @param array $raw_grade_sets - * @return array Raw grade objects - */ - function aggregate_grades($final_grade_sets) { - if (empty($final_grade_sets)) { - debugging("Could not aggregate grades: no array of grades given to aggregate."); - return null; - } - - $aggregated_grades = array(); - $pooled_grades = array(); - - foreach ($final_grade_sets as $setkey => $set) { - foreach ($set as $userid => $final_grade) { - $this->load_grade_item(); - $value = standardise_score((float) $final_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax); - $pooled_grades[$userid][] = (string) $value; - } - } - - foreach ($pooled_grades as $userid => $grades) { - $aggregated_value = null; - - $grades = $this->apply_limit_rules($grades); - - if (count($grades) > 1) { - - switch ($this->aggregation) { - case GRADE_AGGREGATE_MEAN : // Arithmetic average - $num = count($grades); - $sum = array_sum($grades); - $aggregated_value = $sum / $num; - break; - case GRADE_AGGREGATE_MEDIAN : // Middle point value in the set: ignores frequencies - sort($grades); - $num = count($grades); - $halfpoint = intval($num / 2); - - if($num % 2 == 0) { - $aggregated_value = ($grades[ceil($halfpoint)] + $grades[floor($halfpoint)]) / 2; - } else { - $aggregated_value = $grades[$halfpoint]; - } - - break; - case GRADE_AGGREGATE_MODE : // Value that occurs most frequently. Not always useful (all values are likely to be different) - // TODO implement or reject - break; - case GRADE_AGGREGATE_SUM : // I don't see much point to this one either - $aggregated_value = array_sum($grades); - break; - default: - $num = count($grades); - $sum = array_sum($grades); - $aggregated_value = $sum / $num; - break; - } - } elseif (count($grades) == 1) { - $aggregated_value = $grades[0]; - } else { - // TODO what happens if the droplow and keephigh rules have deleted all grades? - $aggregated_value = 0; - } - - $grade_raw = new grade_grades_raw(); - - $grade_raw->userid = $userid; - $grade_raw->gradevalue = $aggregated_value; - $grade_raw->grademin = $this->grade_item->grademin; - $grade_raw->grademax = $this->grade_item->grademax; - $grade_raw->itemid = $this->grade_item->id; - $aggregated_grades[$userid] = $grade_raw; - } - - return $aggregated_grades; } /** @@ -687,21 +683,19 @@ class grade_category extends grade_object { return false; } - $grade_items = get_records_select('grade_items', "iteminstance = $this->id AND itemtype = 'category'", null, '*', 0, 1); + $grade_item = new grade_item(array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id), false); + if (!$grade_items = $grade_item->fetch_all_using_this()) { + // create a new one + $grade_item->gradetype = GRADE_TYPE_VALUE; + $grade_item->insert(); + + } else if (count($grade_items) == 1){ + // found existing one + $grade_item = reset($grade_items); - if ($grade_items){ - $params = current($grade_items); - $grade_item = new grade_item($params); } else { - $grade_item = new grade_item(); - } - - // If the associated grade_item isn't yet created, do it now. But first try loading it, in case it exists in DB. - if (empty($grade_item->id)) { - $grade_item->iteminstance = $this->id; - $grade_item->courseid = $this->courseid; - $grade_item->itemtype = 'category'; - $grade_item->insert(); + debugging("Found more than one grade_item attached to category id:".$this->id); + return false; } return $grade_item; diff --git a/lib/grade/grade_grades_raw.php b/lib/grade/grade_grades_raw.php index baa41bcb60..63fdee8167 100644 --- a/lib/grade/grade_grades_raw.php +++ b/lib/grade/grade_grades_raw.php @@ -289,7 +289,7 @@ class grade_grades_raw extends grade_object { $this->scaleid = $this->scale->id; $this->grademin = 0; $this->scale->load_items(); - $this->grademax = count($this->scale->scale_items); + $this->grademax = count($this->scale->scale_items) - 1; } $trackhistory = false; @@ -340,7 +340,7 @@ class grade_grades_raw extends grade_object { if (!empty($this->scaleid)) { $this->load_scale(); $this->scale->load_items(); - $this->grademax = count ($this->scale->scale_items); + $this->grademax = count ($this->scale->scale_items) - 1; $this->grademin = 0; } diff --git a/lib/grade/grade_item.php b/lib/grade/grade_item.php index 3c7f4b9920..d8d4984703 100644 --- a/lib/grade/grade_item.php +++ b/lib/grade/grade_item.php @@ -35,31 +35,31 @@ class grade_item extends grade_object { * @var string $table */ var $table = 'grade_items'; - + /** * Array of class variables that are not part of the DB table fields * @var array $nonfields */ var $nonfields = array('table', 'nonfields', 'calculation', 'grade_grades_raw', 'grade_grades_final', 'scale', 'category', 'outcome'); - + /** * The course this grade_item belongs to. * @var int $courseid */ var $courseid; - + /** * The category this grade_item belongs to (optional). - * @var int $categoryid + * @var int $categoryid */ var $categoryid; - + /** * The grade_category object referenced by $this->categoryid or $this->iteminstance (itemtype must be == 'category' in that case). - * @var object $category + * @var object $category */ var $category; - + /** * A grade_category object this item used to belong to before getting updated. Will be deleted shortly. * @var object $old_parent @@ -71,31 +71,31 @@ class grade_item extends grade_object { * @var string $itemname */ var $itemname; - + /** * e.g. 'mod', 'blocks', 'import', 'calculate' etc... - * @var string $itemtype + * @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 @@ -113,31 +113,31 @@ class grade_item extends grade_object { * @var int $gradetype */ var $gradetype; - + /** * Maximum allowable grade. * @var float $grademax */ var $grademax; - + /** * Minimum allowable grade. * @var float $grademin */ var $grademin; - + /** * 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 @@ -149,43 +149,43 @@ class grade_item extends grade_object { * @var object $outcome */ var $outcome; - + /** * grade required to pass. (grademin < gradepass <= grademax) * @var float $gradepass */ var $gradepass; - + /** * Multiply all grades by this number. * @var float $multfactor */ var $multfactor; - + /** * Add this to all grades. * @var float $plusfactor */ var $plusfactor; - + /** * Sorting order of the columns. * @var int $sortorder */ var $sortorder; - + /** * Date until which to hide this grade_item. If null, 0 or false, grade_item is not hidden. Hiding prevents viewing. * @var int $hidden */ var $hidden; - + /** * Date until which to lock this grade_item. If null, 0 or false, grade_item is not locked. Locking prevents updating. * @var int $locked */ var $locked = false; - + /** * Whether or not the module instance referred to by this grade_item has been deleted. * @var int $deleted @@ -203,7 +203,7 @@ class grade_item extends grade_object { * @var string $calculation */ var $calculation; - + /** * Array of grade_grades_raw objects linked to this grade_item. They are indexed by userid. * @var array $grade_grades_raw @@ -224,7 +224,7 @@ class grade_item extends grade_object { function grade_item($params=NULL, $fetch=true) { $this->grade_object($params, $fetch); } - + /** * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects. */ @@ -250,26 +250,18 @@ class grade_item extends grade_object { if ($this->gradetype == GRADE_TYPE_SCALE and !empty($this->scaleid)) { $this->load_scale(); - if (!method_exists($this->scale, 'load_items')) { - debugging("The scale referenced by this grade_item ($this->scaleid) does not exist in the database. Grademax cannot be infered from the missing scale."); - return false; - } - - $this->scale->load_items(); - $this->grademax = count ($this->scale->scale_items); - $this->grademin = 0; } else { $this->scaleid = NULL; $this->scale = NULL; } - + $qualifies = $this->qualifies_for_update(); $result = parent::update(); - + if ($result && $qualifies) { $category = $this->get_category(); - + if (!empty($category)) { $result = $result && $category->flag_for_update(); } @@ -290,7 +282,7 @@ class grade_item extends grade_object { } $db_item = new grade_item(array('id' => $this->id)); - + $gradetypediff = $db_item->gradetype != $this->gradetype; $grademaxdiff = $db_item->grademax != $this->grademax; $grademindiff = $db_item->grademin != $this->grademin; @@ -320,7 +312,7 @@ class grade_item extends grade_object { * @param string $fields * @return object grade_item object or false if none found. */ - function fetch($field1, $value1, $field2='', $value2='', $field3='', $value3='', $fields="*") { + function fetch($field1, $value1, $field2='', $value2='', $field3='', $value3='', $fields="*") { if ($grade_item = get_record('grade_items', $field1, $value1, $field2, $value2, $field3, $value3, $fields)) { if (isset($this) && get_class($this) == 'grade_item') { foreach ($grade_item as $param => $value) { @@ -332,7 +324,7 @@ class grade_item extends grade_object { $grade_item = new grade_item($grade_item); return $grade_item; } - } else { + } else { return false; } } @@ -351,7 +343,7 @@ class grade_item extends grade_object { } return $result; } - + /** * In addition to perform parent::insert(), this calls the grade_item's category's (if applicable) flag_for_update() method. * @return int ID of the new grade_item record. @@ -359,6 +351,9 @@ class grade_item extends grade_object { function insert() { global $CFG; + // all new grade_items must be recalculated + $this->needsupdate = true; + if (!isset($this->gradetype)) { $this->gradetype = GRADE_TYPE_VALUE; } @@ -370,9 +365,7 @@ class grade_item extends grade_object { // Retrieve scale and infer grademax from it if ($this->gradetype == GRADE_TYPE_SCALE and !empty($this->scaleid)) { $this->load_scale(); - $this->scale->load_items(); - $this->grademax = count ($this->scale->scale_items); - $this->grademin = 0; + } else { $this->scaleid = NULL; $this->scale = NULL; @@ -383,7 +376,7 @@ class grade_item extends grade_object { $this->load_category(); $this->courseid = $this->category->courseid; } - + // If sortorder not given, extrapolate one if (empty($this->sortorder)) { $last_sortorder = get_field_select('grade_items', 'MAX(sortorder)', ''); @@ -399,16 +392,16 @@ class grade_item extends grade_object { if (!empty($this->itemmodule) && !empty($this->iteminstance)) { $this->idnumber = "$this->itemmodule.$this->iteminstance"; } else { // No itemmodule or iteminstance, generate a random idnumber - $this->idnumber = rand(0,9999999999); // TODO replace rand() with proper random generator + $this->idnumber = rand(0,9999999999); // TODO replace rand() with proper random generator } } - + // If a grade_item already exists with these itemtype, itemmodule and iteminstance - // but not itemnumber, generate an itemnumber. + // but not itemnumber, generate an itemnumber. if (empty($this->itemnumber) && !empty($this->itemtype) && !empty($this->itemmodule) && !empty($this->iteminstance)) { - $existing_item = get_record('grade_items', - 'iteminstance', $this->iteminstance, - 'itemmodule', $this->itemmodule, + $existing_item = get_record('grade_items', + 'iteminstance', $this->iteminstance, + 'itemmodule', $this->itemmodule, 'itemtype', $this->itemtype); if (empty($existing_item->itemnumber)) { @@ -437,64 +430,6 @@ class grade_item extends grade_object { return $result; } - /** - * Takes an array of grade_grades_raw objects, indexed by userid, and saves each as a raw grade - * under this grade_item. This replaces any existing grades, after having logged each change in the history table. - * @param array $raw_grades - * @return boolean success or failure - */ - function save_raw($raw_grades, $howmodified='module', $note=NULL) { - if (!empty($raw_grades) && is_array($raw_grades)) { - $this->load_raw(); - - foreach ($raw_grades as $userid => $raw_grade) { - if (!empty($this->grade_grades_raw[$userid])) { - $raw_grade->update($raw_grade->gradevalue, $howmodified, $note); - } else { - $raw_grade->itemid = $this->id; - if ($raw_grade->gradevalue > $raw_grade->grademax) { - die("raw GRADE EXCEEDED grademax FIRST"); - } - $raw_grade->insert(); - } - - $this->grade_grades_raw[$userid] = $raw_grade; - } - } else { - debugging("The data given to grade_item::save_raw($raw_grades) was not valid, it must be an array of raw grades."); - return false; - } - } - - /** - * Once the raw_grades are imported or entered, this method uses the grade_item's calculation and rules to - * generate final grade entries in the DB. - * @return array final grade objects (grade_grades_final). - */ - function generate_final() { - if (empty($this->grade_grades_raw)) { - $this->load_raw(); - } - - $success = true; - - foreach ($this->grade_grades_raw as $raw_grade) { - $final_grade = new grade_grades_final(); - $final_grade->gradevalue = $this->adjust_grade($raw_grade); - $final_grade->itemid = $this->id; - $final_grade->userid = $raw_grade->userid; - - if ($final_grade->gradevalue > $this->grademax) { - debugging("FINAL GRADE EXCEEDED grademax FIRST"); - return false; - } - $success = $success & $final_grade->insert(); - $this->grade_grades_final[$final_grade->userid] = $final_grade; - } - - return $success; - } - /** * 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 @@ -511,16 +446,16 @@ class grade_item extends grade_object { return $final->locked; } } - + /** - * Locks or unlocks this grade_item and (optionally) all its associated final grades. + * Locks or unlocks this grade_item and (optionally) all its associated final grades. * @param boolean $update_final Whether to update final grades too * @param boolean $new_state Optional new state. Will use inverse of current state otherwise. * @return int Number of final grades changed, or false if error occurred during update. */ function toggle_locking($update_final=false, $new_state=NULL) { $this->locked = !$this->locked; - + if (!empty($new_state)) { $this->locked = $new_state; } @@ -529,9 +464,9 @@ class grade_item extends grade_object { debugging("Could not update this grade_item's locked state in the database."); return false; } - + $count = 0; - + if ($update_final) { $this->load_final(); foreach ($this->grade_grades_final as $id => $final) { @@ -549,14 +484,14 @@ class grade_item extends grade_object { } /** - * Locks or unlocks this grade_item and (optionally) all its associated final grades. + * Locks or unlocks this grade_item and (optionally) all its associated final grades. * @param boolean $update_final Whether to update final grades too * @param boolean $new_state Optional new state. Will use inverse of current state otherwise. * @return int Number of final grades changed, or false if error occurred during update. */ function toggle_hiding($update_final=false, $new_state=NULL) { $this->hidden = !$this->hidden; - + if (!empty($new_state)) { $this->hidden = $new_state; } @@ -565,9 +500,9 @@ class grade_item extends grade_object { debugging("Could not update this grade_item's hidden state in the database."); return false; } - + $count = 0; - + if ($update_final) { $this->load_final(); foreach ($this->grade_grades_final as $id => $final) { @@ -583,66 +518,87 @@ class grade_item extends grade_object { return $count; } - - + + /** * Performs the necessary calculations on the grades_final referenced by this grade_item, - * and stores the results in grade_grades_final. Performs this for only one userid if - * requested. Also resets the needs_update flag once successfully performed. + * and stores the results in grade_grades_final. Also resets the needsupdate flag + * once successfully performed. * - * @param int $userid - * @return int Number of grades updated, or false if error + * This function must be use ONLY from lib/gradeslib.php/grade_update_final_grades(), + * because the calculation must be done in correct order!! + * + * @return boolean true if ok, array of errors otherwise */ - function update_final_grade($userid=NULL) { - if (empty($this->grade_grades_final)) { - $this->load_final(); - } - if (empty($this->grade_grades_raw)) { - $this->load_raw(); - } - - $count = 0; - - $grade_final_array = array(); - $grade_raw_array = array(); + function update_final_grade() { + global $CFG; - if (!empty($userid)) { - $grade_final_array[$userid] = $this->grade_grades_final[$userid]; - $grade_raw_array[$userid] = $this->grade_grades_raw[$userid]; - } else { - $grade_final_array = $this->grade_grades_final; - $grade_raw_array = $this->grade_grades_raw; - } + $errors = array(); - // The following code assumes that there is a grade_final object in DB for every - // grade_raw object. This assumption depends on the correct creation of grade_final entries. - // This also assumes that the two arrays $this->grade_grades_raw and its final counterpart are - // indexed by userid, not sequentially or by grade_id - if (count($this->grade_grades_final) != count($this->grade_grades_raw)) { - $this->generate_final(); - } + if ($this->get_calculation()) { + // this is calculated grade + $this->upgrade_calculation_to_object(); + if ($this->calculation->compute()) { + $this->needsupdate = false; + $this->update(); + return true; + } else { + $errors[] = "Could not calculate grades for grade item id:".$this->id; // TODO: improve and localize + } - foreach ($grade_raw_array as $userid => $raw) { - $newgradevalue = $raw->gradevalue; - - if (!empty($this->calculation)) { - $this->upgrade_calculation_to_object(); - $newgradevalue = $this->calculation->compute($raw->gradevalue); + } else if ($this->itemtype == 'category') { + // aggregate category grade item + $category = $this->get_category(); + if (!$category->generate_grades()) { + $errors[] = "Could not calculate category grade item id:".$this->id; // TODO: improve and localize } - - $final = $this->grade_grades_final[$userid]; + } - $final->gradevalue = $this->adjust_grade($raw, $newgradevalue); - - if ($final->update()) { - $count++; - } else { - debugging("Could not update a final grade in this grade_item."); - return false; + $sql = "SELECT r.*, f.userid AS fuserid, f.id AS fid, f.itemid AS fitemid, f.gradevalue AS fgradevalue + FROM {$CFG->prefix}grade_grades_raw r + LEFT JOIN {$CFG->prefix}grade_grades_final f ON f.itemid=r.itemid AND f.userid=r.userid + WHERE r.itemid={$this->id}"; + + if ($rs = get_recordset_sql($sql)) { + if ($rs->RecordCount() > 0) { + while ($grade = rs_fetch_next_record($rs)) { + if (!empty($errors) or is_null($grade->gradevalue)) { + // unset existing final grade when no raw present or error + $final = grade_grades_final::fetch('id', $grade->fid); + $final->gradevalue = NULL; + $final->update(); + continue; + } + + $finalvalue = $this->adjust_grade($grade); + + if (is_null($grade->fid)) { + // final grade does not exist yet + $final = new grade_grades_final(array('itemid'=>$grade->itemid, 'userid'=>$grade->userid, 'gradevalue'=>$finalvalue, false)); + $final->insert(); + + } else if ($finalvalue != $grade->fgradevalue) { + // let's update the final grade + $final = grade_grades_final::fetch('id', $grade->fid); + $final->gradevalue = $finalvalue; + $final->update(); + } + } } } - return $count; + //TODO: set grade to null for final grades that do not have corresponding raw grade + // using left join SQL update only + + if (!empty($errors)) { + $this->flag_for_update(); + return $errors; + + } else { + $this->needsupdate = false; + $this->update(); + return true; + } } /** @@ -656,58 +612,70 @@ class grade_item extends grade_object { } /** - * Given a float grade value or integer grade scale, applies a number of adjustment based on + * 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 $grade_raw The raw object to compare with this grade_item's rules - * @param mixed $gradevalue The new gradevalue (after calculations are performed). - * If null, the raw_grade's gradevalue will be used. - * @return mixed - */ - function adjust_grade($grade_raw, $gradevalue=NULL) { - $raw_offset = 0; - $item_offset = 0; - + * @param object $rawgrade The raw grade. + * @return mixed + */ + function adjust_grade($rawgrade) { + $gradevalue = $rawgrade->gradevalue; + if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade - if (empty($gradevalue)) { - $gradevalue = $grade_raw->gradevalue; + + if ($this->grademax < $this->grademin) { + return null; } - } elseif($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value - if (empty($gradevalue)) { - $gradevalue = $grade_raw->gradevalue; + if ($this->grademax == $this->grademin) { + return $this->grademax; // no range } - - // In case the scale objects haven't been loaded, do it now - if (empty($grade_raw->scale)) { - $grade_raw->load_scale(); + + // Standardise score to the new grade range + // NOTE: this is not compatible with current assignment grading + if ($rawgrade->grademin != $this->grademin or $rawgrade->grademax != $this->grademax) { + $gradevalue = standardise_score($gradevalue, $rawgrade->grademin, $rawgrade->grademax, $this->grademin, $this->grademax); } - + + // Apply other grade_item factors + $gradevalue *= $this->multfactor; + $gradevalue += $this->plusfactor; + + return bounded_number($this->grademin, $gradevalue, $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 ($rawgrade->grademax != $this->grademax and $rawgrade->grademax > 0) { + $gradevalue = standardise_score($gradevalue, $rawgrade->grademin, $rawgrade->grademax, $this->grademin, $this->grademax); + } + + return (int)bounded_number(0, round($gradevalue), $this->grademax); - $grade_raw->grademax = count($grade_raw->scale->scale_items) - 1; - $this->grademax = count($this->scale->scale_items) - 1; - $grade_raw->grademin = 0; - $this->grademin = 0; - } elseif ($this->gradetype != GRADE_TYPE_TEXT) { // Something's wrong, the raw grade has no value!? - return "Error: The gradeitem did not have a valid gradetype value, was $this->gradetype instead"; + } 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;; } - - // Standardise score to the new grade range - $gradevalue = standardise_score($gradevalue, $grade_raw->grademin, - $grade_raw->grademax, $this->grademin, $this->grademax); - // Apply factors, depending on whether it's a scale or value - if ($this->gradetype == GRADE_TYPE_VALUE) { - // Apply other grade_item factors - $gradevalue *= $this->multfactor; - $gradevalue += $this->plusfactor; - } - return $gradevalue; - } - + + } + /** * Sets this grade_item's needsupdate to true. Also looks at parent category, if any, and calls * its flag_for_update() method. @@ -738,42 +706,43 @@ class grade_item extends grade_object { $this->old_parent = $this->get_category(); $this->category = null; $this->categoryid = null; - return $this->update(); + return $this->update(); } /** - * Instantiates a grade_scale object whose data is retrieved from the DB, + * Instantiates a grade_scale object whose data is retrieved from the DB, * if this item's scaleid variable is set. * @return object grade_scale */ function load_scale() { if (!empty($this->scaleid)) { $this->scale = grade_scale::fetch('id', $this->scaleid); - if (method_exists($this->scale, 'load_items')) { - $this->scale->load_items(); - } else { - $this->scale = null; - } - } + $this->scale->load_items(); + $this->grademax = count($this->scale->scale_items) - 1; + $this->grademin = 0; + } else { + $this->scale = null; + } + return $this->scale; } /** - * Instantiates a grade_outcome object whose data is retrieved from the DB, + * 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('id', $this->outcomeid); - } + } return $this->outcome; } - + /** * Loads all the grade_grades_raw objects for this grade_item from the DB into grade_item::$grade_grades_raw array. * @return array grade_grades_raw objects - */ + */ function load_raw() { $grade_raw_array = get_records('grade_grades_raw', 'itemid', $this->id); @@ -789,47 +758,24 @@ class grade_item extends grade_object { /** * Loads all the grade_grades_final objects for this grade_item from the DB into grade_item::$grade_grades_final array. - * @param boolean $generatefakenullgrades If set to true, AND $CFG->usenullgrades is true, will replace missing grades with grades, gradevalue=grademin * @return array grade_grades_final objects - */ - function load_final($generatefakenullgrades=false) { + */ + function load_final() { global $CFG; + $this->grade_grades_final = array(); + $grade_final_array = get_records('grade_grades_final', 'itemid', $this->id); - - if (empty($grade_final_array)) { - $this->generate_final(); - $grade_final_array = get_records('grade_grades_final', 'itemid', $this->id); - } - + if (empty($grade_final_array)) { - debugging("No final grades recorded for this grade_item"); - return false; + return array(); } foreach ($grade_final_array as $f) { $this->grade_grades_final[$f->userid] = new grade_grades_final($f); } - $returnarray = fullclone($this->grade_grades_final); - - // If we are generating fake null grades, we have to get a list of users - if ($generatefakenullgrades && $CFG->usenullgrades) { - $users = get_records_sql_menu('SELECT userid AS "user", userid FROM ' . $CFG->prefix . 'grade_grades_final GROUP BY userid ORDER BY userid'); - if (!empty($users) && is_array($users)) { - foreach ($users as $userid) { - if (!isset($returnarray[$userid])) { - $fakefinal = new grade_grades_final(); - $fakefinal->itemid = $this->id; - $fakefinal->userid = $userid; - $fakefinal->gradevalue = $this->grademin; - $returnarray[$userid] = $fakefinal; - } - } - } - } - - return $returnarray; + return fullclone($this->grade_grades_final); } /** @@ -839,8 +785,8 @@ class grade_item extends grade_object { function get_standardised_final() { $standardised_finals = array(); - $final_grades = $this->load_final(true); - + $final_grades = $this->load_final(); + if (!empty($final_grades)) { foreach ($final_grades as $userid => $final) { $standardised_finals[$userid] = standardise_score($final->gradevalue, $this->grademin, $this->grademax, 0, 1); @@ -852,23 +798,23 @@ class grade_item extends grade_object { /** * Returns the grade_category object this grade_item belongs to (if any). - * This category object may be the parent (referenced by categoryid) or the associated category + * This category object may be the parent (referenced by categoryid) or the associated category * (referenced by iteminstance). - * + * * @return mixed grade_category object if applicable, NULL otherwise */ function get_category() { $category = null; - + if (!empty($this->categoryid)) { $category = grade_category::fetch('id', $this->categoryid); } elseif (!empty($this->iteminstance) && $this->itemtype == 'category') { $category = grade_category::fetch('id', $this->iteminstance); } - + return $category; } - + /** * Calls upon the get_category method to retrieve the grade_category object * from the DB and assigns it to $this->category. It also returns the object. @@ -882,50 +828,46 @@ class grade_item extends grade_object { /** * Returns this object's calculation. * @param boolean $fetch Whether to fetch the value from the DB or not (false == just use the object's value) - * @return mixed $calculation A string if found, false otherwise. + * @return mixed $calculation Object if found, false otherwise. */ function get_calculation($fetch = false) { - if (!$fetch && get_class($this->calculation) == 'grade_calculation') { - return $this->calculation; - } - $grade_calculation = grade_calculation::fetch('itemid', $this->id); - - if (empty($grade_calculation)) { // There is no calculation in DB - return false; - } elseif (empty($this->calculation) || !is_object($this->calculation)) { // The calculation isn't yet loaded - $this->calculation = $grade_calculation; - return $grade_calculation; - } elseif ($grade_calculation->calculation != $this->calculation->calculation) { // The object's calculation is not in sync with the DB (new value??) - $this->calculation = $grade_calculation; - return $grade_calculation; - } else { // The object's calculation is already in sync with the database - return $this->calculation; + if (is_null($this->calculation)) { + $fetch = true; } + + if ($fetch) { + $this->calculation = grade_calculation::fetch('itemid', $this->id); + } + + return $this->calculation; } /** * 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 method will attempt to retrieve one from the Database, based on - * the variables set in the current object. - * @param string $calculation + * the calculation is removed. + * @param string $formula * @return boolean */ - function set_calculation($calculation = null) { - if (empty($calculation)) { // We are setting this item object's calculation variable from the DB - $grade_calculation = $this->get_calculation(true); - if (empty($grade_calculation)) { - debugging("No calculation to set for this grade_item."); - return false; - } else { - $this->calculation = $grade_calculation; + function set_calculation($formula) { + // remove cached calculation object + $this->calculation = null; + + if (empty($formula)) { // We are removing this calculation + if (!empty($this->id)) { + if ($grade_calculation = $this->get_calculation()) { + $grade_calculation->delete(); + } } + $this->calculation = null; + $status = true; + } else { // We are updating or creating the calculation entry in the DB $grade_calculation = $this->get_calculation(); - + if (empty($grade_calculation)) { // Creating $grade_calculation = new grade_calculation(); - $grade_calculation->calculation = $calculation; + $grade_calculation->calculation = $formula; $grade_calculation->itemid = $this->id; if ($grade_calculation->insert()) { @@ -934,16 +876,19 @@ class grade_item extends grade_object { } else { debugging("Could not save the calculation in the database, for this grade_item."); return false; - } + } } else { // Updating - $grade_calculation->calculation = $calculation; + $grade_calculation->calculation = $formula; $grade_calculation = new grade_calculation($grade_calculation); $this->calculation = $grade_calculation; - return $grade_calculation->update(); + $status = $grade_calculation->update(); } } + + $this->flag_for_update(); + return $status; } - + /** * Returns the raw values for this grade item (as imported by module or other source). * @param int $userid Optional: to retrieve a single raw grade @@ -963,7 +908,7 @@ class grade_item extends grade_object { } return $grade_raw_array; } - + /** * 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 @@ -974,19 +919,21 @@ class grade_item extends grade_object { $this->load_final(); } - $grade_final_array = null; - if (!empty($userid)) { - $f = get_record('grade_grades_final', 'itemid', $this->id, 'userid', $userid); - $grade_final_array[$f->userid] = new grade_grades_final($f); + if (empty($userid)) { + return $this->grade_grades_final; + } else { - $grade_final_array = $this->grade_grades_final; + if (array_key_exists($userid, $this->grade_grades_final)) { + return $this->grade_grades_final[$userid]; + } else { + return new grade_grades_final(array('itemid'=>$this->itemid, 'gradevalue'=>NULL, 'userid'=>$userid)); + } } - return $grade_final_array; } /** - * Returns the sortorder of this grade_item. This method is also available in - * grade_category, for cases where the object type is not know. It will act as a virtual + * Returns the sortorder of this grade_item. This method is also available in + * grade_category, for cases where the object type is not know. It will act as a virtual * variable for a grade_category. * @return int Sort order */ @@ -995,8 +942,8 @@ class grade_item extends grade_object { } /** - * Sets the sortorder of this grade_item. This method is also available in - * grade_category, for cases where the object type is not know. It will act as a virtual + * Sets the sortorder of this grade_item. This method is also available in + * grade_category, for cases where the object type is not know. It will act as a virtual * variable for a grade_category. * @param int $sortorder * @return void @@ -1006,14 +953,14 @@ class grade_item extends grade_object { } /** - * Returns the most descriptive field for this object. This is a standard method used + * 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() { return $this->itemname; - } - + } + /** * Returns this grade_item's id. This is specified for cases where we do not * know an object's type, and want to get either an item's id or a category's item's id. @@ -1039,10 +986,10 @@ class grade_item extends grade_object { function set_parent_id($parentid) { $this->categoryid = $parentid; } - + /** - * Returns the locked state/date of this grade_item. This method is also available in - * grade_category, for cases where the object type is not known. + * Returns the locked state/date of this grade_item. This method is also available in + * grade_category, for cases where the object type is not known. * @return int 0, 1 or timestamp int(10) */ function get_locked() { @@ -1058,10 +1005,10 @@ class grade_item extends grade_object { $this->locked = $locked; return $this->update(); } - + /** - * Returns the hidden state/date of this grade_item. This method is also available in - * grade_category, for cases where the object type is not known. + * Returns the hidden state/date of this grade_item. This method is also available in + * grade_category, for cases where the object type is not known. * @return int 0, 1 or timestamp int(10) */ function get_hidden() { @@ -1069,16 +1016,16 @@ class grade_item extends grade_object { } /** - * Sets the grade_item's hidden variable and updates the grade_item. + * Sets the grade_item's hidden variable and updates the grade_item. * @param int $hidden 0, 1 or a timestamp int(10) after which date the item will be hidden. * @return void */ function set_hidden($hidden) { $this->hidden = $hidden; - return $this->update(); + return $this->update(); } - /** + /** * If the old parent is set (after an update), this checks and returns whether it has any children. Important for * deleting childless categories. * @return boolean @@ -1089,6 +1036,45 @@ class grade_item extends grade_object { } else { return false; } - } + } + + /** + * Finds out on which other items does this depend directly when doing calculation or category agregation + * @return array of grade_item ids this one depends on + */ + function dependson() { + + if ($this->get_calculation()) { + $this->upgrade_calculation_to_object(); + return $this->calculation->dependson(); + + } else if ($this->itemtype == 'category') { + $grade_category = grade_category::fetch('id', $this->iteminstance); + $children = $grade_category->get_children(1, 'flat'); + + if (empty($children)) { + return array(); + } + + $result = array(); + + $childrentype = get_class(reset($children)); + if ($childrentype == 'grade_category') { + foreach ($children as $id => $category) { + $grade_item = $category->get_grade_item(); + $result[] = $grade_item->id; + } + } elseif ($childrentype == 'grade_item') { + foreach ($children as $id => $grade_item) { + $result[] = $grade_item->id; + } + } + + return $result; + + } else { + return array(); + } + } } ?> diff --git a/lib/gradelib.php b/lib/gradelib.php index f91bd6c6d0..ecbeea04c4 100644 --- a/lib/gradelib.php +++ b/lib/gradelib.php @@ -32,10 +32,11 @@ * @package moodlecore */ -define('GRADE_AGGREGATE_MEAN', 0); +define('GRADE_AGGREGATE_MEAN_ALL', 0); define('GRADE_AGGREGATE_MEDIAN', 1); -define('GRADE_AGGREGATE_SUM', 2); -define('GRADE_AGGREGATE_MODE', 3); +define('GRADE_AGGREGATE_MEAN_GRADED', 2); +define('GRADE_AGGREGATE_MIN', 3); +define('GRADE_AGGREGATE_MAX', 4); define('GRADE_CHILDTYPE_ITEM', 0); define('GRADE_CHILDTYPE_CAT', 1); @@ -338,6 +339,8 @@ function grade_get_items($courseid, $itemtype=NULL, $itemmodule=NULL, $iteminsta * @param string $aggregation * @return mixed New grade_category id if successful */ +/* +// TODO: this should be obsoleted by grade_update() or removed completely - modules must not use any IDs or grade_item objects directly! function grade_create_category($courseid, $fullname, $items, $aggregation=GRADE_AGGREGATE_MEAN) { $grade_category = new grade_category(compact('courseid', 'fullname', 'items', 'aggregation')); @@ -347,34 +350,133 @@ function grade_create_category($courseid, $fullname, $items, $aggregation=GRADE_ return $grade_category->id; } } +*/ /** - * Updates all grade_grades_final for each grade_item matching the given attributes. - * The search is further restricted, so that only grade_items that have needs_update == TRUE - * or that use calculation are retrieved. + * Updates all grade_grades_final in course. * * @param int $courseid - * @param int $gradeitemid - * @return int Number of grade_items updated + * @return boolean true if ok, array of errors if problems found */ -function grade_update_final_grades($courseid=NULL, $gradeitemid=NULL) { +function grade_update_final_grades($courseid) { + $errors = array(); $grade_item = new grade_item(); $grade_item->courseid = $courseid; - $grade_item->id = $gradeitemid; - $grade_items = $grade_item->fetch_all_using_this(); + if (!$grade_items = $grade_item->fetch_all_using_this()) { + return true; + } - $count = 0; + $needsupdate = false; + $calculated = false; + foreach ($grade_items as $gid=>$gitem) { + $grade_item =& $grade_items[$gid]; + if ($grade_item->needsupdate) { + $needsupdate = true; + } + if ($grade_item->get_calculation()) { + $calculated = true; + } + } - foreach ($grade_items as $gi) { - $calculation = $gi->get_calculation(); - if (!empty($calculation) || $gi->needsupdate) { - if ($gi->update_final_grade()) { - $count++; + // no update needed + if (!$needsupdate) { + return true; + } + + // the easy way + if (!$calculated) { + foreach ($grade_items as $gid=>$gitem) { + $grade_item =& $grade_items[$gid]; + if ($grade_item->needsupdate) { + $result = $grade_item->update_final_grade(); + if ($result !== true) { + $errors = array_merge($errors, $result); + } } } + + if (count($errors) == 0) { + return true; + } else { + return $errors; + } } - return $count; + // now the hard way with calculated grade_items or categories + $finalitems = array(); + $finalids = array(); + while (count($grade_items) > 0) { + $count = 0; + foreach ($grade_items as $gid=>$gitem) { + $grade_item =& $grade_items[$gid]; + if (!$grade_item->needsupdate and $grade_item->itemtype!='category' and !$grade_item->get_calculation()) { + $finalitems[$gid] = $grade_item; + $finalids[] = $gid; + unset($grade_items[$gid]); + continue; + } + + $dependson = $grade_item->dependson(); + + //are we dealing with category with no calculated items? + // we can not trust the needsupdate flag because category might contain calculated items + if ($grade_item->itemtype=='category' and !$grade_item->needsupdate) { + $forceupdate = false; + foreach ($dependson as $childid) { + if (in_array($childid, $finalids)) { + $child = $finalitems[$childid]; + } else { + $child = $grade_items[$childid]; + } + if ($child->itemtype == 'category' or $child->get_calculation()) { + $forceupdate = true; + } + } + + if ($forceupdate) { + $grade_item->flag_for_update(); + } else { + $finalitems[$gid] = $grade_item; + $finalids[] = $gid; + unset($grade_items[$gid]); + continue; + } + } + + //do we have all data for this item? + $doupdate = true; + foreach ($dependson as $did) { + if (!in_array($did, $finalids)) { + $doupdate = false; + } + } + + //oki - let's update, calculate or aggregate :-) + if ($doupdate) { + $result = $grade_item->update_final_grade(); + if ($result !== true) { + $errors = array_merge($errors, $result); + } else { + $finalitems[$gid] = $grade_item; + $finalids[] = $gid; + unset($grade_items[$gid]); + } + } + } + + if ($count == 0) { + foreach($grade_items as $grade_item) { + $errors[] = 'Probably circular reference in grade_item id:'.$grade_item->id; // TODO: localize + } + break; + } + } + + if (count($errors) == 0) { + return true; + } else { + return $errors; + } } /** @@ -596,5 +698,61 @@ function standardise_score($gradevalue, $source_min, $source_max, $target_min, $ return $standardised_value; } +/** + * This function is used to migrade old date and settings from old gradebook into new grading system. + * + * TODO: + * - category weight not used - we would have to create extra top course grade calculated category + * - exta_credit item flag not used - does not fit all our aggregation types, could be used in SUM only + */ +function grade_oldgradebook_upgrade($courseid) { + global $CFG; + + $categories = array(); + if ($oldcats = get_records('grade_category', 'courseid', $courseid)) { + foreach ($oldcats as $oldcat) { + $newcat = new grade_category(array('courseid'=>$courseid, 'fullname'=>$oldcat->name)); + $newcat->droplow = $oldcat->drop_x_lowest; + $newcat->aggregation = GRADE_AGGREGATE_MEAN_GRADED; + + if (empty($newcat->id)) { + $newcat->insert(); + } else { + $newcat->update(); + } + + $categories[$oldcat->id] = $newcat; + + $catitem = $newcat->get_grade_item(); + $catitem->gradetype = GRADE_TYPE_VALUE; + $catitem->plusfactor = $oldcat->bonus_points; + $catitem->hidden = $oldcat->hidden; + $catitem->update(); + } + } + + // get all grade items with mod details + $sql = "SELECT gi.*, cm.idnumber as cmidnumber, m.name as modname + FROM {$CFG->prefix}grade_item gi, {$CFG->prefix}course_modules cm, {$CFG->prefix}modules m + WHERE gi.courseid=$courseid AND m.id=gi.modid AND cm.instance=gi.cminstance + ORDER BY gi.sortorder ASC"; + + if ($olditems = get_records_sql($sql)) { + foreach ($olditems as $olditem) { + $newitem = new grade_item(array('courseid'=>$olditem->courseid, 'itemtype'=>'mod', 'itemmodule'=>$olditem->modname, 'iteminstance'=>$olditem->cminstance, 'itemnumber'=>0)); + if (!empty($olditem->category)) { + // we do this low level stuff to get some speedup during upgrade + $newitem->set_parent_id($categories[$olditem->category]->id); + } + $newitem->gradetype = GRADE_TYPE_NONE; + $newitem->multfactor = $olditem->scale_grade; + if (empty($newitem->id)) { + $newitem->insert(); + } else { + $newitem->update(); + } + } + } +} ?> diff --git a/lib/simpletest/fixtures/gradetest.php b/lib/simpletest/fixtures/gradetest.php index edb76cfd58..e9dd1a4fe6 100644 --- a/lib/simpletest/fixtures/gradetest.php +++ b/lib/simpletest/fixtures/gradetest.php @@ -357,9 +357,11 @@ class grade_test extends UnitTestCase { $scale->scale = 'Way off topic, Not very helpful, Fairly neutral, Fairly helpful, Supportive, Some good information, Perfect answer!'; $scale->description = 'This scale defines some of qualities that make posts helpful within the Moodle help forums.\n Your feedback will help others see how their posts are being received.'; $scale->timemodified = mktime(); + $temp = explode(',', $scale->scale); + $scale->max = count($temp) -1; if ($scale->id = insert_record('scale', $scale)) { - $this->scale[] = $scale; + $this->scale[0] = $scale; } $scale = new stdClass(); @@ -370,9 +372,11 @@ class grade_test extends UnitTestCase { $scale->scale = 'Distinction, Very Good, Good, Pass, Fail'; $scale->description = 'This scale is used to mark standard assignments.'; $scale->timemodified = mktime(); + $temp = explode(',', $scale->scale); + $scale->max = count($temp) -1; if ($scale->id = insert_record('scale', $scale)) { - $this->scale[] = $scale; + $this->scale[1] = $scale; } $scale = new stdClass(); @@ -383,9 +387,11 @@ class grade_test extends UnitTestCase { $scale->scale = 'Loner, Contentious, Disinterested, Participative, Follower, Leader'; $scale->description = 'Describes the level of teamwork of a student.'; $scale->timemodified = mktime(); + $temp = explode(',', $scale->scale); + $scale->max = count($temp) -1; if ($scale->id = insert_record('scale', $scale)) { - $this->scale[] = $scale; + $this->scale[2] = $scale; } $scale->name = 'unittestscale4'; @@ -394,9 +400,11 @@ class grade_test extends UnitTestCase { $scale->scale = 'Does not understand theory, Understands theory but fails practice, Manages through, Excels'; $scale->description = 'Level of expertise at a technical task, with a theoretical framework.'; $scale->timemodified = mktime(); + $temp = explode(',', $scale->scale); + $scale->max = count($temp) -1; if ($scale->id = insert_record('scale', $scale)) { - $this->scale[] = $scale; + $this->scale[3] = $scale; } $scale->name = 'unittestscale5'; @@ -405,9 +413,11 @@ class grade_test extends UnitTestCase { $scale->scale = 'Insufficient, Acceptable, Excellent.'; $scale->description = 'Description of skills.'; $scale->timemodified = mktime(); + $temp = explode(',', $scale->scale); + $scale->max = count($temp) -1; if ($scale->id = insert_record('scale', $scale)) { - $this->scale[] = $scale; + $this->scale[4] = $scale; } } @@ -419,7 +429,7 @@ class grade_test extends UnitTestCase { $grade_category->fullname = 'unittestcategory1'; $grade_category->courseid = $this->courseid; - $grade_category->aggregation = GRADE_AGGREGATE_MEAN; + $grade_category->aggregation = GRADE_AGGREGATE_MEAN_GRADED; $grade_category->keephigh = 100; $grade_category->droplow = 0; $grade_category->hidden = 0; @@ -428,14 +438,14 @@ class grade_test extends UnitTestCase { $grade_category->depth = 1; if ($grade_category->id = insert_record('grade_categories', $grade_category)) { - $this->grade_categories[] = $grade_category; + $this->grade_categories[0] = $grade_category; } $grade_category = new stdClass(); $grade_category->fullname = 'unittestcategory2'; $grade_category->courseid = $this->courseid; - $grade_category->aggregation = GRADE_AGGREGATE_MEAN; + $grade_category->aggregation = GRADE_AGGREGATE_MEAN_GRADED; $grade_category->keephigh = 100; $grade_category->droplow = 0; $grade_category->hidden = 0; @@ -445,14 +455,14 @@ class grade_test extends UnitTestCase { $grade_category->depth = 2; if ($grade_category->id = insert_record('grade_categories', $grade_category)) { - $this->grade_categories[] = $grade_category; + $this->grade_categories[1] = $grade_category; } $grade_category = new stdClass(); $grade_category->fullname = 'unittestcategory3'; $grade_category->courseid = $this->courseid; - $grade_category->aggregation = GRADE_AGGREGATE_MEAN; + $grade_category->aggregation = GRADE_AGGREGATE_MEAN_GRADED; $grade_category->keephigh = 100; $grade_category->droplow = 0; $grade_category->hidden = 0; @@ -462,7 +472,7 @@ class grade_test extends UnitTestCase { $grade_category->depth = 2; if ($grade_category->id = insert_record('grade_categories', $grade_category)) { - $this->grade_categories[] = $grade_category; + $this->grade_categories[2] = $grade_category; } // A category with no parent, but grade_items as children @@ -471,7 +481,7 @@ class grade_test extends UnitTestCase { $grade_category->fullname = 'level1category'; $grade_category->courseid = $this->courseid; - $grade_category->aggregation = GRADE_AGGREGATE_MEAN; + $grade_category->aggregation = GRADE_AGGREGATE_MEAN_GRADED; $grade_category->keephigh = 100; $grade_category->droplow = 0; $grade_category->hidden = 0; @@ -480,7 +490,7 @@ class grade_test extends UnitTestCase { $grade_category->depth = 1; if ($grade_category->id = insert_record('grade_categories', $grade_category)) { - $this->grade_categories[] = $grade_category; + $this->grade_categories[3] = $grade_category; } } @@ -499,7 +509,7 @@ class grade_test extends UnitTestCase { $grade_item->iteminstance = 1; $grade_item->gradetype = GRADE_TYPE_VALUE; $grade_item->grademin = 30; - $grade_item->grademax = 140; + $grade_item->grademax = 110; $grade_item->itemnumber = 1; $grade_item->iteminfo = 'Grade item used for unit testing'; $grade_item->timecreated = mktime(); @@ -507,7 +517,7 @@ class grade_test extends UnitTestCase { $grade_item->sortorder = 3; if ($grade_item->id = insert_record('grade_items', $grade_item)) { - $this->grade_items[] = $grade_item; + $this->grade_items[0] = $grade_item; } // id = 1 @@ -530,7 +540,7 @@ class grade_test extends UnitTestCase { $grade_item->sortorder = 4; if ($grade_item->id = insert_record('grade_items', $grade_item)) { - $this->grade_items[] = $grade_item; + $this->grade_items[1] = $grade_item; } // id = 2 @@ -545,14 +555,14 @@ class grade_test extends UnitTestCase { $grade_item->gradetype = GRADE_TYPE_SCALE; $grade_item->scaleid = $this->scale[0]->id; $grade_item->grademin = 0; - $grade_item->grademax = 7; + $grade_item->grademax = $this->scale[0]->max; $grade_item->iteminfo = 'Grade item used for unit testing'; $grade_item->timecreated = mktime(); $grade_item->timemodified = mktime(); $grade_item->sortorder = 6; if ($grade_item->id = insert_record('grade_items', $grade_item)) { - $this->grade_items[] = $grade_item; + $this->grade_items[2] = $grade_item; } // Load grade_items associated with the 3 categories @@ -573,7 +583,7 @@ class grade_test extends UnitTestCase { $grade_item->sortorder = 1; if ($grade_item->id = insert_record('grade_items', $grade_item)) { - $this->grade_items[] = $grade_item; + $this->grade_items[3] = $grade_item; } // id = 4 @@ -593,7 +603,7 @@ class grade_test extends UnitTestCase { $grade_item->sortorder = 2; if ($grade_item->id = insert_record('grade_items', $grade_item)) { - $this->grade_items[] = $grade_item; + $this->grade_items[4] = $grade_item; } // id = 5 @@ -613,7 +623,7 @@ class grade_test extends UnitTestCase { $grade_item->sortorder = 5; if ($grade_item->id = insert_record('grade_items', $grade_item)) { - $this->grade_items[] = $grade_item; + $this->grade_items[5] = $grade_item; } // Orphan grade_item @@ -634,7 +644,7 @@ class grade_test extends UnitTestCase { $grade_item->sortorder = 7; if ($grade_item->id = insert_record('grade_items', $grade_item)) { - $this->grade_items[] = $grade_item; + $this->grade_items[6] = $grade_item; } // 2 grade items under level1category @@ -650,14 +660,14 @@ class grade_test extends UnitTestCase { $grade_item->gradetype = GRADE_TYPE_SCALE; $grade_item->scaleid = $this->scale[0]->id; $grade_item->grademin = 0; - $grade_item->grademax = 7; + $grade_item->grademax = $this->scale[0]->max; $grade_item->iteminfo = 'Grade item used for unit testing'; $grade_item->timecreated = mktime(); $grade_item->timemodified = mktime(); $grade_item->sortorder = 9; if ($grade_item->id = insert_record('grade_items', $grade_item)) { - $this->grade_items[] = $grade_item; + $this->grade_items[7] = $grade_item; } // id = 8 @@ -678,7 +688,7 @@ class grade_test extends UnitTestCase { $grade_item->sortorder = 10; if ($grade_item->id = insert_record('grade_items', $grade_item)) { - $this->grade_items[] = $grade_item; + $this->grade_items[8] = $grade_item; } // Grade_item for level1category @@ -692,15 +702,15 @@ class grade_test extends UnitTestCase { $grade_item->iteminstance = $this->grade_categories[3]->id; $grade_item->needsupdate = true; $grade_item->gradetype = GRADE_TYPE_VALUE; - $grade_item->grademin = 10; - $grade_item->grademax = 120; + $grade_item->grademin = 0; + $grade_item->grademax = 100; $grade_item->iteminfo = 'Orphan Grade item used for unit testing'; $grade_item->timecreated = mktime(); $grade_item->timemodified = mktime(); $grade_item->sortorder = 8; if ($grade_item->id = insert_record('grade_items', $grade_item)) { - $this->grade_items[] = $grade_item; + $this->grade_items[9] = $grade_item; } } @@ -709,41 +719,17 @@ class grade_test extends UnitTestCase { * Load grade_calculation data into the database, and adds the corresponding objects to this class' variable. */ function load_grade_calculations() { - // Calculation for grade_item 1 - $grade_calculation = new stdClass(); - $grade_calculation->itemid = $this->grade_items[0]->id; - $grade_calculation->calculation = '[unittestgradeitem1] * 1.4 - 3'; - $grade_calculation->timecreated = mktime(); - $grade_calculation->timemodified = mktime(); - - if ($grade_calculation->id = insert_record('grade_calculations', $grade_calculation)) { - $this->grade_calculations[] = $grade_calculation; - $this->grade_items[0]->calculation = $grade_calculation; - } - // Calculation for grade_item 2 $grade_calculation = new stdClass(); $grade_calculation->itemid = $this->grade_items[1]->id; - $grade_calculation->calculation = '[unittestgradeitem2] + 3'; + $grade_calculation->calculation = '= gi'.$this->grade_items[0]->id.'* + 30 '; $grade_calculation->timecreated = mktime(); $grade_calculation->timemodified = mktime(); if ($grade_calculation->id = insert_record('grade_calculations', $grade_calculation)) { - $this->grade_calculations[] = $grade_calculation; + $this->grade_calculations[0] = $grade_calculation; $this->grade_items[1]->calculation = $grade_calculation; } - - // Calculation for grade_item 3 - $grade_calculation = new stdClass(); - $grade_calculation->itemid = $this->grade_items[2]->id; - $grade_calculation->calculation = '[unittestgradeitem3] / 2 + 40'; - $grade_calculation->timecreated = mktime(); - $grade_calculation->timemodified = mktime(); - - if ($grade_calculation->id = insert_record('grade_calculations', $grade_calculation)) { - $this->grade_calculations[] = $grade_calculation; - $this->grade_items[2]->calculation = $grade_calculation; - } } /** @@ -754,7 +740,7 @@ class grade_test extends UnitTestCase { $grade_raw = new stdClass(); $grade_raw->itemid = $this->grade_items[0]->id; $grade_raw->userid = 1; - $grade_raw->gradevalue = 72; + $grade_raw->gradevalue = 15; // too small $grade_raw->timecreated = mktime(); $grade_raw->timemodified = mktime(); @@ -765,7 +751,7 @@ class grade_test extends UnitTestCase { $grade_raw = new stdClass(); $grade_raw->itemid = $this->grade_items[0]->id; $grade_raw->userid = 2; - $grade_raw->gradevalue = 78; + $grade_raw->gradevalue = 40; $grade_raw->timecreated = mktime(); $grade_raw->timemodified = mktime(); @@ -776,7 +762,7 @@ class grade_test extends UnitTestCase { $grade_raw = new stdClass(); $grade_raw->itemid = $this->grade_items[0]->id; $grade_raw->userid = 3; - $grade_raw->gradevalue = 68; + $grade_raw->gradevalue = 170; // too big $grade_raw->timecreated = mktime(); $grade_raw->timemodified = mktime(); @@ -784,40 +770,8 @@ class grade_test extends UnitTestCase { $this->grade_grades_raw[] = $grade_raw; } - // Grades for grade_item 2 - - $grade_raw = new stdClass(); - $grade_raw->itemid = $this->grade_items[1]->id; - $grade_raw->userid = 1; - $grade_raw->gradevalue = 66; - $grade_raw->timecreated = mktime(); - $grade_raw->timemodified = mktime(); - - if ($grade_raw->id = insert_record('grade_grades_raw', $grade_raw)) { - $this->grade_grades_raw[] = $grade_raw; - } - - $grade_raw = new stdClass(); - $grade_raw->itemid = $this->grade_items[1]->id; - $grade_raw->userid = 2; - $grade_raw->gradevalue = 84; - $grade_raw->timecreated = mktime(); - $grade_raw->timemodified = mktime(); - - if ($grade_raw->id = insert_record('grade_grades_raw', $grade_raw)) { - $this->grade_grades_raw[] = $grade_raw; - } - - $grade_raw = new stdClass(); - $grade_raw->itemid = $this->grade_items[1]->id; - $grade_raw->userid = 3; - $grade_raw->gradevalue = 91; - $grade_raw->timecreated = mktime(); - $grade_raw->timemodified = mktime(); + // No raw grades for grade_item 2 - it is calculated - if ($grade_raw->id = insert_record('grade_grades_raw', $grade_raw)) { - $this->grade_grades_raw[] = $grade_raw; - } // Grades for grade_item 3 @@ -894,45 +848,34 @@ class grade_test extends UnitTestCase { // Grades for grade_item 8 - $grade_raw = new stdClass(); - $grade_raw->itemid = $this->grade_items[7]->id; - $grade_raw->userid = 1; - $grade_raw->gradevalue = 97; - $grade_raw->timecreated = mktime(); - $grade_raw->timemodified = mktime(); - - if ($grade_raw->id = insert_record('grade_grades_raw', $grade_raw)) { - $this->grade_grades_raw[] = $grade_raw; - } - $grade_raw = new stdClass(); $grade_raw->itemid = $this->grade_items[7]->id; $grade_raw->userid = 2; - $grade_raw->gradevalue = 49; + $grade_raw->gradevalue = 3; $grade_raw->timecreated = mktime(); $grade_raw->timemodified = mktime(); if ($grade_raw->id = insert_record('grade_grades_raw', $grade_raw)) { $this->grade_grades_raw[] = $grade_raw; } - + $grade_raw = new stdClass(); $grade_raw->itemid = $this->grade_items[7]->id; $grade_raw->userid = 3; - $grade_raw->gradevalue = 67; + $grade_raw->gradevalue = 6; $grade_raw->timecreated = mktime(); $grade_raw->timemodified = mktime(); if ($grade_raw->id = insert_record('grade_grades_raw', $grade_raw)) { $this->grade_grades_raw[] = $grade_raw; } - + // Grades for grade_item 9 $grade_raw = new stdClass(); $grade_raw->itemid = $this->grade_items[8]->id; $grade_raw->userid = 1; - $grade_raw->gradevalue = 49; + $grade_raw->gradevalue = 20; $grade_raw->timecreated = mktime(); $grade_raw->timemodified = mktime(); @@ -943,7 +886,7 @@ class grade_test extends UnitTestCase { $grade_raw = new stdClass(); $grade_raw->itemid = $this->grade_items[8]->id; $grade_raw->userid = 2; - $grade_raw->gradevalue = 93; + $grade_raw->gradevalue = 50; $grade_raw->timecreated = mktime(); $grade_raw->timemodified = mktime(); @@ -954,7 +897,7 @@ class grade_test extends UnitTestCase { $grade_raw = new stdClass(); $grade_raw->itemid = $this->grade_items[7]->id; $grade_raw->userid = 3; - $grade_raw->gradevalue = 76; + $grade_raw->gradevalue = 100; $grade_raw->timecreated = mktime(); $grade_raw->timemodified = mktime(); @@ -972,7 +915,7 @@ class grade_test extends UnitTestCase { $grade_final = new stdClass(); $grade_final->itemid = $this->grade_items[0]->id; $grade_final->userid = 1; - $grade_final->gradevalue = 97.8; + $grade_final->gradevalue = 30; $grade_final->timecreated = mktime(); $grade_final->timemodified = mktime(); @@ -983,7 +926,7 @@ class grade_test extends UnitTestCase { $grade_final = new stdClass(); $grade_final->itemid = $this->grade_items[0]->id; $grade_final->userid = 2; - $grade_final->gradevalue = 106.2; + $grade_final->gradevalue = 40; $grade_final->timecreated = mktime(); $grade_final->timemodified = mktime(); $grade_final->locked = true; @@ -995,7 +938,7 @@ class grade_test extends UnitTestCase { $grade_final = new stdClass(); $grade_final->itemid = $this->grade_items[0]->id; $grade_final->userid = 3; - $grade_final->gradevalue = 92.2; + $grade_final->gradevalue = 110; $grade_final->timecreated = mktime(); $grade_final->timemodified = mktime(); $grade_final->locked = false; @@ -1009,7 +952,7 @@ class grade_test extends UnitTestCase { $grade_final = new stdClass(); $grade_final->itemid = $this->grade_items[1]->id; $grade_final->userid = 1; - $grade_final->gradevalue = 69; + $grade_final->gradevalue = 70; $grade_final->timecreated = mktime(); $grade_final->timemodified = mktime(); $grade_final->locked = true; @@ -1021,7 +964,7 @@ class grade_test extends UnitTestCase { $grade_final = new stdClass(); $grade_final->itemid = $this->grade_items[1]->id; $grade_final->userid = 2; - $grade_final->gradevalue = 87; + $grade_final->gradevalue = 100; $grade_final->timecreated = mktime(); $grade_final->timemodified = mktime(); $grade_final->locked = true; @@ -1120,22 +1063,10 @@ class grade_test extends UnitTestCase { // Grades for grade_item 8 - $grade_final = new stdClass(); - $grade_final->itemid = $this->grade_items[7]->id; - $grade_final->userid = 1; - $grade_final->gradevalue = 69; - $grade_final->timecreated = mktime(); - $grade_final->timemodified = mktime(); - $grade_final->locked = true; - - if ($grade_final->id = insert_record('grade_grades_final', $grade_final)) { - $this->grade_grades_final[] = $grade_final; - } - $grade_final = new stdClass(); $grade_final->itemid = $this->grade_items[7]->id; $grade_final->userid = 2; - $grade_final->gradevalue = 87; + $grade_final->gradevalue = 3; $grade_final->timecreated = mktime(); $grade_final->timemodified = mktime(); $grade_final->locked = true; @@ -1147,7 +1078,7 @@ class grade_test extends UnitTestCase { $grade_final = new stdClass(); $grade_final->itemid = $this->grade_items[7]->id; $grade_final->userid = 3; - $grade_final->gradevalue = 94; + $grade_final->gradevalue = 6; $grade_final->timecreated = mktime(); $grade_final->timemodified = mktime(); $grade_final->locked = false; @@ -1161,7 +1092,7 @@ class grade_test extends UnitTestCase { $grade_final = new stdClass(); $grade_final->itemid = $this->grade_items[8]->id; $grade_final->userid = 1; - $grade_final->gradevalue = 69; + $grade_final->gradevalue = 20; $grade_final->timecreated = mktime(); $grade_final->timemodified = mktime(); $grade_final->locked = true; @@ -1173,7 +1104,7 @@ class grade_test extends UnitTestCase { $grade_final = new stdClass(); $grade_final->itemid = $this->grade_items[8]->id; $grade_final->userid = 2; - $grade_final->gradevalue = 87; + $grade_final->gradevalue = 50; $grade_final->timecreated = mktime(); $grade_final->timemodified = mktime(); $grade_final->locked = true; @@ -1185,7 +1116,7 @@ class grade_test extends UnitTestCase { $grade_final = new stdClass(); $grade_final->itemid = $this->grade_items[8]->id; $grade_final->userid = 3; - $grade_final->gradevalue = 94; + $grade_final->gradevalue = 100; $grade_final->timecreated = mktime(); $grade_final->timemodified = mktime(); $grade_final->locked = false; diff --git a/lib/simpletest/grade/simpletest/testgradecategory.php b/lib/simpletest/grade/simpletest/testgradecategory.php index 647c5b0a1c..37690ed6bb 100755 --- a/lib/simpletest/grade/simpletest/testgradecategory.php +++ b/lib/simpletest/grade/simpletest/testgradecategory.php @@ -79,7 +79,7 @@ class grade_category_test extends grade_test { $grade_category->fullname = 'unittestcategory4'; $grade_category->courseid = $this->courseid; - $grade_category->aggregation = GRADE_AGGREGATE_MEAN; + $grade_category->aggregation = GRADE_AGGREGATE_MEAN_GRADED; $grade_category->keephigh = 100; $grade_category->droplow = 10; $grade_category->hidden = 0; @@ -166,47 +166,31 @@ class grade_category_test extends grade_test { } function test_grade_category_generate_grades() { - global $CFG; - $CFG->usenullgrades = true; - - $category = new grade_category($this->grade_categories[0]); + $category = new grade_category($this->grade_categories[3]); $this->assertTrue(method_exists($category, 'generate_grades')); - $category->generate_grades(); $category->load_grade_item(); + $raw_grades = get_records('grade_grades_raw', 'itemid', $category->grade_item->id); - $final_grades = get_records('grade_grades_final', 'itemid', $category->grade_item->id); + $this->assertFalse($raw_grades); + $category->generate_grades(); + $raw_grades = get_records('grade_grades_raw', 'itemid', $category->grade_item->id); $this->assertEqual(3, count($raw_grades)); - $this->assertEqual(3, count($final_grades)); - + + $rawvalues = array(); foreach ($raw_grades as $grade) { $this->assertWithinMargin($grade->gradevalue, $grade->grademin, $grade->grademax); + $rawvalues[] = (int)$grade->gradevalue; } - - foreach ($final_grades as $grade) { - $this->assertWithinMargin($grade->gradevalue, 0, 100); - } + sort($rawvalues); + // calculated mean results + $this->assertEqual($rawvalues, array(20,50,100)); } function test_grade_category_aggregate_grades() { $category = new grade_category($this->grade_categories[0]); - $this->assertTrue(method_exists($category, 'aggregate_grades')); - - // Generate 3 random data sets - $grade_sets = array(); - - for ($i = 0; $i < 3; $i++) { - for ($j = 0; $j < 200; $j++) { - $grade_sets[$i][] = $this->generate_random_raw_grade(new grade_item($this->grade_items[$i]), $j); - } - } - - $aggregated_grades = $category->aggregate_grades($grade_sets); - $this->assertEqual(200, count($aggregated_grades)); - $this->assertWithinMargin($aggregated_grades[rand(1, count($aggregated_grades) - 1)]->gradevalue, 0, 100); - $this->assertWithinMargin($aggregated_grades[rand(1, count($aggregated_grades) - 1)]->gradevalue, 0, 100); - $this->assertWithinMargin($aggregated_grades[rand(1, count($aggregated_grades) - 1)]->gradevalue, 0, 100); - $this->assertWithinMargin($aggregated_grades[rand(1, count($aggregated_grades) - 1)]->gradevalue, 0, 100); + $this->assertTrue(method_exists($category, 'aggregate_grades')); + // tested above in test_grade_category_generate_grades() } function generate_random_raw_grade($item, $userid) { @@ -274,13 +258,17 @@ class grade_category_test extends grade_test { $grades = array(5.374, 9.4743, 2.5474, 7.3754); $category->droplow = 2; - $result = $category->apply_limit_rules(fullclone($grades)); - $this->assertEqual(array(7.3754, 9.4743), $result); - + $category->apply_limit_rules($grades); + sort($grades, SORT_NUMERIC); + $this->assertEqual(array(7.3754, 9.4743), $grades); + + $category = new grade_category(); + $grades = array(5.374, 9.4743, 2.5474, 7.3754); + $category->keephigh = 1; $category->droplow = 0; - $result = $category->apply_limit_rules(fullclone($grades)); - $this->assertEqual(array(9.4743), $result); + $category->apply_limit_rules($grades); + $this->assertEqual(array(9.4743), $grades); } } ?> diff --git a/lib/simpletest/grade/simpletest/testgradeitem.php b/lib/simpletest/grade/simpletest/testgradeitem.php index 4536766ce4..84b535a619 100755 --- a/lib/simpletest/grade/simpletest/testgradeitem.php +++ b/lib/simpletest/grade/simpletest/testgradeitem.php @@ -250,18 +250,15 @@ class grade_item_test extends grade_test { function test_grade_item_get_final() { $grade_item = new grade_item($this->grade_items[0]); $this->assertTrue(method_exists($grade_item, 'get_final')); - - $final_grades = $grade_item->get_final($this->userid); - $final_grade = current($final_grades); - $this->assertEqual(1, count($final_grade)); - $this->assertEqual($this->grade_grades_final[0]->gradevalue, $final_grade->gradevalue); + $final_grade = $grade_item->get_final($this->userid); + $this->assertEqual($this->grade_grades_final[0]->gradevalue, $final_grade->gradevalue); } function test_grade_item_get_calculation() { - $grade_item = new grade_item($this->grade_items[0]); + $grade_item = new grade_item($this->grade_items[1]); $this->assertTrue(method_exists($grade_item, 'get_calculation')); - $grade_calculation = $grade_item->get_calculation(); + $this->assertEqual($this->grade_calculations[0]->id, $grade_calculation->id); } @@ -286,13 +283,12 @@ class grade_item_test extends grade_test { } /** - * Test update of all final grades, then only 1 grade (give a $userid) + * Test update of all final grades */ function test_grade_item_update_final_grades() { $grade_item = new grade_item($this->grade_items[0]); $this->assertTrue(method_exists($grade_item, 'update_final_grade')); - $this->assertEqual(3, $grade_item->update_final_grade()); - $this->assertEqual(1, $grade_item->update_final_grade(1)); + $this->assertEqual(true, $grade_item->update_final_grade()); } /** @@ -324,19 +320,12 @@ class grade_item_test extends grade_test { function test_grade_item_load_fake_final() { $grade_item = new grade_item($this->grade_items[0]); $this->assertTrue(method_exists($grade_item, 'load_final')); - global $CFG; - $CFG->usenullgrades = true; // Delete one of the final grades $final_grade = new grade_grades_final($this->grade_grades_final[0]); $final_grade->delete(); unset($this->grade_grades_final[0]); - // Load the final grades - $final_grades = $grade_item->load_final(true); - $this->assertEqual(3, count($final_grades)); - $this->assertEqual($grade_item->grademin, $final_grades[1]->gradevalue); - // Load normal final grades $final_grades = $grade_item->load_final(); $this->assertEqual(2, count($final_grades)); @@ -412,7 +401,7 @@ class grade_item_test extends grade_test { $grade_item->insert(); $grade_item->load_scale(); $this->assertEqual('Very Good', $grade_item->scale->scale_items[1]); - + // Load raw grade and its scale $grade_raw = new grade_grades_raw(array('scaleid' => $this->scale[0]->id), false); $grade_raw->gradevalue = 4; @@ -421,20 +410,11 @@ class grade_item_test extends grade_test { $grade_raw->insert(); $grade_raw->load_scale(); $this->assertEqual('Fairly neutral', $grade_raw->scale->scale_items[2]); - + // Test grade_item::adjust_scale - $this->assertEqual(3, round($grade_item->adjust_grade($grade_raw, null, 'gradevalue'))); + $this->assertEqual(3, $grade_item->adjust_grade($grade_raw)); $grade_raw->gradevalue = 6; - $this->assertEqual(4, $grade_item->adjust_grade($grade_raw, null, 'gradevalue')); - - // Check that the final grades have the correct values now - $grade_item->load_raw(); - $grade_item->update_final_grade(); - - $this->assertFalse(empty($grade_item->grade_grades_final)); - $this->assertEqual($grade_item->id, $grade_item->grade_grades_final[1]->itemid); - $this->assertEqual(2.66667, round($grade_item->grade_grades_final[1]->gradevalue, 5)); - $this->assertEqual(1, $grade_item->grade_grades_final[1]->userid); + $this->assertEqual(4, $grade_item->adjust_grade($grade_raw)); } function test_grade_item_toggle_locking() { @@ -473,51 +453,6 @@ class grade_item_test extends grade_test { $this->assertTrue($grade_item->grade_grades_final[3]->hidden); } - function test_grade_item_generate_final() { - $grade_item = new grade_item(); - $grade_item->courseid = $this->courseid; - $grade_item->categoryid = $this->grade_categories[1]->id; - $grade_item->itemname = 'unittestgradeitem4'; - $grade_item->itemtype = 'mod'; - $grade_item->grademin = 0; - $grade_item->grademax = 100; - $grade_item->itemmodule = 'quiz'; - $grade_item->iteminfo = 'Grade item used for unit testing'; - - $grade_item->insert(); - - $grade_grades_raw = new grade_grades_raw(); - $grade_grades_raw->itemid = $grade_item->id; - $grade_grades_raw->userid = 1; - $grade_grades_raw->gradevalue = 88; - $grade_grades_raw->grademax = 110; - $grade_grades_raw->grademin = 18; - $grade_grades_raw->insert(); - - $grade_grades_raw = new grade_grades_raw(); - $grade_grades_raw->itemid = $grade_item->id; - $grade_grades_raw->userid = 2; - $grade_grades_raw->gradevalue = 68; - $grade_grades_raw->grademax = 110; - $grade_grades_raw->grademin = 18; - $grade_grades_raw->insert(); - - $grade_grades_raw = new grade_grades_raw(); - $grade_grades_raw->itemid = $grade_item->id; - $grade_grades_raw->userid = 3; - $grade_grades_raw->gradevalue = 81; - $grade_grades_raw->grademax = 110; - $grade_grades_raw->grademin = 18; - $grade_grades_raw->insert(); - - $grade_item->load_raw(); - $this->assertEqual(3, count($grade_item->grade_grades_raw)); - - $grade_item->generate_final(); - $grade_item->load_final(); - $this->assertEqual(3, count($grade_item->grade_grades_final)); - } - function test_float_keys() { } } diff --git a/lib/simpletest/grade/simpletest/testgraderaw.php b/lib/simpletest/grade/simpletest/testgraderaw.php index c5f33433bd..cd6bf2b13b 100755 --- a/lib/simpletest/grade/simpletest/testgraderaw.php +++ b/lib/simpletest/grade/simpletest/testgraderaw.php @@ -88,7 +88,7 @@ class grade_raw_test extends grade_test { $grade_grades_raw->insert(); - $this->assertEqual(7, $grade_grades_raw->grademax); + $this->assertEqual(6, $grade_grades_raw->grademax); $this->assertEqual(0, $grade_grades_raw->grademin); } diff --git a/lib/simpletest/grade/simpletest/testgradetree.php b/lib/simpletest/grade/simpletest/testgradetree.php index 1d90a5f820..3e09b8584e 100644 --- a/lib/simpletest/grade/simpletest/testgradetree.php +++ b/lib/simpletest/grade/simpletest/testgradetree.php @@ -284,7 +284,7 @@ class grade_tree_test extends grade_test { function test_grade_tree_get_tree() { $tree = new grade_tree($this->courseid, true); - $this->assertEqual(48, count($tree->tree_array, COUNT_RECURSIVE)); + $this->assertEqual(47, count($tree->tree_array, COUNT_RECURSIVE)); } function test_grade_tree_remove_element() { diff --git a/lib/simpletest/testgradelib.php b/lib/simpletest/testgradelib.php index 12f778045c..9ff1245176 100644 --- a/lib/simpletest/testgradelib.php +++ b/lib/simpletest/testgradelib.php @@ -53,6 +53,8 @@ class gradelib_test extends grade_test { } } +/* +// obsolted function, should be replaced by grade_update() or removed completely function test_grade_create_category() { if (get_class($this) == 'gradelib_test') { $grade_category = new stdClass(); @@ -73,7 +75,7 @@ class gradelib_test extends grade_test { $this->grade_items[] = $db_grade_category->grade_item; } } - +*/ function test_grade_is_locked() { if (get_class($this) == 'gradelib_test') { $grade_item = $this->grade_items[0]; diff --git a/lib/simpletest/testmathslib.php b/lib/simpletest/testmathslib.php index 486bb4d9da..071490dc57 100755 --- a/lib/simpletest/testmathslib.php +++ b/lib/simpletest/testmathslib.php @@ -65,6 +65,15 @@ class mathsslib_test extends UnitTestCase { $this->assertEqual($res, 30, 'maximum is: %s'); } + /** + * Tests special chars + */ + function test__specialchars() { + $formula = new calc_formula('=gi1 + gi2 + gi11', array('gi1'=>10,'gi2'=>20,'gi11'=>30)); + $res = $formula->evaluate(); + $this->assertEqual($res, 60, 'sum is: %s'); + } + } ?> \ No newline at end of file -- 2.39.5