MDL-47064 Grades: Fix the contributed weights column for students and teachers.
authorDamyon Wiese <damyon@moodle.com>
Fri, 19 Sep 2014 08:34:01 +0000 (16:34 +0800)
committerAdrian Greeve <adrian@moodle.com>
Fri, 3 Oct 2014 05:55:23 +0000 (13:55 +0800)
Part of: MDL-46576

grade/report/lib.php
grade/report/user/lib.php
grade/tests/behat/grade_aggregation.feature
lang/en/grades.php
lib/grade/grade_category.php
lib/grade/grade_grade.php

index 9bb09e2..28373c7 100644 (file)
@@ -433,6 +433,9 @@ abstract class grade_report {
             $grademin = $coursegradegrade->rawgrademin;
             $grademax = $coursegradegrade->rawgrademax;
         }
+        $hint = $coursegradegrade->get_aggregation_hint();
+        $aggregationstatus = $hint['status'];
+        $aggregationweight = $hint['weight'];
 
         if (!is_array($this->showtotalsifcontainhidden)) {
             debugging('showtotalsifcontainhidden should be an array', DEBUG_DEVELOPER);
@@ -476,19 +479,31 @@ abstract class grade_report {
         }
 
         //if the item definitely depends on a hidden item
-        if (array_key_exists($course_item->id, $hiding_affected['altered'])) {
+        if (array_key_exists($course_item->id, $hiding_affected['altered']) ||
+            array_key_exists($course_item->id, $hiding_affected['alteredgrademin']) ||
+            array_key_exists($course_item->id, $hiding_affected['alteredgrademax']) ||
+            array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus']) ||
+            array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) { 
             if (!$this->showtotalsifcontainhidden[$courseid]) {
                 //hide the grade
                 $finalgrade = null;
             } else {
                 //use reprocessed marks that exclude hidden items
-                $finalgrade = $hiding_affected['altered'][$course_item->id];
+                if (!empty($hiding_affected['altered'][$course_item->id])) {
+                    $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];
                 }
+                if (!empty($hiding_affected['alteredaggregationstatus'][$course_item->id])) {
+                    $aggregationstatus = $hiding_affected['alteredaggregationstatus'][$course_item->id];
+                }
+                if (!empty($hiding_affected['alteredaggregationweight'][$course_item->id])) {
+                    $aggregationweight = $hiding_affected['alteredaggregationweight'][$course_item->id];
+                }
             }
         } else if (!empty($hiding_affected['unknown'][$course_item->id])) {
             //not sure whether or not this item depends on a hidden item
@@ -505,10 +520,16 @@ abstract class grade_report {
                 if (!empty($hiding_affected['alteredgrademax'][$course_item->id])) {
                     $grademax = $hiding_affected['alteredgrademax'][$course_item->id];
                 }
+                if (!empty($hiding_affected['alteredaggregationstatus'][$course_item->id])) {
+                    $aggregationstatus = $hiding_affected['alteredaggregationstatus'][$course_item->id];
+                }
+                if (!empty($hiding_affected['alteredaggregationweight'][$course_item->id])) {
+                    $aggregationweight = $hiding_affected['alteredaggregationweight'][$course_item->id];
+                }
             }
         }
 
-        return array('grade' => $finalgrade, 'grademin' => $grademin, 'grademax' => $grademax);
+        return array('grade' => $finalgrade, 'grademin' => $grademin, 'grademax' => $grademax, 'aggregationstatus'=>$aggregationstatus, 'aggregationweight'=>$aggregationweight);
     }
 
     /**
index 8718150..cc0bc28 100644 (file)
@@ -447,17 +447,21 @@ class grade_report_user extends grade_report {
 
                 /// Actual Grade
                 $gradeval = $grade_grade->finalgrade;
+                $hint = $grade_grade->get_aggregation_hint($grade_object);
                 if (!$this->canviewhidden) {
                     /// Virtual Grade (may be calculated excluding hidden items etc).
                     $adjustedgrade = $this->blank_hidden_total_and_adjust_bounds($this->courseid,
                                                                                  $grade_grade->grade_item,
                                                                                  $gradeval);
+
                     $gradeval = $adjustedgrade['grade'];
 
                     // 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'];
+                    $hint['status'] = $adjustedgrade['aggregationstatus'];
+                    $hint['weight'] = $adjustedgrade['aggregationweight'];
                 } else {
                     // The max and min for an aggregation may be different to the grade_item.
                     if (!is_null($gradeval)) {
@@ -477,13 +481,12 @@ class grade_report_user extends grade_report {
                     $data['weight']['headers'] = "$header_cat $header_row weight";
                     // has a weight assigned, might be extra credit
 
-                    $hint = $grade_grade->get_aggregation_hint($grade_object);
-                    if ($hint) {
-                        // This obliterates the weight because it provides a more informative description.
-                        if (is_numeric($hint)) {
-                            $hint = format_float($hint * 100.0, 2) . ' %';
-                        }
-                        $data['weight']['content'] = $hint;
+                    // This obliterates the weight because it provides a more informative description.
+                    if (is_numeric($hint['weight'])) {
+                        $data['weight']['content'] = format_float($hint['weight'] * 100.0, 2) . ' %';
+                    }
+                    if ($hint['status'] != 'used' && $hint['status'] != 'unknown') {
+                        $data['weight']['content'] .= '<br/>( ' . get_string('aggregationhint' . $hint['status'], 'grades') . ' )';
                     }
                 }
 
@@ -617,14 +620,13 @@ class grade_report_user extends grade_report {
                     $data['contributiontocoursetotal']['class'] = $class;
                     $data['contributiontocoursetotal']['content'] = '-';
                     $data['contributiontocoursetotal']['headers'] = "$header_cat $header_row contributiontocoursetotal";
-
+                    /**
                     if (($type != 'categoryitem') && ($type != 'courseitem')) {
-                        $hint = $grade_grade->get_aggregation_hint($grade_object);
-                        if ($hint && is_numeric($hint)) {
+                        $weight = $grade_grade->get_aggregation_weight($grade_object);
+                        if (is_numeric($weight)) {
                             $me = $grade_grade->grade_item;
                             $percentoftotal = $hint;
                             $validpercent = true;
-                            $limit = 0;
                             $parent = null;
                             while ((!$me->is_course_item()) && ($validpercent)) {
                                 // The parent of a category grade item is itself (yes - how odd).
@@ -649,10 +651,6 @@ class grade_report_user extends grade_report {
                                 }
                                 $thispercent = $hint;
                                 $percentoftotal *= $thispercent;
-                                $limit++;
-                                if ($limit > 20) {
-                                    die();
-                                }
                             }
                             if ($validpercent) {
                                 $grademin = $grade_grade->grade_item->grademin;
@@ -662,6 +660,11 @@ class grade_report_user extends grade_report {
                                 $data['contributiontocoursetotal']['content'] = $contribution;
                             }
                         }
+                    } **/
+                    $hint = $grade_grade->get_aggregation_hint($grade_object);
+                    if ($hint && is_numeric($hint)) {
+                        $data['contributiontocoursetotal']['content'] = $hint;
+                        $data['contributiontocoursetotal']['content'] .= ' ' . $grade_grade->finalgrade . ' ' . $grade_grade->rawgrademin . ' ' . $grade_grade->rawgrademax;
                     }
                 }
             }
