MDL-46139 Grades: Add a column to grade_grades to record how a grade is aggregated

This commit is contained in:
Damyon Wiese 2014-08-07 16:33:09 +08:00 committed by Adrian Greeve
parent 2e19f5e8e7
commit bfe969e8b3
8 changed files with 311 additions and 35 deletions

View File

@ -37,15 +37,7 @@ class edit_category_form extends moodleform {
$category = $this->_customdata['current'];
$this->aggregation_options = 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'));
$this->aggregation_options = grade_helper::get_aggregation_strings();
// visible elements
$mform->addElement('header', 'headercategory', get_string('gradecategory', 'grades'));

View File

@ -661,15 +661,7 @@ class grade_edit_tree_column_aggregation extends grade_edit_tree_column_category
throw new Exception('Array key (id) missing from 3rd param of grade_edit_tree_column_aggregation::get_category_cell($category, $levelclass, $params)');
}
$options = 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'));
$options = grade_helper::get_aggregation_strings();
$visible = explode(',', $CFG->grade_aggregations_visible);
foreach ($options as $constant => $string) {
@ -751,7 +743,7 @@ class grade_edit_tree_column_weight extends grade_edit_tree_column {
public function get_header_cell() {
global $OUTPUT;
$headercell = clone($this->headercell);
$headercell->text = get_string('weightuc', 'grades').$OUTPUT->help_icon('aggregationcoefweight', 'grades');
$headercell->text = get_string('weights', 'grades').$OUTPUT->help_icon('aggregationcoefweight', 'grades');
return $headercell;
}

View File

@ -1171,17 +1171,17 @@ class grade_structure {
} 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).'"/>';
}
@ -2449,6 +2449,11 @@ abstract class grade_helper {
* @var array
*/
protected static $pluginstrings = null;
/**
* Cached grade aggregation strings
* @var array
*/
protected static $aggregationstrings = null;
/**
* Gets strings commonly used by the describe plugins
@ -2481,6 +2486,29 @@ abstract class grade_helper {
}
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
*

View File

@ -408,10 +408,12 @@ class grade_report_user extends grade_report {
if (!$hide) {
/// Excluded Item
/**
if ($grade_grade->is_excluded()) {
$fullname .= ' ['.get_string('excluded', 'grades').']';
$excluded = ' excluded';
}
**/
/// Other class information
$class = "$hidden $excluded";
@ -456,8 +458,14 @@ class grade_report_user extends grade_report {
$data['weight']['content'] = '-';
$data['weight']['headers'] = "$header_cat $header_row weight";
// has a weight assigned, might be extra credit
if ($grade_object->aggregationcoef > 0 && $type <> 'courseitem') {
$data['weight']['content'] = number_format($grade_object->aggregationcoef,2);
$hints = $grade_grade->get_aggregation_hint($grade_object);
if ($hints) {
// This obliterates the weight because it provides a more informative description.
if (intval($hints)) {
$hints = format_float(intval($hints) / 100.0, 2) . ' %';
}
$data['weight']['content'] = $hints;
}
}

1
lib/db/install.xml Normal file → Executable file
View File

@ -1764,6 +1764,7 @@
<FIELD NAME="informationformat" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="format of information text"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="the time this grade was first created"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="the time this grade was last modified"/>
<FIELD NAME="usedinaggregation" TYPE="char" LENGTH="10" NOTNULL="true" DEFAULT="unknown" SEQUENCE="false" COMMENT="One of several values describing how this grade_grade was used when calculating the aggregation. Possible values are &quot;unknown&quot;, &quot;dropped&quot;, &quot;novalue&quot;, &quot;included&quot;"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>

View File

@ -3719,6 +3719,21 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2014072400.01);
}
if ($oldversion < 2014080700.00) {
// Define field usedinaggregation to be added to grade_grades.
$table = new xmldb_table('grade_grades');
$field = new xmldb_field('usedinaggregation', XMLDB_TYPE_CHAR, '10', null, XMLDB_NOTNULL, null, 'unknown', 'timemodified');
// Conditionally launch add field usedinaggregation.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2014080700.00);
}
if ($oldversion < 2014080801.00) {
// Define index behaviour (not unique) to be added to question_attempts.

View File

@ -526,6 +526,12 @@ class grade_category extends grade_object {
*/
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;
@ -552,10 +558,10 @@ class grade_category extends grade_object {
// 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;
}
@ -566,6 +572,8 @@ class grade_category extends grade_object {
if (!is_null($oldfinalgrade)) {
$grade->update('aggregation');
}
$dropped = $grade_values;
$this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped);
return;
}
@ -578,10 +586,12 @@ class grade_category extends grade_object {
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.
@ -603,7 +613,13 @@ class grade_category extends grade_object {
}
// 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
@ -614,11 +630,12 @@ class grade_category extends grade_object {
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) {
@ -635,9 +652,66 @@ class grade_category extends grade_object {
$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
*
@ -646,12 +720,14 @@ class grade_category extends grade_object {
* @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;
@ -668,17 +744,42 @@ class grade_category extends grade_object {
} 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();
@ -690,6 +791,9 @@ class grade_category extends grade_object {
} else {
$converted_grade_values[$k] = $gv;
}
if ($weights !== null) {
$weights[$k] = 0;
}
}
$freq = array_count_values($converted_grade_values);
@ -698,6 +802,14 @@ class grade_category extends grade_object {
$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
@ -711,13 +823,22 @@ class grade_category extends grade_object {
}
$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;
@ -739,13 +860,23 @@ class grade_category extends grade_object {
}
$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
@ -757,9 +888,22 @@ class grade_category extends grade_object {
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;
}
}
}
@ -781,9 +925,11 @@ class grade_category extends grade_object {
$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)
@ -791,6 +937,12 @@ class grade_category extends grade_object {
$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;
}
@ -874,12 +1026,19 @@ class grade_category extends grade_object {
* @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) {
@ -906,6 +1065,11 @@ class grade_category extends grade_object {
$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)) {

View File

@ -49,7 +49,8 @@ class grade_grade extends grade_object {
*/
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)
@ -159,6 +160,12 @@ class grade_grade extends grade_object {
*/
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
@ -285,6 +292,26 @@ class grade_grade extends grade_object {
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
*
@ -923,5 +950,54 @@ class grade_grade extends grade_object {
// 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;
}
}