} else if (($is_course or $is_category) and ($is_scale or $is_value)) {
if ($category = $element['object']->get_item_category()) {
+ $aggrstrings = grade_helper::get_aggregation_strings();
+ $stragg = $aggrstrings[$category->aggregation];
switch ($category->aggregation) {
case GRADE_AGGREGATE_MEAN:
case GRADE_AGGREGATE_MEDIAN:
case GRADE_AGGREGATE_WEIGHTED_MEAN:
case GRADE_AGGREGATE_WEIGHTED_MEAN2:
case GRADE_AGGREGATE_EXTRACREDIT_MEAN:
- $stragg = get_string('aggregation', 'grades');
return '<img src="'.$OUTPUT->pix_url('i/agg_mean') . '" ' .
'class="icon itemicon" title="'.s($stragg).'" alt="'.s($stragg).'"/>';
case GRADE_AGGREGATE_SUM:
- $stragg = get_string('aggregation', 'grades');
return '<img src="'.$OUTPUT->pix_url('i/agg_sum') . '" ' .
'class="icon itemicon" title="'.s($stragg).'" alt="'.s($stragg).'"/>';
}
* @var array
*/
protected static $pluginstrings = null;
+ /**
+ * Cached grade aggregation strings
+ * @var array
+ */
+ protected static $aggregationstrings = null;
/**
* Gets strings commonly used by the describe plugins
}
return self::$pluginstrings;
}
+
+ /**
+ * Gets strings describing the available aggregation methods.
+ *
+ * @return array
+ */
+ public static function get_aggregation_strings() {
+ if (self::$aggregationstrings === null) {
+ self::$aggregationstrings = array(
+ GRADE_AGGREGATE_MEAN => get_string('aggregatemean', 'grades'),
+ GRADE_AGGREGATE_WEIGHTED_MEAN => get_string('aggregateweightedmean', 'grades'),
+ GRADE_AGGREGATE_WEIGHTED_MEAN2 => get_string('aggregateweightedmean2', 'grades'),
+ GRADE_AGGREGATE_EXTRACREDIT_MEAN => get_string('aggregateextracreditmean', 'grades'),
+ GRADE_AGGREGATE_MEDIAN => get_string('aggregatemedian', 'grades'),
+ GRADE_AGGREGATE_MIN => get_string('aggregatemin', 'grades'),
+ GRADE_AGGREGATE_MAX => get_string('aggregatemax', 'grades'),
+ GRADE_AGGREGATE_MODE => get_string('aggregatemode', 'grades'),
+ GRADE_AGGREGATE_SUM => get_string('aggregatesum', 'grades')
+ );
+ }
+ return self::$aggregationstrings;
+ }
+
/**
* Get grade_plugin_info object for managing settings if the user can
*
*/
private function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded) {
global $CFG;
+
+ // Remember these so we can set flags on them to describe how they were used in the aggregation.
+ $novalue = array();
+ $dropped = array();
+ $usedweights = array();
+
if (empty($userid)) {
//ignore first call
return;
// can not use own final category grade in calculation
unset($grade_values[$this->grade_item->id]);
-
// sum is a special aggregation types - it adjusts the min max, does not use relative values
if ($this->aggregation == GRADE_AGGREGATE_SUM) {
- $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded);
+ $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded, $usedweights);
+ $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped);
return;
}
if (!is_null($oldfinalgrade)) {
$grade->update('aggregation');
}
+ $dropped = $grade_values;
+ $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped);
return;
}
if (is_null($v)) {
// null means no grade
unset($grade_values[$itemid]);
+ $novalue[$itemid] = 0;
continue;
} else if (in_array($itemid, $excluded)) {
unset($grade_values[$itemid]);
+ $dropped[$itemid] = 0;
continue;
}
// If grademin is hidden, set it to 0.
}
// limit and sort
+ $allvalues = $grade_values;
$this->apply_limit_rules($grade_values, $items);
+
+ $moredropped = array_diff($allvalues, $grade_values);
+ foreach ($moredropped as $drop => $unused) {
+ $dropped[$drop] = 0;
+ }
asort($grade_values, SORT_NUMERIC);
// let's see we have still enough grades to do any statistics
if (!is_null($oldfinalgrade)) {
$grade->update('aggregation');
}
+ $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped);
return;
}
// do the maths
- $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items);
+ $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items, $usedweights);
$agg_grade = $result['grade'];
if (!$minvisible and $this->grade_item->gradetype != GRADE_TYPE_SCALE) {
$grade->update('aggregation');
}
+ $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped);
+
return;
}
+ /**
+ * Set the flags on the grade_grade items to indicate how individual grades are used
+ * in the aggregation.
+ *
+ * @param int $userid The user we have aggregated the grades for.
+ * @param array $usedweights An array with keys for each of the grade_item columns included in the aggregation. The value are the relative weight.
+ * @param array $novalue An array with keys for each of the grade_item columns skipped because
+ * they had no value in the aggregation
+ * @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) {
+ global $DB;
+
+ // Included.
+ if (!empty($usedweights)) {
+ // The usedweights items are updated individually to record the weights.
+ foreach ($usedweights as $gradeitemid => $contribution) {
+ // Convert contribution to a 4 digit integer so there are no localization problems.
+ $contribution = intval($contribution * 10000);
+ $DB->set_field_select('grade_grades',
+ 'usedinaggregation',
+ $contribution,
+ "itemid = :itemid AND userid = :userid",
+ array('itemid'=>$gradeitemid, 'userid'=>$userid));
+ }
+ }
+
+ // No value.
+ if (!empty($novalue)) {
+ list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($novalue), SQL_PARAMS_NAMED, 'g');
+
+ $itemlist['userid'] = $userid;
+
+ $DB->set_field_select('grade_grades',
+ 'usedinaggregation',
+ 'novalue',
+ "itemid $itemsql AND userid = :userid",
+ $itemlist);
+ }
+
+ // Dropped.
+ if (!empty($dropped)) {
+ list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($dropped), SQL_PARAMS_NAMED, 'g');
+
+ $itemlist['userid'] = $userid;
+
+ $DB->set_field_select('grade_grades',
+ 'usedinaggregation',
+ 'dropped',
+ "itemid $itemsql AND userid = :userid",
+ $itemlist);
+ }
+ }
+
/**
* Internal function that calculates the aggregated grade and new min/max for this grade category
*
* @param array $grade_values An array of values to be aggregated
* @param array $items The array of grade_items
* @since Moodle 2.6.5, 2.7.2
+ * @param array & $weights If provided, will be filled with the normalized weights
+ * for each grade_item as used in the aggregation.
* @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_and_adjust_bounds($grade_values, $items) {
+ public function aggregate_values_and_adjust_bounds($grade_values, $items, & $weights = null) {
$category_item = $this->get_grade_item();
$grademin = $category_item->grademin;
$grademax = $category_item->grademax;
} else {
$agg_grade = $grades[intval(($num/2)-0.5)];
}
+
+ // Record the weights evenly.
+ if ($weights !== null && $num > 0) {
+ foreach ($grade_values as $itemid=>$grade_value) {
+ $weights[$itemid] = 1.0 / $num;
+ }
+ }
break;
case GRADE_AGGREGATE_MIN:
$agg_grade = reset($grade_values);
+ // Record the weights as used.
+ if ($weights !== null) {
+ foreach ($grade_values as $itemid=>$grade_value) {
+ $weights[$itemid] = 0;
+ }
+ }
+ // Set the first item to 1.
+ $itemids = array_keys($grade_values);
+ $weights[reset($itemids)] = 1;
break;
case GRADE_AGGREGATE_MAX:
- $agg_grade = array_pop($grade_values);
+ // Record the weights as used.
+ if ($weights !== null) {
+ foreach ($grade_values as $itemid=>$grade_value) {
+ $weights[$itemid] = 0;
+ }
+ }
+ // Set the last item to 1.
+ $itemids = array_keys($grade_values);
+ $weights[end($itemids)] = 1;
+ $agg_grade = end($grade_values);
break;
- case GRADE_AGGREGATE_MODE: // the most common value, average used if multimode
+ case GRADE_AGGREGATE_MODE: // the most common value
// array_count_values only counts INT and STRING, so if grades are floats we must convert them to string
$converted_grade_values = array();
} else {
$converted_grade_values[$k] = $gv;
}
+ if ($weights !== null) {
+ $weights[$k] = 0;
+ }
}
$freq = array_count_values($converted_grade_values);
$modes = array_keys($freq, $top); // search for all modes (have the same highest count)
rsort($modes, SORT_NUMERIC); // get highest mode
$agg_grade = reset($modes);
+ // Record the weights as used.
+ if ($weights !== null && $top > 0) {
+ foreach ($grade_values as $k => $gv) {
+ if ($gv == $agg_grade) {
+ $weights[$k] = 1.0 / $top;
+ }
+ }
+ }
break;
case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades, weight specified in coef
}
$weightsum += $items[$itemid]->aggregationcoef;
$sum += $items[$itemid]->aggregationcoef * $grade_value;
+ if ($weights !== null) {
+ $weights[$itemid] = $items[$itemid]->aggregationcoef;
+ }
}
-
if ($weightsum == 0) {
$agg_grade = null;
} else {
$agg_grade = $sum / $weightsum;
+ if ($weights !== null) {
+ // Normalise the weights.
+ foreach ($weights as $itemid => $weight) {
+ $weights[$itemid] = $weight / $weightsum;
+ }
+ }
+
}
break;
}
$sum += $weight * $grade_value;
}
-
if ($weightsum == 0) {
$agg_grade = $sum; // only extra credits
} else {
$agg_grade = $sum / $weightsum;
}
+ // Record the weights as used.
+ if ($weights !== null) {
+ foreach ($grade_values as $itemid=>$grade_value) {
+ if ($items[$itemid]->aggregationcoef == 0 && $weightsum > 0) {
+ $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
+ $weights[$itemid] = ($items[$itemid]->grademax - $items[$itemid]->grademin) / $weightsum;
+ } else {
+ $weights[$itemid] = 0;
+ }
+ }
+ }
break;
case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average
if ($items[$itemid]->aggregationcoef == 0) {
$num += 1;
$sum += $grade_value;
+ if ($weights !== null) {
+ $weights[$itemid] = 1;
+ }
} else if ($items[$itemid]->aggregationcoef > 0) {
$sum += $items[$itemid]->aggregationcoef * $grade_value;
+ if ($weights !== null) {
+ $weights[$itemid] = 0;
+ }
+ }
+ }
+ if ($weights !== null && $num > 0) {
+ foreach ($grade_values as $itemid=>$grade_value) {
+ if ($weights[$itemid]) {
+ $weights[$itemid] = 1.0 / $num;
+ }
}
}
$sum += $grade_value * ($items[$itemid]->grademax - $items[$itemid]->grademin);
$grademin += $items[$itemid]->grademin;
$grademax += $items[$itemid]->grademax;
+ if ($weights !== null && $num > 0) {
+ $weights[$itemid] = 1.0 / $num;
+ }
}
- $agg_grade = $sum / ($grademax - $grademin);
break;
case GRADE_AGGREGATE_MEAN: // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum)
$num = count($grade_values);
$sum = array_sum($grade_values);
$agg_grade = $sum / $num;
+ // Record the weights evenly.
+ if ($weights !== null && $num > 0) {
+ foreach ($grade_values as $itemid=>$grade_value) {
+ $weights[$itemid] = 1.0 / $num;
+ }
+ }
break;
}
* @param array $items Grade items
* @param array $grade_values Grade values
* @param array $excluded Excluded
+ * @param array & $weights For filling with the weights used in the aggregation.
*/
- private function sum_grades(&$grade, $oldfinalgrade, $items, $grade_values, $excluded) {
+ private function sum_grades(&$grade, $oldfinalgrade, $items, $grade_values, $excluded, & $weights = null) {
if (empty($items)) {
return null;
}
+ if ($weights) {
+ foreach ($grade_values as $itemid => $value) {
+ $weights[$itemid] = 0;
+ }
+ }
+
// ungraded and excluded items are not used in aggregation
foreach ($grade_values as $itemid=>$v) {
$sum = array_sum($grade_values);
$grade->finalgrade = $this->grade_item->bounded_grade($sum);
+ if ($weights !== null && $sum > 0) {
+ foreach ($grade_values as $itemid => $value) {
+ $weights[$itemid] = $value / $sum;
+ }
+ }
// update in db if changed
if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
*/
public $required_fields = array('id', 'itemid', 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin',
'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked',
- 'locktime', 'exported', 'overridden', 'excluded', 'timecreated', 'timemodified');
+ 'locktime', 'exported', 'overridden', 'excluded', 'timecreated',
+ 'timemodified', 'usedinaggregation');
/**
* Array of optional fields with default values (these should match db defaults)
*/
public $timemodified = null;
+ /**
+ * Used in aggregation flag. Can be one of 'unknown', 'dropped', 'novalue' or a specific weighting.
+ * @var string $usedinaggregation
+ */
+ public $usedinaggregation = 'unknown';
+
/**
* Returns array of grades for given grade_item+users
return $this->timecreated;
}
+ /**
+ * Returns the info on how this value was used in the aggregated grade
+ *
+ * @return string One of 'dropped', 'excluded', 'novalue' or a specific weighting
+ */
+ public function get_usedinaggregation() {
+ return $this->usedinaggregation;
+ }
+
+ /**
+ * Set usedinaggregation flag
+ *
+ * @param string $usedinaggregation
+ * @return void
+ */
+ public function set_usedinaggregation($usedinaggregation) {
+ $this->usedinaggregation = $usedinaggregation;
+ $this->update();
+ }
+
/**
* Returns timestamp when last graded, null if no grade present
*
// Pass information on to completion system
$completion->inform_grade_changed($cm, $this->grade_item, $this, $deleted);
- }
+ }
+
+ /**
+ * Get some useful information about how this grade_grade is reflected in the aggregation
+ * for the grade_category. For example this could be an extra credit item, and it could be
+ * 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.
+ */
+ 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()) {
+ $parent_category = $item->get_parent_category();
+ $parent_category->apply_forced_settings();
+ if ($parent_category->is_extracredit_used() && ($item->aggregationcoef > 0)) {
+ $hint = get_string('aggregationcoefextra', 'grades');
+ }
+ }
+
+ }
+
+ // Is it dropped?
+ if ($hint == '') {
+ $aggr = $this->get_usedinaggregation();
+ if ($aggr == 'dropped') {
+ $hint = get_string('dropped', 'grades');
+ } else if ($aggr == 'novalue') {
+ $hint = '-';
+ } else if ($aggr != 'unknown') {
+ $hint = $aggr;
+ }
+ }
+
+ return $hint;
+ }
}