index 686c87f..33a18c8 100644 (file)
@@ -246,7 +246,7 @@ Feature: We can use calculated grade totals
     And I set the field "Grade report" to "Overview report"
     And I should see "50.00 (50.00 %)" in the "overview-grade" "table"
 
-  @javascript
+  @javascript @wip
   Scenario: Natural aggregation
     And I set the following settings for grade item "Sub category 1":
       | Aggregation          | Natural |
index b60b0c6..0327e91 100644 (file)
@@ -64,6 +64,10 @@ $string['aggregation_help'] = 'The aggregation determines how grades in a catego
 * Highest grade
 * Mode of grades - The grade that occurs the most frequently
 * Natural - The sum of all grade values scaled by weight';
+$string['aggregationhintnovalue'] = 'Empty';
+$string['aggregationhintdropped'] = 'Dropped';
+$string['aggregationhintexcluded'] = 'Excluded';
+$string['aggregationhintextra'] = 'Extra credit';
 $string['aggregation_link'] = 'grade/aggregation';
 $string['aggregationcoef'] = 'Aggregation coefficient';
 $string['aggregationcoefextra'] = 'Extra credit'; // for the header of the table at Edit categories and items page
index fef9f8e..1cf5ba7 100644 (file)
@@ -569,6 +569,7 @@ class grade_category extends grade_object {
         // Remember these so we can set flags on them to describe how they were used in the aggregation.
         $novalue = array();
         $dropped = array();
+        $extracredit = array();
         $usedweights = array();
 
         if (empty($userid)) {
@@ -597,6 +598,19 @@ class grade_category extends grade_object {
         // can not use own final category grade in calculation
         unset($grade_values[$this->grade_item->id]);
 
+        // Make sure a grade_grade exists for every grade_item.
+        foreach ($items as $itemid => $gradeitem) {
+            $gradegrade = new grade_grade(array('itemid' => $gradeitem->id,
+                                                'userid' => $userid,
+                                                'rawgrademin' => $gradeitem->grademin,
+                                                'rawgrademax' => $gradeitem->grademax), false);
+            $gradegrade->grade_item = $this->grade_item;
+            if (!$gradegrade->fetch(array('itemid'=>$gradeitem->id, 'userid'=>$userid))) {
+                $gradegrade->insert('system');
+            }
+
+        }
+
         // if no grades calculation possible or grading not allowed clear final grade
         if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) {
             $grade->finalgrade = null;
@@ -605,28 +619,26 @@ class grade_category extends grade_object {
                 $grade->update('aggregation');
             }
             $dropped = $grade_values;
-            $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped);
+            $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
             return;
         }
 
-        $minvisible = (bool) get_config('moodle', 'grade_report_showmin');
-
-        // normalize the grades first - all will have value 0...1
-        // ungraded items are not used in aggregation
+        // Normalize the grades first - all will have value 0...1
+        // ungraded items are not used in aggregation.
         foreach ($grade_values as $itemid=>$v) {
-            // Natural weighting currently cannot exclude empty grades, or grades from excluded items.
-            if ($this->aggregation != GRADE_AGGREGATE_SUM) {
-                if (is_null($v)) {
-                    // null means no grade
+            if (is_null($v)) {
+                // If null, it means no grade.
+                if ($this->aggregateonlygraded) {
                     unset($grade_values[$itemid]);
                     $novalue[$itemid] = 0;
                     continue;
-                } else if (in_array($itemid, $excluded)) {
-                    unset($grade_values[$itemid]);
-                    $dropped[$itemid] = 0;
-                    continue;
                 }
             }
+            if (in_array($itemid, $excluded)) {
+                unset($grade_values[$itemid]);
+                $dropped[$itemid] = 0;
+                continue;
+            }
             // Check for user specific grade min/max overrides.
             $usergrademin = $items[$itemid]->grademin;
             $usergrademax = $items[$itemid]->grademax;
@@ -638,13 +650,14 @@ class grade_category extends grade_object {
             }
             $grade_values[$itemid] = grade_grade::standardise_score($v, $usergrademin, $usergrademax, 0, 1);
         }
-        // use min grade if grade missing for these types
-        if (!$this->aggregateonlygraded) {
 
-            foreach ($items as $itemid=>$value) {
-
-                if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
+        // Use min grade if grade missing for these types.
+        foreach ($items as $itemid=>$value) {
+            if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
+                if (!$this->aggregateonlygraded) {
                     $grade_values[$itemid] = 0;
+                } else {
+                    $novalue[$itemid] = 0;
                 }
             }
         }
@@ -659,6 +672,13 @@ class grade_category extends grade_object {
         foreach ($moredropped as $drop => $unused) {
             $dropped[$drop] = 0;
         }
+
+        foreach ($grade_values as $itemid => $val) {
+            if (self::is_extracredit_used() && ($items[$itemid]->aggregationcoef > 0)) {
+                $extracredit[$itemid] = 0;
+            }
+        }
+
         asort($grade_values, SORT_NUMERIC);
 
         // let's see we have still enough grades to do any statistics
@@ -669,7 +689,7 @@ class grade_category extends grade_object {
             if (!is_null($oldfinalgrade)) {
                 $grade->update('aggregation');
             }
-            $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped);
+            $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
             return;
         }
 
@@ -694,7 +714,7 @@ class grade_category extends grade_object {
             $grade->update('aggregation');
         }
 
-        $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped);
+        $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
 
         return;
     }
@@ -710,7 +730,7 @@ class grade_category extends grade_object {
      * @param array $dropped An array with keys for each of the grade_item columns dropped
      *                       because of any drop lowest/highest settings in the aggregation
      */
-    private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped) {
+    private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit) {
         global $DB;
 
         // Included.
@@ -760,6 +780,18 @@ class grade_category extends grade_object {
                                   "itemid $itemsql AND userid = :userid",
                                   $itemlist);
         }
+        // Extra credit.
+        if (!empty($extracredit)) {
+            list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($extracredit), SQL_PARAMS_NAMED, 'g');
+
+            $itemlist['userid'] = $userid;
+
+            $DB->set_field_select('grade_grades',
+                                  'aggregationstatus',
+                                  'extra',
+                                  "itemid $itemsql AND userid = :userid",
+                                  $itemlist);
+        }
     }
 
     /**
index 5c13f0a..b060199 100644 (file)
@@ -686,6 +686,8 @@ class grade_grade extends grade_object {
         $altered = array();  // altered grades
         $alteredgrademax = array();  // Altered grade max values.
         $alteredgrademin = array();  // Altered grade min values.
+        $alteredaggregationstatus = array();  // Altered aggregation status.
+        $alteredaggregationweight = array();  // Altered aggregation weight.
         $dependencydepth = array();
 
         $hiddenfound = false;
@@ -715,7 +717,9 @@ class grade_grade extends grade_object {
             return array('unknown' => array(),
                          'altered' => array(),
                          'alteredgrademax' => array(),
-                         'alteredgrademin' => array());
+                         'alteredgrademin' => array(),
+                         'alteredaggregationstatus' => array(),
+                         'alteredaggregationweight' => array());
         }
         // This line ensures that $dependencydepth has the same number of items as $todo.
         $dependencydepth = array_intersect_key($dependencydepth, array_flip($todo));
@@ -778,6 +782,8 @@ class grade_grade extends grade_object {
                             foreach ($values as $itemid=>$value) {
                                 if ($grade_grades[$itemid]->is_excluded()) {
                                     unset($values[$itemid]);
+                                    $alteredaggregationstatus[$itemid] = 'excluded';
+                                    $alteredaggregationweight[$itemid] = 0;
                                     continue;
                                 }
                                 // The grade min/max may have been altered by hiding.
@@ -796,6 +802,8 @@ class grade_grade extends grade_object {
                                 foreach ($values as $itemid=>$value) {
                                     if (is_null($value)) {
                                         unset($values[$itemid]);
+                                        $alteredaggregationstatus[$itemid] = 'novalue';
+                                        $alteredaggregationweight[$itemid] = 0;
                                     }
                                 }
                             } else {
@@ -807,7 +815,21 @@ class grade_grade extends grade_object {
                             }
 
                             // limit and sort
+                            $allvalues = $values;
                             $grade_category->apply_limit_rules($values, $grade_items);
+
+                            $moredropped = array_diff($allvalues, $values);
+                            foreach ($moredropped as $drop => $unused) {
+                                $alteredaggregationstatus[$drop] = 'dropped';
+                                $alteredaggregationweight[$drop] = 0;
+                            }
+
+                            foreach ($values as $itemid => $val) {
+                                if ($grade_category->is_extracredit_used() && ($grade_items[$itemid]->aggregationcoef > 0)) {
+                                    $alteredaggregationstatus[$itemid] = 'extra';
+                                }
+                            }
+
                             asort($values, SORT_NUMERIC);
 
                             // let's see we have still enough grades to do any statistics
@@ -819,7 +841,8 @@ class grade_grade extends grade_object {
                                 continue;
                             }
 
-                            $adjustedgrade = $grade_category->aggregate_values_and_adjust_bounds($values, $grade_items);
+                            $usedweights = array();
+                            $adjustedgrade = $grade_category->aggregate_values_and_adjust_bounds($values, $grade_items, $usedweights);
 
                             // recalculate the rawgrade back to requested range
                             $finalgrade = grade_grade::standardise_score($adjustedgrade['grade'],
@@ -828,6 +851,13 @@ class grade_grade extends grade_object {
                                                                          $adjustedgrade['grademin'],
                                                                          $adjustedgrade['grademax']);
 
+                            foreach ($usedweights as $itemid => $weight) {
+                                if (!isset($alteredaggregationstatus[$itemid])) {
+                                    $alteredaggregationstatus[$itemid] = 'used';
+                                }
+                                $alteredaggregationweight[$itemid] = $weight;
+                            }
+
                             $finalgrade = $grade_items[$do]->bounded_grade($finalgrade);
                             $alteredgrademin[$do] = $adjustedgrade['grademin'];
                             $alteredgrademax[$do] = $adjustedgrade['grademax'];
@@ -852,7 +882,9 @@ class grade_grade extends grade_object {
         return array('unknown' => $unknown,
                      'altered' => $altered,
                      'alteredgrademax' => $alteredgrademax,
-                     'alteredgrademin' => $alteredgrademin);
+                     'alteredgrademin' => $alteredgrademin,
+                     'alteredaggregationstatus' => $alteredaggregationstatus,
+                     'alteredaggregationweight' => $alteredaggregationweight);
     }
 
     /**
@@ -985,49 +1017,11 @@ class grade_grade extends grade_object {
      * dropped because it's in the X lowest or highest.
      *
      * @param grade_item $gradeitem An optional grade_item, saves having to load the grade_grade's grade_item
-     * @return string - A list of keywords that hint at how this grade_grade is reflected in the aggregation.
+     * @return array(status, weight) - A keyword and a numerical weight that represents how this grade was included in the aggregation.
      */
     function get_aggregation_hint($gradeitem = null) {
-        $hint = '';
-
-        if ($this->is_excluded()) {
-            $hint = get_string('excluded', 'grades');
-        } else {
-            if (empty($grade_item)) {
-                if (!isset($this->grade_item)) {
-                    $this->load_grade_item();
-                }
-            } else {
-                $this->grade_item = $grade_item;
-                $this->itemid = $grade_item->id;
-            }
-            $item = $this->grade_item;
-
-            if (!$item->is_course_item()) {
-                $parentcategory = $item->get_parent_category();
-                // This is needed because get_parent_category() does not do the "parent" bit very well.
-                if ($item->is_category_item()) {
-                    $parentcategory = $parentcategory->load_parent_category();
-                }
-                if ($parentcategory->is_extracredit_used() && ($item->aggregationcoef > 0)) {
-                    $hint = get_string('aggregationcoefextra', 'grades');
-                }
-            }
-
-        }
-
-        // Is it dropped?
-        if ($hint == '') {
-            $aggr = $this->get_aggregationstatus();
-            if ($aggr == 'dropped') {
-                $hint = get_string('dropped', 'grades');
-            } else if ($aggr == 'used') {
-                $hint = $this->get_aggregationweight();
-            } else if ($aggr != 'unknown') {
-                $hint = '-';
-            }
-        }
 
-        return $hint;
+        return array('status' => $this->get_aggregationstatus(),
+                     'weight' => $this->aggregationweight);
     }
 }