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).
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+22
-24
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
Reference in New Issue
Block a user