From 2c72af1f08c17836aa2ae6c6d7f6aa61a789dcbe Mon Sep 17 00:00:00 2001 From: nicolasconnault Date: Tue, 8 May 2007 08:01:55 +0000 Subject: [PATCH] MDL-9506 Stuck on grade_category->generate_grades. I cannot figure out how to recursively generate raw grades for each category's associated grade_item based on that category's children categories and items. Heaps of other changes with this commit, including a new grade_object::update_from_db() method, which uses the state of the record in DB to update the current object with a matching id (useful when you insert an incomplete object in the DB and want to get the default values as set up in the DB). --- lib/grade/grade_calculation.php | 3 +- lib/grade/grade_category.php | 111 ++++++++++++++++-- lib/grade/grade_item.php | 46 ++++---- lib/grade/grade_object.php | 21 ++++ lib/gradelib.php | 17 +++ .../grade/simpletest/testgradecategory.php | 35 ++++++ .../grade/simpletest/testgradeitem.php | 4 +- lib/simpletest/testgradelib.php | 19 ++- 8 files changed, 216 insertions(+), 40 deletions(-) diff --git a/lib/grade/grade_calculation.php b/lib/grade/grade_calculation.php index 8fec2e23d3..1508dc89a2 100644 --- a/lib/grade/grade_calculation.php +++ b/lib/grade/grade_calculation.php @@ -70,10 +70,9 @@ class grade_calculation extends grade_object { /** * Applies the formula represented by this object to the value given, and returns the result. * @param float $oldvalue - * @param string $valuetype Either 'gradevalue' or 'gradescale' * @return float result */ - function compute($oldvalue, $valuetype = 'gradevalue') { + function compute($oldvalue) { return $oldvalue; // TODO implement computation using parser } diff --git a/lib/grade/grade_category.php b/lib/grade/grade_category.php index e05f223e1b..b161019c79 100644 --- a/lib/grade/grade_category.php +++ b/lib/grade/grade_category.php @@ -219,7 +219,7 @@ class grade_category extends grade_object { * to apply calculations to and generate final grades. */ function generate_grades() { - // Check that the children have final grades. If not, call their generate_raw_grades method (recursion) + // 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'); } @@ -229,34 +229,118 @@ class grade_category extends grade_object { foreach ($this->children as $child) { if (get_class($child) == 'grade_item') { - $category_raw_grades[$child->id] = $child->load_final(); - } elseif ($get_class($child) == 'grade_category') { - $category_raw_grades[$child->id] = $child->load_final(); - if (empty($category_raw_grades)) { - $category_raw_grades[$child->id] = $child->generate_grades(); + $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($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); + } + foreach ($aggregated_grades as $raw_grade) { + $raw_grade->itemid = $this->grade_item->id; $raw_grade->insert(); } - $this->grade_item->generate_final(); + $this->load_grade_item(); + $this->grade_item->generate_final(); } + + $this->grade_item->load_raw(); + return $this->grade_item->grade_grades_raw; } /** * 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. + * 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 gradescale between 0 and 7!). Aggregated + * values will be saved as grade_grades_raw->gradevalue, even when scales are involved. * @param array $raw_grade_sets * @return array Raw grade objects */ function aggregate_grades($raw_grade_sets) { + if (empty($raw_grade_sets)) { + return null; + } + $aggregated_grades = array(); + $pooled_grades = array(); + + foreach ($raw_grade_sets as $setkey => $set) { + foreach ($set as $gradekey => $raw_grade) { + $valuetype = 'gradevalue'; + + if (!empty($raw_grade->gradescale)) { + $valuetype = 'gradescale'; + } + $this->load_grade_item(); + + $value = standardise_score($raw_grade->$valuetype, $raw_grade->grademin, $raw_grade->grademax, + $this->grade_item->grademin, $this->grade_item->grademax); + $pooled_grades[$raw_grade->userid][] = $value; + } + } + + foreach ($pooled_grades as $userid => $grades) { + $aggregated_value = null; + + 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 : + $aggregated_value = array_sum($grades); + break; + default: + $num = count($grades); + $sum = array_sum($grades); + $aggregated_value = $sum / $num; + break; + } + + $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; } /** @@ -414,6 +498,15 @@ class grade_category extends grade_object { function load_grade_item() { $params = get_record('grade_items', 'categoryid', $this->id, 'itemtype', 'category'); $this->grade_item = new grade_item($params); + + // If the associated grade_item isn't yet created, do it now + if (empty($this->grade_item->id)) { + $this->grade_item->iteminstance = $this->id; + $this->grade_item->itemtype = 'category'; + $this->grade_item->insert(); + $this->grade_item->update_from_db(); + } + return $this->grade_item; } } diff --git a/lib/grade/grade_item.php b/lib/grade/grade_item.php index c559469c5d..27862020ff 100644 --- a/lib/grade/grade_item.php +++ b/lib/grade/grade_item.php @@ -296,6 +296,11 @@ class grade_item extends grade_object { */ function load_raw() { $grade_raw_array = get_records('grade_grades_raw', 'itemid', $this->id); + + if (empty($grade_raw_array)) { + return null; + } + foreach ($grade_raw_array as $r) { $this->grade_grades_raw[$r->userid] = new grade_grades_raw($r); } @@ -567,24 +572,21 @@ class grade_item extends grade_object { foreach ($grade_raw_array as $userid => $raw) { // the value could be gradevalue or gradescale - $valuetype = null; - - if (!empty($raw->gradevalue)) { - $valuetype = 'gradevalue'; - } elseif (!empty($raw->gradescale)) { - $valuetype = 'gradescale'; + $valuetype = "grade$this->gradetype"; + if (empty($raw->$valuetype)) { + return 'ERROR! The raw grade has no value for ' . $valuetype . ' (or the grade_item has no gradetype.)'; } - + $newgradevalue = $raw->$valuetype; if (!empty($this->calculation)) { $this->upgrade_calculation_to_object(); - $newgradevalue = $this->calculation->compute($raw->$valuetype, $valuetype); + $newgradevalue = $this->calculation->compute($raw->$valuetype); } $final = $this->grade_grades_final[$userid]; - $final->$valuetype = $this->adjust_grade($raw, $newgradevalue, $valuetype); + $final->$valuetype = $this->adjust_grade($raw, $newgradevalue); if ($final->update()) { $count++; @@ -612,19 +614,18 @@ class grade_item extends grade_object { * @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 or gradescale will be used. - * @param string $valuetype Either 'gradevalue' or 'gradescale' * @return mixed */ - function adjust_grade($grade_raw, $gradevalue=NULL, $valuetype='gradevalue') { + function adjust_grade($grade_raw, $gradevalue=NULL) { $raw_offset = 0; $item_offset = 0; - - if ($valuetype == 'gradevalue') { // Dealing with numerical grade + + if ($this->gradetype == 'value') { // Dealing with numerical grade if (empty($gradevalue)) { $gradevalue = $grade_raw->gradevalue; } - } elseif($valuetype == 'gradescale') { // Dealing with a scale value + } elseif($this->gradetype == 'scale') { // Dealing with a scale value if (empty($gradevalue)) { $gradevalue = $grade_raw->gradescale; } @@ -647,23 +648,20 @@ class grade_item extends grade_object { return false; } - /** - * Darlene's formula - */ - $factor = ($gradevalue - $grade_raw->grademin) / ($grade_raw->grademax - $grade_raw->grademin); - $diff = $this->grademax - $this->grademin; - $gradevalue = $factor * $diff + $this->grademin; + // Standardise score to the new grade range + $gradevalue = standardise_score($gradevalue, $grade_raw->grademin, + $grade_raw->grademax, $this->grademin, $this->grademax); // Apply rounding or factors, depending on whether it's a scale or value - if ($valuetype == 'gradevalue') { + if ($this->gradetype == 'value') { // Apply other grade_item factors $gradevalue *= $this->multfactor; $gradevalue += $this->plusfactor; - } elseif ($valuetype == 'gradescale') { + } elseif ($this->gradetype == 'scale') { $gradevalue = (int) round($gradevalue); } - + return $gradevalue; - } + } } ?> diff --git a/lib/grade/grade_object.php b/lib/grade/grade_object.php index 8ef6e192df..d234079d01 100644 --- a/lib/grade/grade_object.php +++ b/lib/grade/grade_object.php @@ -121,6 +121,27 @@ class grade_object { $this->id = insert_record($this->table, $clone, true); return $this->id; } + + /** + * Using this object's id field, fetches the matching record in the DB, and looks at + * each variable in turn. If the DB has different data, the db's data is used to update + * the object. This is different from the update() function, which acts on the DB record + * based on the object. + */ + function update_from_db() { + if (empty($this->id)) { + return false; + } else { + $class = get_class($this); + $object = new $class(array('id' => $this->id)); + foreach ($object as $var => $val) { + if ($this->$var != $val) { + $this->$var = $val; + } + } + } + return true; + } /** * Uses the variables of this object to retrieve all matching objects from the DB. diff --git a/lib/gradelib.php b/lib/gradelib.php index 259da1153a..799dee06ac 100644 --- a/lib/gradelib.php +++ b/lib/gradelib.php @@ -208,4 +208,21 @@ function grades_grab_grades() { } } +/** + * Given a float value situated between a source minimum and a source maximum, converts it to the + * corresponding value situated between a target minimum and a target maximum. Thanks to Darlene + * for the formula :-) + * @param float $gradevalue + * @param float $source_min + * @param float $source_max + * @param float $target_min + * @param float $target_max + * @return float Converted value + */ +function standardise_score($gradevalue, $source_min, $source_max, $target_min, $target_max) { + $factor = ($gradevalue - $source_min) / ($source_max - $source_min); + $diff = $target_max - $target_min; + $gradevalue = $factor * $diff + $target_min; + return $gradevalue; +} ?> diff --git a/lib/simpletest/grade/simpletest/testgradecategory.php b/lib/simpletest/grade/simpletest/testgradecategory.php index a89a56e660..2c44cdab17 100755 --- a/lib/simpletest/grade/simpletest/testgradecategory.php +++ b/lib/simpletest/grade/simpletest/testgradecategory.php @@ -159,6 +159,41 @@ class grade_category_test extends gradelib_test { $category = new grade_category(); $this->assertFalse($category->has_children()); } + + function test_grade_category_generate_grades() { + $category = new grade_category($this->grade_categories[0]); + $this->assertTrue(method_exists($category, 'generate_grades')); + $raw_grades = $category->generate_grades(); + $this->assertEqual(3, count($raw_grades)); + } + 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($this->grade_items[$i], $j); + } + } + + $this->assertEqual(200, count($category->aggregate_grades($grade_sets))); + + } + + function generate_random_raw_grade($item, $userid) { + $raw_grade = new grade_grades_raw(); + $raw_grade->itemid = $item->id; + $raw_grade->userid = $userid; + $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->insert(); + return $raw_grade; + } } ?> diff --git a/lib/simpletest/grade/simpletest/testgradeitem.php b/lib/simpletest/grade/simpletest/testgradeitem.php index c6cf4a102c..ee03453393 100755 --- a/lib/simpletest/grade/simpletest/testgradeitem.php +++ b/lib/simpletest/grade/simpletest/testgradeitem.php @@ -401,8 +401,8 @@ class grade_item_test extends gradelib_test { $this->assertEqual(3, count($grade_item->grade_grades_raw)); $grade_item->generate_final(); - $this->assertEqual(3, count($grade_item->grade_grades_final)); - + $grade_item->load_final(); + $this->assertEqual(3, count($grade_item->grade_grades_final)); } } ?> diff --git a/lib/simpletest/testgradelib.php b/lib/simpletest/testgradelib.php index a065957f53..4966cbe2f7 100644 --- a/lib/simpletest/testgradelib.php +++ b/lib/simpletest/testgradelib.php @@ -174,7 +174,9 @@ class gradelib_test extends UnitTestCase { $grade_item->itemtype = 'mod'; $grade_item->itemmodule = 'quiz'; $grade_item->iteminstance = 1; - $grade_item->grademax = 137; + $grade_item->gradetype = 'value'; + $grade_item->grademin = 30; + $grade_item->grademax = 140; $grade_item->itemnumber = 1; $grade_item->iteminfo = 'Grade item used for unit testing'; $grade_item->timecreated = mktime(); @@ -191,13 +193,16 @@ class gradelib_test extends UnitTestCase { $grade_item->itemname = 'unittestgradeitem2'; $grade_item->itemtype = 'import'; $grade_item->itemmodule = 'assignment'; + $grade_item->gradetype = 'value'; $grade_item->iteminstance = 2; $grade_item->itemnumber = null; + $grade_item->grademin = 0; + $grade_item->grademax = 100; $grade_item->iteminfo = 'Grade item used for unit testing'; $grade_item->locked = mktime() + 240000; $grade_item->timecreated = mktime(); $grade_item->timemodified = mktime(); - + if ($grade_item->id = insert_record('grade_items', $grade_item)) { $this->grade_items[] = $grade_item; } @@ -210,7 +215,10 @@ class gradelib_test extends UnitTestCase { $grade_item->itemtype = 'mod'; $grade_item->itemmodule = 'forum'; $grade_item->iteminstance = 3; - $grade_item->itemnumber = 3; + $grade_item->gradetype = 'scale'; + $grade_item->scaleid = 1; + $grade_item->grademin = 0; + $grade_item->grademax = 7; $grade_item->iteminfo = 'Grade item used for unit testing'; $grade_item->timecreated = mktime(); $grade_item->timemodified = mktime(); @@ -742,6 +750,11 @@ class gradelib_test extends UnitTestCase { $this->assertTrue(grade_is_locked($grade_item->itemtype, $grade_item->itemmodule, $grade_item->iteminstance, $grade_item->itemnumber)); } } + + function test_grade_standardise_score() { + $this->assertEqual(4, round(standardise_score(6, 0, 7, 0, 5))); + $this->assertEqual(40, standardise_score(50, 30, 80, 0, 100)); + } } ?> -- 2.39.5