From 5232d3f2c9eeafa53c48adcf6fba3f5a59007672 Mon Sep 17 00:00:00 2001 From: Damyon Wiese <damyon@moodle.com> Date: Mon, 4 Aug 2014 15:03:26 +0800 Subject: [PATCH] MDL-35984 grade: Recalculate min/max for sum of grades when hidden grades are involved. --- grade/report/lib.php | 42 +++++++++++++++++++++++++--- grade/report/overview/lib.php | 10 ++++++- grade/report/user/lib.php | 13 +++++++-- grade/tests/reportlib_test.php | 40 +++++++++++++++++++-------- lib/grade/grade_category.php | 50 ++++++++++++++++++++++++++++++---- lib/grade/grade_grade.php | 28 +++++++++++++++---- 6 files changed, 154 insertions(+), 29 deletions(-) diff --git a/grade/report/lib.php b/grade/report/lib.php index 3e757cd1084..29ebe66ad78 100644 --- a/grade/report/lib.php +++ b/grade/report/lib.php @@ -412,9 +412,9 @@ abstract class grade_report { * @param string $courseid the course id * @param string $course_item an instance of grade_item * @param string $finalgrade the grade for the course_item - * @return string The new final grade + * @return array[] containing values for 'grade', 'grademax' and 'grademin' */ - protected function blank_hidden_total($courseid, $course_item, $finalgrade) { + protected function blank_hidden_total_and_adjust_bounds($courseid, $course_item, $finalgrade) { global $CFG, $DB; static $hiding_affected = null;//array of items in this course affected by hiding @@ -424,13 +424,16 @@ abstract class grade_report { // If we're dealing with multiple courses we need to know when we've moved on to a new course. static $previous_courseid = null; + $grademin = $course_item->grademin; + $grademax = $course_item->grademax; + if (!is_array($this->showtotalsifcontainhidden)) { debugging('showtotalsifcontainhidden should be an array', DEBUG_DEVELOPER); $this->showtotalsifcontainhidden = array($courseid => $this->showtotalsifcontainhidden); } if ($this->showtotalsifcontainhidden[$courseid] == GRADE_REPORT_SHOW_REAL_TOTAL_IF_CONTAINS_HIDDEN) { - return $finalgrade; + return array('grade' => $finalgrade, 'grademin' => $grademin, 'grademax' => $grademax); } // If we've moved on to another course or user, reload the grades. @@ -473,6 +476,12 @@ abstract class grade_report { } else { //use reprocessed marks that exclude hidden items $finalgrade = $hiding_affected['altered'][$course_item->id]; + if (!empty($hiding_affected['alteredgrademin'][$course_item->id])) { + $grademin = $hiding_affected['alteredgrademin'][$course_item->id]; + } + if (!empty($hiding_affected['alteredgrademax'][$course_item->id])) { + $grademax = $hiding_affected['alteredgrademax'][$course_item->id]; + } } } else if (!empty($hiding_affected['unknown'][$course_item->id])) { //not sure whether or not this item depends on a hidden item @@ -482,10 +491,35 @@ abstract class grade_report { } else { //use reprocessed marks that exclude hidden items $finalgrade = $hiding_affected['unknown'][$course_item->id]; + + if (!empty($hiding_affected['alteredgrademin'][$course_item->id])) { + $grademin = $hiding_affected['alteredgrademin'][$course_item->id]; + } + if (!empty($hiding_affected['alteredgrademax'][$course_item->id])) { + $grademax = $hiding_affected['alteredgrademax'][$course_item->id]; + } } } - return $finalgrade; + return array('grade' => $finalgrade, 'grademin' => $grademin, 'grademax' => $grademax); + } + + /** + * Optionally blank out course/category totals if they contain any hidden items + * @deprecated since Moodle 2.8 - Call blank_hidden_total_and_adjust_bounds instead. + * @param string $courseid the course id + * @param string $course_item an instance of grade_item + * @param string $finalgrade the grade for the course_item + * @return string The new final grade + */ + protected function blank_hidden_total($courseid, $course_item, $finalgrade) { + // Note it is flawed to call this function directly because + // the aggregated grade does not make sense without the updated min and max information. + + debugging('grade_report::blank_hidden_total() is deprecated. + Call grade_report::blank_hidden_total_and_adjust_bounds instead.', DEBUG_DEVELOPER); + $result = $this->blank_hidden_total_and_adjust_bounds($courseid, $course_item, $finalgrade); + return $result['grade']; } } diff --git a/grade/report/overview/lib.php b/grade/report/overview/lib.php index 8c279f7a891..90ca93def3e 100644 --- a/grade/report/overview/lib.php +++ b/grade/report/overview/lib.php @@ -169,7 +169,15 @@ class grade_report_overview extends grade_report { if ($course_grade->is_hidden()) { $finalgrade = null; } else { - $finalgrade = $this->blank_hidden_total($course->id, $course_item, $finalgrade); + $adjustedgrade = $this->blank_hidden_total_and_adjust_bounds($course->id, + $course_item, + $finalgrade); + + // We temporarily adjust the view of this grade item - because the min and + // max are affected by the hidden values in the aggregation. + $finalgrade = $adjustedgrade['grade']; + $course_item->grademax = $adjustedgrade['grademax']; + $course_item->grademin = $adjustedgrade['grademin']; } } diff --git a/grade/report/user/lib.php b/grade/report/user/lib.php index bdbc83cc90d..930a69375aa 100644 --- a/grade/report/user/lib.php +++ b/grade/report/user/lib.php @@ -435,8 +435,17 @@ class grade_report_user extends grade_report { $data['grade']['content'] = '-'; } else { $data['grade']['class'] = $class; - $gradeval = $this->blank_hidden_total($this->courseid, $grade_grade->grade_item, $gradeval); - $data['grade']['content'] = grade_format_gradevalue($gradeval, $grade_grade->grade_item, true); + $adjustedgrade = $this->blank_hidden_total_and_adjust_bounds($this->courseid, + $grade_grade->grade_item, + $gradeval); + + // We temporarily adjust the view of this grade item - because the min and + // max are affected by the hidden values in the aggregation. + $grade_grade->grade_item->grademax = $adjustedgrade['grademax']; + $grade_grade->grade_item->grademin = $adjustedgrade['grademin']; + $data['grade']['content'] = grade_format_gradevalue($adjustedgrade['grade'], + $grade_grade->grade_item, + true); } $data['grade']['headers'] = "$header_cat $header_row grade"; } diff --git a/grade/tests/reportlib_test.php b/grade/tests/reportlib_test.php index dac85f9da31..4ff5240a79d 100644 --- a/grade/tests/reportlib_test.php +++ b/grade/tests/reportlib_test.php @@ -39,10 +39,10 @@ class grade_report_test extends grade_report { } /** - * A wrapper around blank_hidden_total() to allow test code to call it directly + * A wrapper around blank_hidden_total_and_adjust_bounds() to allow test code to call it directly */ - public function blank_hidden_total($courseid, $courseitem, $finalgrade) { - return parent::blank_hidden_total($courseid, $courseitem, $finalgrade); + public function blank_hidden_total_and_adjust_bounds($courseid, $courseitem, $finalgrade) { + return parent::blank_hidden_total_and_adjust_bounds($courseid, $courseitem, $finalgrade); } /** @@ -64,9 +64,9 @@ class grade_report_test extends grade_report { class core_grade_reportlib_testcase extends advanced_testcase { /** - * Tests grade_report::blank_hidden_total() + * Tests grade_report::blank_hidden_total_and_adjust_bounds() */ - public function test_blank_hidden_total() { + public function test_blank_hidden_total_and_adjust_bounds() { global $DB; $this->resetAfterTest(true); @@ -121,15 +121,24 @@ class core_grade_reportlib_testcase extends advanced_testcase { // Should return the supplied student total grade regardless of hiding. $report->showtotalsifcontainhidden = array($course->id => GRADE_REPORT_SHOW_REAL_TOTAL_IF_CONTAINS_HIDDEN); - $this->assertEquals($datagrade + $forumgrade, $report->blank_hidden_total($course->id, $coursegradeitem, $datagrade + $forumgrade)); + $result = $report->blank_hidden_total_and_adjust_bounds($course->id, $coursegradeitem, $datagrade + $forumgrade); + $this->assertEquals(array('grade' => $datagrade + $forumgrade, + 'grademax' => $coursegradeitem->grademax, + 'grademin' => $coursegradeitem->grademin), $result); // Should blank the student total as course grade depends on a hidden item. $report->showtotalsifcontainhidden = array($course->id => GRADE_REPORT_HIDE_TOTAL_IF_CONTAINS_HIDDEN); - $this->assertEquals(null, $report->blank_hidden_total($course->id, $coursegradeitem, $datagrade + $forumgrade)); + $result = $report->blank_hidden_total_and_adjust_bounds($course->id, $coursegradeitem, $datagrade + $forumgrade); + $this->assertEquals(array('grade' => null, + 'grademax' => $coursegradeitem->grademax, + 'grademin' => $coursegradeitem->grademin), $result); // Should return the course total minus the hidden database activity grade. $report->showtotalsifcontainhidden = array($course->id => GRADE_REPORT_SHOW_TOTAL_IF_CONTAINS_HIDDEN); - $this->assertEquals($forumgrade, $report->blank_hidden_total($course->id, $coursegradeitem, $datagrade + $forumgrade)); + $result = $report->blank_hidden_total_and_adjust_bounds($course->id, $coursegradeitem, $datagrade + $forumgrade); + $this->assertEquals(array('grade' => $forumgrade, + 'grademax' => $coursegradeitem->grademax, + 'grademin' => $coursegradeitem->grademin), $result); // Note: we cannot simply hide modules and call $report->blank_hidden_total() again. // It stores grades in a static variable so $report->blank_hidden_total() will return incorrect totals @@ -183,15 +192,24 @@ class core_grade_reportlib_testcase extends advanced_testcase { // Should return the supplied student total grade regardless of hiding. $report->showtotalsifcontainhidden = array($course->id => GRADE_REPORT_SHOW_REAL_TOTAL_IF_CONTAINS_HIDDEN); - $this->assertEquals($datagrade + $forumgrade, $report->blank_hidden_total($course->id, $coursegradeitem, $datagrade + $forumgrade)); + $result = $report->blank_hidden_total_and_adjust_bounds($course->id, $coursegradeitem, $datagrade + $forumgrade); + $this->assertEquals(array('grade' => $datagrade + $forumgrade, + 'grademax' => $coursegradeitem->grademax, + 'grademin' => $coursegradeitem->grademin), $result); // Should blank the student total as course grade depends on a hidden item. $report->showtotalsifcontainhidden = array($course->id => GRADE_REPORT_HIDE_TOTAL_IF_CONTAINS_HIDDEN); - $this->assertEquals(null, $report->blank_hidden_total($course->id, $coursegradeitem, $datagrade + $forumgrade)); + $result = $report->blank_hidden_total_and_adjust_bounds($course->id, $coursegradeitem, $datagrade + $forumgrade); + $this->assertEquals(array('grade' => null, + 'grademax' => $coursegradeitem->grademax, + 'grademin' => $coursegradeitem->grademin), $result); // Should return the course total minus the hidden activity grades. // They are both hidden so should return null. $report->showtotalsifcontainhidden = array($course->id => GRADE_REPORT_SHOW_TOTAL_IF_CONTAINS_HIDDEN); - $this->assertEquals(null, $report->blank_hidden_total($course->id, $coursegradeitem, $datagrade + $forumgrade)); + $result = $report->blank_hidden_total_and_adjust_bounds($course->id, $coursegradeitem, $datagrade + $forumgrade); + $this->assertEquals(array('grade' => null, + 'grademax' => $coursegradeitem->grademax, + 'grademin' => $coursegradeitem->grademin), $result); } } diff --git a/lib/grade/grade_category.php b/lib/grade/grade_category.php index d5a9e6d72fa..6c23d3188f1 100644 --- a/lib/grade/grade_category.php +++ b/lib/grade/grade_category.php @@ -612,10 +612,11 @@ class grade_category extends grade_object { } // do the maths - $agg_grade = $this->aggregate_values($grade_values, $items); + $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items); + $agg_grade = $result['grade']; // recalculate the grade back to requested range - $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax); + $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $result['grademin'], $result['grademax']); $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade); @@ -628,15 +629,22 @@ class grade_category extends grade_object { } /** - * Internal function that calculates the aggregated grade for this grade category + * Internal function that calculates the aggregated grade and new min/max for this grade category * * Must be public as it is used by grade_grade::get_hiding_affected() * * @param array $grade_values An array of values to be aggregated * @param array $items The array of grade_items - * @return float The aggregate grade for this grade category + * @return array containing values for: + * 'grade' => the new calculated grade + * 'grademin' => the new calculated min grade for the category + * 'grademax' => the new calculated max grade for the category */ - public function aggregate_values($grade_values, $items) { + public function aggregate_values_and_adjust_bounds($grade_values, $items) { + $category_item = $this->get_grade_item(); + $grademin = $category_item->grademin; + $grademax = $category_item->grademax; + switch ($this->aggregation) { case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies @@ -752,6 +760,19 @@ class grade_category extends grade_object { } break; + case GRADE_AGGREGATE_SUM: // Add up all the items. + $num = count($grade_values); + $sum = array_sum($grade_values); + $agg_grade = $sum / $num; + // Excluded items can affect the grademax for this grade_item. + $grademin = 0; + $grademax = 0; + foreach ($grade_values as $itemid => $grade_value) { + $grademin += $items[$itemid]->grademin; + $grademax += $items[$itemid]->grademax; + } + break; + case GRADE_AGGREGATE_MEAN: // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum) default: $num = count($grade_values); @@ -760,7 +781,24 @@ class grade_category extends grade_object { break; } - return $agg_grade; + return array('grade' => $agg_grade, 'grademin' => $grademin, 'grademax' => $grademax); + } + + /** + * Internal function that calculates the aggregated grade for this grade category + * + * Must be public as it is used by grade_grade::get_hiding_affected() + * + * @deprecated since Moodle 2.8 + * @param array $grade_values An array of values to be aggregated + * @param array $items The array of grade_items + * @return float The aggregate grade for this grade category + */ + public function aggregate_values($grade_values, $items) { + debugging('grade_category::aggregate_values() is deprecated. + Call grade_category::aggregate_values_and_adjust_bounds() instead.', DEBUG_DEVELOPER); + $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items); + return $result['grade']; } /** diff --git a/lib/grade/grade_grade.php b/lib/grade/grade_grade.php index 8763fdd3a7a..fcc4098c9a3 100644 --- a/lib/grade/grade_grade.php +++ b/lib/grade/grade_grade.php @@ -572,7 +572,10 @@ class grade_grade extends grade_object { * * @param array $grade_grades all course grades of one user, & used for better internal caching * @param array $grade_items array of grade items, & used for better internal caching - * @return array + * @return array This is an array of 3 arrays: + * unknown => list of item ids that may be affected by hiding (with the calculated grade as the value) + * altered => list of item ids that are definitely affected by hiding (with the calculated grade as the value) + * alteredgrademax => for each item in altered or unknown, the new value of the grademax */ public static function get_hiding_affected(&$grade_grades, &$grade_items) { global $CFG; @@ -585,6 +588,8 @@ class grade_grade extends grade_object { $todo = array(); $unknown = array(); // can not find altered $altered = array(); // altered grades + $alteredgrademax = array(); // Altered grade max values. + $alteredgrademin = array(); // Altered grade min values. $hiddenfound = false; foreach($grade_grades as $itemid=>$unused) { @@ -604,7 +609,10 @@ class grade_grade extends grade_object { } } if (!$hiddenfound) { - return array('unknown'=>array(), 'altered'=>array()); + return array('unknown' => array(), + 'altered' => array(), + 'alteredgrademax' => array(), + 'alteredgrademin' => array()); } $max = count($todo); @@ -646,6 +654,7 @@ class grade_grade extends grade_object { if (array_key_exists($itemid, $altered)) { //nulling an altered precursor $values[$itemid] = $altered[$itemid]; + unset($values[$itemid]); } elseif (empty($values[$itemid])) { $values[$itemid] = $grade_grades[$itemid]->finalgrade; } @@ -686,12 +695,18 @@ class grade_grade extends grade_object { continue; } - $agg_grade = $grade_category->aggregate_values($values, $grade_items); + $adjustedgrade = $grade_category->aggregate_values_and_adjust_bounds($values, $grade_items); // recalculate the rawgrade back to requested range - $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $grade_items[$do]->grademin, $grade_items[$do]->grademax); + $finalgrade = grade_grade::standardise_score($adjustedgrade['grade'], + 0, + 1, + $adjustedgrade['grademin'], + $adjustedgrade['grademax']); $finalgrade = $grade_items[$do]->bounded_grade($finalgrade); + $alteredgrademin[$do] = $adjustedgrade['grademin']; + $alteredgrademax[$do] = $adjustedgrade['grademax']; $altered[$do] = $finalgrade; unset($todo[$key]); @@ -706,7 +721,10 @@ class grade_grade extends grade_object { } } - return array('unknown'=>$unknown, 'altered'=>$altered); + return array('unknown' => $unknown, + 'altered' => $altered, + 'alteredgrademax' => $alteredgrademax, + 'alteredgrademin' => $alteredgrademin); } /**