From: nicolasconnault Date: Fri, 11 May 2007 08:46:34 +0000 (+0000) Subject: MDL-9506 Almost completed category aggregation, including generation of raw and final... X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=2df712352d59ce268ddff869be4ef0ef19462c17;p=moodle.git MDL-9506 Almost completed category aggregation, including generation of raw and final grades held by these categories. Only a few small glitches remain, that cause these grades not to be generated properly. This is the last critical element of the gradebook API, so I'm looking forward to finishing it :-) --- diff --git a/lib/grade/grade_category.php b/lib/grade/grade_category.php index c4c7db977f..08c0d6fefa 100644 --- a/lib/grade/grade_category.php +++ b/lib/grade/grade_category.php @@ -134,6 +134,8 @@ class grade_category extends grade_object { $this->grade_item->update(); } } + + $this->path = grade_category::build_path($this); } @@ -304,7 +306,6 @@ class grade_category extends grade_object { 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! } - $this->path = grade_category::build_path($this); $paths = explode('/', $this->path); @@ -330,81 +331,82 @@ 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. + * 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) + * 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() { - // Check that the children have final grades. If not, call their generate_grades method (recursion) - if (empty($this->children)) { - $this->children = $this->get_children(1, 'flat'); - } + // 1. Get immediate children + $children = $this->get_children(1, 'flat'); - $category_raw_grades = array(); - $aggregated_grades = array(); - - foreach ($this->children as $child) { - if (get_class($child) == 'grade_item') { - $category_raw_grades[$child->id] = $child->load_raw(); - } elseif (get_class($child) == 'grade_category') { - $child->load_grade_item(); - $raw_grades = $child->grade_item->load_raw(); - - if (empty($raw_grades)) { - $child->generate_grades(); - $category_raw_grades[$child->id] = $child->grade_item->load_raw(); - } else { - $category_raw_grades[$child->id] = $raw_grades; - } - } + if (empty($children)) { + return false; } + + // This assumes that all immediate children are of the same type (category OR item) + $childrentype = get_class(current($children)); - if (empty($category_raw_grades)) { - return null; - } else { - $aggregated_grades = $this->aggregate_grades($category_raw_grades); - - if (count($category_raw_grades) == 1) { - $aggregated_grades = current($category_raw_grades); + $final_grades_for_aggregation = array(); + + // 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(); + + if ($category->grade_item->needsupdate) { + $category->generate_grades(); + } + + $final_grades_for_aggregation[] = $category->grade_item->get_standardised_final(); } - - foreach ($aggregated_grades as $raw_grade) { - $raw_grade->itemid = $this->grade_item->id; - $raw_grade->insert(); + } elseif ($childrentype == 'grade_item') { + foreach ($children as $id => $item) { + if ($item->needsupdate) { + $item->generate_final(); + } + + $final_grades_for_aggregation[] = $item->get_standardised_final(); } - - $this->grade_item->generate_final(); } - - $this->grade_item->load_raw(); - return $this->grade_item->grade_grades_raw; + // 3. Aggregate the grades + $aggregated_grades = $this->aggregate_grades($final_grades_for_aggregation); + + // 4. Save the resulting array of grades as raw grades + $this->load_grade_item(); + $this->grade_item->save_raw($aggregated_grades); + + // 5. Use the grade_item's generate_final method + $this->grade_item->generate_final(); + + return true; } /** - * Given an array of arrays of grade objects (raw or final), uses this category's aggregation method to - * compute and return a single array of grade_raw objects with the aggregated gradevalue. This method - * must also standardise all the scores (which have different mins and maxs) so that their values can - * be meaningfully aggregated (it would make no sense to perform MEAN(239, 5) on a grade_item with a - * gradevalue between 20 and 250 and another grade_item with a gradevalue between 0 and 7!). Aggregated - * values will be saved as grade_grades_raw->gradevalue, even when scales are involved. + * 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($raw_grade_sets) { - if (empty($raw_grade_sets)) { + function aggregate_grades($final_grade_sets) { + if (empty($final_grade_sets)) { return null; } - $aggregated_grades = array(); $pooled_grades = array(); - foreach ($raw_grade_sets as $setkey => $set) { - foreach ($set as $gradekey => $raw_grade) { + foreach ($final_grade_sets as $setkey => $set) { + foreach ($set as $userid => $final_grade) { $this->load_grade_item(); - - $value = standardise_score($raw_grade->gradevalue, $raw_grade->grademin, $raw_grade->grademax, - $this->grade_item->grademin, $this->grade_item->grademax); - $pooled_grades[$raw_grade->userid][] = $value; + $value = standardise_score((float) $final_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax); + $pooled_grades[$userid][] = $value; } } - + foreach ($pooled_grades as $userid => $grades) { $aggregated_value = null; @@ -429,7 +431,7 @@ class grade_category extends grade_object { 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 : + case GRADE_AGGREGATE_SUM : // I don't see much point to this one either $aggregated_value = array_sum($grades); break; default: @@ -447,7 +449,6 @@ class grade_category extends grade_object { $grade_raw->itemid = $this->grade_item->id; $aggregated_grades[$userid] = $grade_raw; } - return $aggregated_grades; } diff --git a/lib/grade/grade_item.php b/lib/grade/grade_item.php index ab8e8df81e..d4fa038ebb 100644 --- a/lib/grade/grade_item.php +++ b/lib/grade/grade_item.php @@ -257,9 +257,12 @@ 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() { + function load_final($generatefakenullgrades=false) { + global $CFG; + $grade_final_array = get_records('grade_grades_final', 'itemid', $this->id); if (empty($grade_final_array)) { @@ -274,7 +277,41 @@ class grade_item extends grade_object { foreach ($grade_final_array as $f) { $this->grade_grades_final[$f->userid] = new grade_grades_final($f); } - return $this->grade_grades_final; + + $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; + } + + /** + * Returns an array of values (NOT objects) standardised from the final grades of this grade_item. They are indexed by userid. + * @return array integers + */ + function get_standardised_final() { + $standardised_finals = array(); + + $final_grades = $this->load_final(true); + foreach ($final_grades as $userid => $final) { + $standardised_finals[$userid] = standardise_score($final->gradevalue, $this->grademin, $this->grademax, 0, 1, true); + } + + return $standardised_finals; } /** @@ -436,6 +473,31 @@ class grade_item extends grade_object { } return $grade_raw_array; } + + /** + * 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; + $raw_grade->insert(); + } + + $this->grade_grades_raw[$userid] = $raw_grade; + } + } else { + return false; + } + } /** * Returns the final values for this grade item (as imported by module or other source). diff --git a/lib/gradelib.php b/lib/gradelib.php index f069b60825..4e43a85038 100644 --- a/lib/gradelib.php +++ b/lib/gradelib.php @@ -222,7 +222,15 @@ function grades_grab_grades() { * @param float $target_max * @return float Converted value */ -function standardise_score($gradevalue, $source_min, $source_max, $target_min, $target_max) { +function standardise_score($gradevalue, $source_min, $source_max, $target_min, $target_max, $debug=false) { + if ($debug) { + echo 'standardise_score debug info: (lib/gradelib.php)'; + print_object(array('gradevalue' => $gradevalue, + 'source_min' => $source_min, + 'source_max' => $source_max, + 'target_min' => $target_min, + 'target_max' => $target_max)); + } $factor = ($gradevalue - $source_min) / ($source_max - $source_min); $diff = $target_max - $target_min; $gradevalue = $factor * $diff + $target_min; diff --git a/lib/simpletest/grade/simpletest/testgradecategory.php b/lib/simpletest/grade/simpletest/testgradecategory.php index 07528c0f24..d14cbfe9fd 100755 --- a/lib/simpletest/grade/simpletest/testgradecategory.php +++ b/lib/simpletest/grade/simpletest/testgradecategory.php @@ -163,10 +163,20 @@ class grade_category_test extends gradelib_test { } function test_grade_category_generate_grades() { + global $CFG; + $CFG->usenullgrades = true; + $category = new grade_category($this->grade_categories[0]); $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->assertEqual(3, count($raw_grades)); + $this->assertEqual(3, count($final_grades)); + } +/** function test_grade_category_aggregate_grades() { $category = new grade_category($this->grade_categories[0]); $this->assertTrue(method_exists($category, 'aggregate_grades')); @@ -179,11 +189,15 @@ class grade_category_test extends gradelib_test { $grade_sets[$i][] = $this->generate_random_raw_grade($this->grade_items[$i], $j); } } - - $this->assertEqual(200, count($category->aggregate_grades($grade_sets))); - + + $aggregated_grades = $category->aggregate_grades($grade_sets); + $this->assertEqual(200, count($aggregated_grades)); + $this->assertWithinMargin($aggregated_grades[rand(0, count($aggregated_grades))]->gradevalue, 0, 100); + $this->assertWithinMargin($aggregated_grades[rand(0, count($aggregated_grades))]->gradevalue, 0, 100); + $this->assertWithinMargin($aggregated_grades[rand(0, count($aggregated_grades))]->gradevalue, 0, 100); + $this->assertWithinMargin($aggregated_grades[rand(0, count($aggregated_grades))]->gradevalue, 0, 100); } - +*/ function generate_random_raw_grade($item, $userid) { $raw_grade = new grade_grades_raw(); $raw_grade->itemid = $item->id; @@ -191,7 +205,7 @@ class grade_category_test extends gradelib_test { $raw_grade->grademin = $item->grademin; $raw_grade->grademax = $item->grademax; $valuetype = "grade$item->gradetype"; - $raw_grade->$valuetype = rand($raw_grade->grademin, $raw_grade->grademax); + $raw_grade->gradevalue = rand(0, 1000) / 1000; $raw_grade->insert(); return $raw_grade; } diff --git a/lib/simpletest/grade/simpletest/testgradeitem.php b/lib/simpletest/grade/simpletest/testgradeitem.php index 2d1bb26d18..f6c88821a1 100755 --- a/lib/simpletest/grade/simpletest/testgradeitem.php +++ b/lib/simpletest/grade/simpletest/testgradeitem.php @@ -67,8 +67,8 @@ class grade_item_test extends gradelib_test { // Check the grade_category's needsupdate variable first $category = $grade_item->get_category(); $category->load_grade_item(); + $category->grade_item->needsupdate = false; $this->assertNotNull($category->grade_item); - $this->assertFalse($category->grade_item->needsupdate); $grade_item->insert(); @@ -89,7 +89,7 @@ class grade_item_test extends gradelib_test { $category = $grade_item->get_category(); $category->load_grade_item(); $this->assertNotNull($category->grade_item); - $this->assertFalse($category->grade_item->needsupdate); + $category->grade_item->needsupdate = false; $this->assertTrue($grade_item->delete()); @@ -110,7 +110,7 @@ class grade_item_test extends gradelib_test { $category= $grade_item->get_category(); $category->load_grade_item(); $this->assertNotNull($category->grade_item); - $this->assertFalse($category->grade_item->needsupdate); + $category->grade_item->needsupdate = false; $this->assertTrue($grade_item->update()); @@ -281,6 +281,30 @@ class grade_item_test extends gradelib_test { $this->assertEqual($this->grade_grades_final[0]->gradevalue, $grade_item->grade_grades_final[1]->gradevalue); $this->assertEqual($this->grade_grades_raw[0]->gradevalue, $grade_item->grade_grades_raw[1]->gradevalue); } + + /** + * Test loading final items, generating fake values to replace missing grades + */ + 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)); + } /** * Test the adjust_grade method diff --git a/lib/simpletest/testgradelib.php b/lib/simpletest/testgradelib.php index e34d5e6045..656d6d4e4e 100644 --- a/lib/simpletest/testgradelib.php +++ b/lib/simpletest/testgradelib.php @@ -498,6 +498,7 @@ class gradelib_test extends UnitTestCase { $grade_item->courseid = $this->courseid; $grade_item->iteminstance = $this->grade_categories[0]->id; $grade_item->itemname = 'unittestgradeitemcategory1'; + $grade_item->needsupdate = true; $grade_item->itemtype = 'category'; $grade_item->iteminfo = 'Grade item used for unit testing'; $grade_item->timecreated = mktime(); @@ -513,6 +514,7 @@ class gradelib_test extends UnitTestCase { $grade_item->iteminstance = $this->grade_categories[1]->id; $grade_item->itemname = 'unittestgradeitemcategory2'; $grade_item->itemtype = 'category'; + $grade_item->needsupdate = true; $grade_item->iteminfo = 'Grade item used for unit testing'; $grade_item->timecreated = mktime(); $grade_item->timemodified = mktime(); @@ -527,6 +529,7 @@ class gradelib_test extends UnitTestCase { $grade_item->iteminstance = $this->grade_categories[2]->id; $grade_item->itemname = 'unittestgradeitemcategory3'; $grade_item->itemtype = 'category'; + $grade_item->needsupdate = true; $grade_item->iteminfo = 'Grade item used for unit testing'; $grade_item->timecreated = mktime(); $grade_item->timemodified = mktime();