/** * This function is called after the table has been built and the aggregationhints * have been collected. We need this info to walk up the list of parents of each * grade_item. * * @param $element - An array containing the table data for the current row. */ public function fill_contributions_column($element) { // Recursively iterate through all child elements. if (isset($element['children'])) { foreach ($element['children'] as $key => $child) { $this->fill_contributions_column($element['children'][$key]); } } else { if ($element['type'] == 'item') { // This is a grade item (We don't do this for categories or we would double count). $grade_object = $element['object']; $itemid = $grade_object->id; // Ignore anything with no hint - e.g. a hidden row. if (isset($this->aggregationhints[$itemid])) { // Normalise the gradeval. $gradecat = $grade_object->load_parent_category(); if ($gradecat->aggregation == GRADE_AGGREGATE_SUM) { // Natural aggregation/Sum of grades does not consider the mingrade, cannot traditionnally normalise it. $graderange = $this->aggregationhints[$itemid]['grademax']; if ($graderange != 0) { $gradeval = $this->aggregationhints[$itemid]['grade'] / $graderange; } else { $gradeval = 0; } } else { $gradeval = grade_grade::standardise_score($this->aggregationhints[$itemid]['grade'], $this->aggregationhints[$itemid]['grademin'], $this->aggregationhints[$itemid]['grademax'], 0, 1); } // Multiply the normalised value by the weight // of all the categories higher in the tree. $parent = null; do { if (!is_null($this->aggregationhints[$itemid]['weight'])) { $gradeval *= $this->aggregationhints[$itemid]['weight']; } else { if (empty($parent)) { // If we are in the first loop, and the weight is null, then we cannot calculate the contribution. $gradeval = null; break; } } // The second part of this if is to prevent infinite loops // in case of crazy data. if (isset($this->aggregationhints[$itemid]['parent']) && $this->aggregationhints[$itemid]['parent'] != $itemid) { $parent = $this->aggregationhints[$itemid]['parent']; $itemid = $parent; } else { // We are at the top of the tree. $parent = false; } } while ($parent); // Finally multiply by the course grademax. if (!is_null($gradeval)) { // Convert to percent. $gradeval *= 100; } // Now we need to loop through the "built" table data and update the // contributions column for the current row. $header_row = "row_{$grade_object->id}_{$this->user->id}"; foreach ($this->tabledata as $key => $row) { if (isset($row['itemname']) && $row['itemname']['id'] == $header_row) { // Found it - update the column. $content = '-'; if (!is_null($gradeval)) { $decimals = $grade_object->get_decimals(); $content = format_float($gradeval, $decimals, true) . ' %'; } $this->tabledata[$key]['contributiontocoursetotal']['content'] = $content; break; } } } } } }
/** * Returns string representation of grade value * @param float $value grade value * @param object $grade_item - by reference to prevent scale reloading * @param bool $localized use localised decimal separator * @param int $display type of display - raw, letter, percentage * @param int $decimalplaces number of decimal places when displaying float values * @return string */ function grade_format_gradevalue($value, &$grade_item, $localized = true, $displaytype = null, $decimals = null) { if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) { return ''; } // no grade yet? if (is_null($value)) { return '-'; } if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) { //unknown type?? return ''; } if (is_null($displaytype)) { $displaytype = $grade_item->get_displaytype(); } if (is_null($decimals)) { $decimals = $grade_item->get_decimals(); } switch ($displaytype) { case GRADE_DISPLAY_TYPE_REAL: if ($grade_item->gradetype == GRADE_TYPE_SCALE) { $scale = $grade_item->load_scale(); $value = (int) bounded_number($grade_item->grademin, $value, $grade_item->grademax); return format_string($scale->scale_items[$value - 1]); } else { return format_float($value, $decimals, $localized); } case GRADE_DISPLAY_TYPE_PERCENTAGE: $min = $grade_item->grademin; $max = $grade_item->grademax; if ($min == $max) { return ''; } $value = bounded_number($min, $value, $max); $percentage = ($value - $min) * 100 / ($max - $min); return format_float($percentage, $decimals, $localized) . ' %'; case GRADE_DISPLAY_TYPE_LETTER: $context = get_context_instance(CONTEXT_COURSE, $grade_item->courseid); if (!($letters = grade_get_letters($context))) { return ''; // no letters?? } $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100); $value = bounded_number(0, $value, 100); // just in case foreach ($letters as $boundary => $letter) { if ($value >= $boundary) { return format_string($letter); } } return '-'; // no match? maybe '' would be more correct // no match? maybe '' would be more correct default: return ''; } }
/** * Internal function for grade category grade aggregation * * @param int $userid The User ID * @param array $items Grade items * @param array $grade_values Array of grade values * @param object $oldgrade Old grade * @param array $excluded Excluded */ private function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded) { global $CFG; if (empty($userid)) { //ignore first call return; } if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); $grade->grade_item =& $this->grade_item; } else { // insert final grade - it will be needed later anyway $grade = new grade_grade(array('itemid' => $this->grade_item->id, 'userid' => $userid), false); $grade->grade_item =& $this->grade_item; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { 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); return; } // 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; if (!is_null($oldfinalgrade)) { $grade->update('aggregation'); } return; } // normalize the grades first - all will have value 0...1 // ungraded items are not used in aggregation foreach ($grade_values as $itemid => $v) { if (is_null($v)) { // null means no grade unset($grade_values[$itemid]); continue; } else { if (in_array($itemid, $excluded)) { unset($grade_values[$itemid]); continue; } } $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 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)) { $grade_values[$itemid] = 0; } } } // limit and sort $this->apply_limit_rules($grade_values, $items); asort($grade_values, SORT_NUMERIC); // let's see we have still enough grades to do any statistics if (count($grade_values) == 0) { // not enough attempts yet $grade->finalgrade = null; if (!is_null($oldfinalgrade)) { $grade->update('aggregation'); } return; } // do the maths $agg_grade = $this->aggregate_values($grade_values, $items); // recalculate the grade back to requested range $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax); $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade); // update in db if changed if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->update('aggregation'); } return; }
/** * Given a float grade value or integer grade scale, applies a number of adjustment based on * grade_item variables and returns the result. * @param float $rawgrade The raw grade value. * @param float $rawmin original rawmin * @param float $rawmax original rawmax * @return mixed */ function adjust_raw_grade($rawgrade, $rawmin, $rawmax) { if (is_null($rawgrade)) { return null; } if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade if ($this->grademax < $this->grademin) { return null; } if ($this->grademax == $this->grademin) { return $this->grademax; // no range } // Standardise score to the new grade range // NOTE: this is not compatible with current assignment grading if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) { $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax); } // Apply other grade_item factors $rawgrade *= $this->multfactor; $rawgrade += $this->plusfactor; return bounded_number($this->grademin, $rawgrade, $this->grademax); } else { if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value if (empty($this->scale)) { $this->load_scale(); } if ($this->grademax < 0) { return null; // scale not present - no grade } if ($this->grademax == 0) { return $this->grademax; // only one option } // Convert scale if needed // NOTE: this is not compatible with current assignment grading if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) { $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax); } return (int) bounded_number(0, round($rawgrade + 1.0E-5), $this->grademax); } else { if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value // somebody changed the grading type when grades already existed return null; } else { debugging("Unknown grade type"); return null; } } } }
/** * Returns a letter grade representation of a grade value * The array of grade letters used is produced by {@link grade_get_letters()} using the course context * * @param float $value The grade value * @param object $grade_item Grade item object * @return string */ function grade_format_gradevalue_letter($value, $grade_item) { $context = get_context_instance(CONTEXT_COURSE, $grade_item->courseid); if (!($letters = grade_get_letters($context))) { return ''; // no letters?? } if (is_null($value)) { return '-'; } $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100); $value = bounded_number(0, $value, 100); // just in case foreach ($letters as $boundary => $letter) { if ($value >= $boundary) { return format_string($letter); } } return '-'; // no match? maybe '' would be more correct }
/** * Return array of grade item ids that are either hidden or indirectly depend * on hidden grades, excluded grades are not returned. * THIS IS A REALLY BIG HACK! to be replaced by conditional aggregation of hidden grades in 2.0 * * @static * @param array $grades all course grades of one user, & used for better internal caching * @param array $items $grade_items array of grade items, & used for better internal caching * @return array */ public static function get_hiding_affected(&$grade_grades, &$grade_items) { global $CFG; if (count($grade_grades) !== count($grade_items)) { print_error('invalidarraysize', 'debug', '', 'grade_grade::get_hiding_affected()!'); } $dependson = array(); $todo = array(); $unknown = array(); // can not find altered $altered = array(); // altered grades $hiddenfound = false; foreach ($grade_grades as $itemid => $unused) { $grade_grade =& $grade_grades[$itemid]; if ($grade_grade->is_excluded()) { //nothing to do, aggregation is ok } else { if ($grade_grade->is_hidden()) { $hiddenfound = true; $altered[$grade_grade->itemid] = null; } else { if ($grade_grade->is_locked() or $grade_grade->is_overridden()) { // no need to recalculate locked or overridden grades } else { $dependson[$grade_grade->itemid] = $grade_items[$grade_grade->itemid]->depends_on(); if (!empty($dependson[$grade_grade->itemid])) { $todo[] = $grade_grade->itemid; } } } } } if (!$hiddenfound) { return array('unknown' => array(), 'altered' => array()); } $max = count($todo); $hidden_precursors = null; for ($i = 0; $i < $max; $i++) { $found = false; foreach ($todo as $key => $do) { $hidden_precursors = array_intersect($dependson[$do], $unknown); if ($hidden_precursors) { // this item depends on hidden grade indirectly $unknown[$do] = $do; unset($todo[$key]); $found = true; continue; } else { if (!array_intersect($dependson[$do], $todo)) { $hidden_precursors = array_intersect($dependson[$do], array_keys($altered)); if (!$hidden_precursors) { // hiding does not affect this grade unset($todo[$key]); $found = true; continue; } else { // depends on altered grades - we should try to recalculate if possible if ($grade_items[$do]->is_calculated() or !$grade_items[$do]->is_category_item() and !$grade_items[$do]->is_course_item()) { $unknown[$do] = $do; unset($todo[$key]); $found = true; continue; } else { $grade_category = $grade_items[$do]->load_item_category(); $values = array(); foreach ($dependson[$do] as $itemid) { if (array_key_exists($itemid, $altered)) { //nulling an altered precursor $values[$itemid] = $altered[$itemid]; } elseif (empty($values[$itemid])) { $values[$itemid] = $grade_grades[$itemid]->finalgrade; } } foreach ($values as $itemid => $value) { if ($grade_grades[$itemid]->is_excluded()) { unset($values[$itemid]); continue; } $values[$itemid] = grade_grade::standardise_score($value, $grade_items[$itemid]->grademin, $grade_items[$itemid]->grademax, 0, 1); } if ($grade_category->aggregateonlygraded) { foreach ($values as $itemid => $value) { if (is_null($value)) { unset($values[$itemid]); } } } else { foreach ($values as $itemid => $value) { if (is_null($value)) { $values[$itemid] = 0; } } } // limit and sort $grade_category->apply_limit_rules($values, $grade_items); asort($values, SORT_NUMERIC); // let's see we have still enough grades to do any statistics if (count($values) == 0) { // not enough attempts yet $altered[$do] = null; unset($todo[$key]); $found = true; continue; } $agg_grade = $grade_category->aggregate_values($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_items[$do]->bounded_grade($finalgrade); $altered[$do] = $finalgrade; unset($todo[$key]); $found = true; continue; } } } } } if (!$found) { break; } } return array('unknown' => $unknown, 'altered' => $altered); }
/** * Return array of grade item ids that are either hidden or indirectly depend * on hidden grades, excluded grades are not returned. * THIS IS A REALLY BIG HACK! to be replaced by conditional aggregation of hidden grades in 2.0 * * @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 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 * alteredgrademin => for each item in altered or unknown, the new value of the grademin * alteredgradestatus => for each item with a modified status - the value of the new status * alteredgradeweight => for each item with a modified weight - the value of the new weight */ public static function get_hiding_affected(&$grade_grades, &$grade_items) { global $CFG; if (count($grade_grades) !== count($grade_items)) { print_error('invalidarraysize', 'debug', '', 'grade_grade::get_hiding_affected()!'); } $dependson = array(); $todo = array(); $unknown = array(); // can not find altered $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; foreach ($grade_grades as $itemid => $unused) { $grade_grade =& $grade_grades[$itemid]; // We need the immediate dependencies of all every grade_item so we can calculate nested dependencies. $dependson[$grade_grade->itemid] = $grade_items[$grade_grade->itemid]->depends_on(); if ($grade_grade->is_excluded()) { //nothing to do, aggregation is ok } else { if ($grade_grade->is_hidden()) { $hiddenfound = true; $altered[$grade_grade->itemid] = null; $alteredaggregationstatus[$grade_grade->itemid] = 'dropped'; $alteredaggregationweight[$grade_grade->itemid] = 0; } else { if ($grade_grade->is_locked() or $grade_grade->is_overridden()) { // no need to recalculate locked or overridden grades } else { if (!empty($dependson[$grade_grade->itemid])) { $dependencydepth[$grade_grade->itemid] = 1; $todo[] = $grade_grade->itemid; } } } } } // Flatten the dependency tree and count number of branches to each leaf. self::flatten_dependencies_array($dependson, $dependencydepth); if (!$hiddenfound) { return array('unknown' => array(), 'altered' => array(), 'alteredgrademax' => 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)); // We need to resort the todo list by the dependency depth. This guarantees we process the leaves, then the branches. array_multisort($dependencydepth, $todo); $max = count($todo); $hidden_precursors = null; for ($i = 0; $i < $max; $i++) { $found = false; foreach ($todo as $key => $do) { $hidden_precursors = array_intersect($dependson[$do], $unknown); if ($hidden_precursors) { // this item depends on hidden grade indirectly $unknown[$do] = $do; unset($todo[$key]); $found = true; continue; } else { if (!array_intersect($dependson[$do], $todo)) { $hidden_precursors = array_intersect($dependson[$do], array_keys($altered)); if (!$hidden_precursors) { // hiding does not affect this grade unset($todo[$key]); $found = true; continue; } else { // depends on altered grades - we should try to recalculate if possible if ($grade_items[$do]->is_calculated() or !$grade_items[$do]->is_category_item() and !$grade_items[$do]->is_course_item()) { // This is a grade item that is not a category or course and has been affected by grade hiding. // I guess this means it is a calculation that needs to be recalculated. $unknown[$do] = $do; unset($todo[$key]); $found = true; continue; } else { // This is a grade category (or course). $grade_category = $grade_items[$do]->load_item_category(); // Build a new list of the grades in this category. $values = array(); $immediatedepends = $grade_items[$do]->depends_on(); foreach ($immediatedepends as $itemid) { if (array_key_exists($itemid, $altered)) { //nulling an altered precursor $values[$itemid] = $altered[$itemid]; if (is_null($values[$itemid])) { // This means this was a hidden grade item removed from the result. unset($values[$itemid]); } } elseif (empty($values[$itemid])) { $values[$itemid] = $grade_grades[$itemid]->finalgrade; } } foreach ($values as $itemid => $value) { if ($grade_grades[$itemid]->is_excluded()) { unset($values[$itemid]); $alteredaggregationstatus[$itemid] = 'excluded'; $alteredaggregationweight[$itemid] = null; continue; } // The grade min/max may have been altered by hiding. $grademin = $grade_items[$itemid]->grademin; if (isset($alteredgrademin[$itemid])) { $grademin = $alteredgrademin[$itemid]; } $grademax = $grade_items[$itemid]->grademax; if (isset($alteredgrademax[$itemid])) { $grademax = $alteredgrademax[$itemid]; } $values[$itemid] = grade_grade::standardise_score($value, $grademin, $grademax, 0, 1); } if ($grade_category->aggregateonlygraded) { foreach ($values as $itemid => $value) { if (is_null($value)) { unset($values[$itemid]); $alteredaggregationstatus[$itemid] = 'novalue'; $alteredaggregationweight[$itemid] = null; } } } else { foreach ($values as $itemid => $value) { if (is_null($value)) { $values[$itemid] = 0; } } } // 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] = null; } 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 if (count($values) == 0) { // not enough attempts yet $altered[$do] = null; unset($todo[$key]); $found = true; continue; } $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'], 0, 1, $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']; // We need to muck with the "in-memory" grade_items records so // that subsequent calculations will use the adjusted grademin and grademax. $grade_items[$do]->grademin = $adjustedgrade['grademin']; $grade_items[$do]->grademax = $adjustedgrade['grademax']; $altered[$do] = $finalgrade; unset($todo[$key]); $found = true; continue; } } } } } if (!$found) { break; } } return array('unknown' => $unknown, 'altered' => $altered, 'alteredgrademax' => $alteredgrademax, 'alteredgrademin' => $alteredgrademin, 'alteredaggregationstatus' => $alteredaggregationstatus, 'alteredaggregationweight' => $alteredaggregationweight); }
/** * Internal function for grade category grade aggregation * * @param int $userid The User ID * @param array $items Grade items * @param array $grade_values Array of grade values * @param object $oldgrade Old grade * @param array $excluded Excluded * @param array $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid) * @param array $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid) */ private function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded, $grademinoverrides, $grademaxoverrides) { global $CFG, $DB; // 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)) { //ignore first call return; } if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); $grade->grade_item =& $this->grade_item; } else { // insert final grade - it will be needed later anyway $grade = new grade_grade(array('itemid' => $this->grade_item->id, 'userid' => $userid), false); $grade->grade_item =& $this->grade_item; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return; } // 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. // We need to do this so we can set the aggregationstatus // with a set_field call instead of checking if each one exists and creating/updating. if (!empty($items)) { list($ggsql, $params) = $DB->get_in_or_equal(array_keys($items), SQL_PARAMS_NAMED, 'g'); $params['userid'] = $userid; $sql = "SELECT itemid\n FROM {grade_grades}\n WHERE itemid {$ggsql} AND userid = :userid"; $existingitems = $DB->get_records_sql($sql, $params); $notexisting = array_diff(array_keys($items), array_keys($existingitems)); foreach ($notexisting as $itemid) { $gradeitem = $items[$itemid]; $gradegrade = new grade_grade(array('itemid' => $itemid, 'userid' => $userid, 'rawgrademin' => $gradeitem->grademin, 'rawgrademax' => $gradeitem->grademax), false); $gradegrade->grade_item = $gradeitem; $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; if (!is_null($oldfinalgrade)) { $success = $grade->update('aggregation'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } $dropped = $grade_values; $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); return; } // Normalize the grades first - all will have value 0...1 // ungraded items are not used in aggregation. foreach ($grade_values as $itemid => $v) { if (is_null($v)) { // If null, it means no grade. if ($this->aggregateonlygraded) { unset($grade_values[$itemid]); // Mark this item as "excluded empty" because it has no grade. $novalue[$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; if (isset($grademinoverrides[$itemid])) { $usergrademin = $grademinoverrides[$itemid]; } if (isset($grademaxoverrides[$itemid])) { $usergrademax = $grademaxoverrides[$itemid]; } if ($this->aggregation == GRADE_AGGREGATE_SUM) { // Assume that the grademin is 0 when standardising the score, to preserve negative grades. $grade_values[$itemid] = grade_grade::standardise_score($v, 0, $usergrademax, 0, 1); } else { $grade_values[$itemid] = grade_grade::standardise_score($v, $usergrademin, $usergrademax, 0, 1); } } // For items with no value, and not excluded - either set their grade to 0 or exclude them. foreach ($items as $itemid => $value) { if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) { if (!$this->aggregateonlygraded) { $grade_values[$itemid] = 0; } else { // We are specifically marking these items as "excluded empty". $novalue[$itemid] = 0; } } } // limit and sort $allvalues = $grade_values; if ($this->can_apply_limit_rules()) { $this->apply_limit_rules($grade_values, $items); } $moredropped = array_diff($allvalues, $grade_values); 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 if (count($grade_values) == 0) { // not enough attempts yet $grade->finalgrade = null; if (!is_null($oldfinalgrade)) { $success = $grade->update('aggregation'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); return; } // do the maths $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items, $usedweights, $grademinoverrides, $grademaxoverrides); $agg_grade = $result['grade']; // Set the actual grademin and max to bind the grade properly. $this->grade_item->grademin = $result['grademin']; $this->grade_item->grademax = $result['grademax']; if ($this->aggregation == GRADE_AGGREGATE_SUM) { // The natural aggregation always displays the range as coming from 0 for categories. // However, when we bind the grade we allow for negative values. $result['grademin'] = 0; } // Recalculate the grade back to requested range. $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $result['grademin'], $result['grademax']); $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade); $oldrawgrademin = $grade->rawgrademin; $oldrawgrademax = $grade->rawgrademax; $grade->rawgrademin = $result['grademin']; $grade->rawgrademax = $result['grademax']; // Update in db if changed. if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || grade_floats_different($grade->rawgrademax, $oldrawgrademax) || grade_floats_different($grade->rawgrademin, $oldrawgrademin)) { $success = $grade->update('aggregation'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); return; }
function sub_test_grade_grade_standardise_score() { $this->assertEqual(4, round(grade_grade::standardise_score(6, 0, 7, 0, 5))); $this->assertEqual(40, grade_grade::standardise_score(50, 30, 80, 0, 100)); }
/** * Returns a letter grade representation of a grade value * The array of grade letters used is produced by {@link grade_get_letters()} using the course context * * @param float $value The grade value * @param object $grade_item Grade item object * @return string */ function grade_format_gradevalue_letter($value, $grade_item) { global $CFG; $context = context_course::instance($grade_item->courseid, IGNORE_MISSING); if (!($letters = grade_get_letters($context))) { return ''; // no letters?? } if (is_null($value)) { return '-'; } $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100); $value = bounded_number(0, $value, 100); // just in case $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $grade_item->courseid; foreach ($letters as $boundary => $letter) { if (property_exists($CFG, $gradebookcalculationsfreeze) && (int) $CFG->{$gradebookcalculationsfreeze} <= 20160518) { // Do nothing. } else { // The boundary is a percentage out of 100 so use 0 as the min and 100 as the max. $boundary = grade_grade::standardise_score($boundary, 0, 100, 0, 100); } if ($value >= $boundary) { return format_string($letter); } } return '-'; // no match? maybe '' would be more correct }
/** * Given a float grade value or integer grade scale, applies a number of adjustment based on * grade_item variables and returns the result. * * @param float $rawgrade The raw grade value * @param float $rawmin original rawmin * @param float $rawmax original rawmax * @return mixed */ public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) { if (is_null($rawgrade)) { return null; } if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade if ($this->grademax < $this->grademin) { return null; } if ($this->grademax == $this->grademin) { return $this->grademax; // no range } // Standardise score to the new grade range // NOTE: skip if the activity provides a manual rescaling option. $manuallyrescale = component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false; if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) { $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax); } // Apply other grade_item factors $rawgrade *= $this->multfactor; $rawgrade += $this->plusfactor; return $this->bounded_grade($rawgrade); } else { if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value if (empty($this->scale)) { $this->load_scale(); } if ($this->grademax < 0) { return null; // scale not present - no grade } if ($this->grademax == 0) { return $this->grademax; // only one option } // Convert scale if needed // NOTE: skip if the activity provides a manual rescaling option. $manuallyrescale = component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false; if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) { // This should never happen because scales are locked if they are in use. $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax); } return $this->bounded_grade($rawgrade); } else { if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value // somebody changed the grading type when grades already existed return null; } else { debugging("Unknown grade type"); return null; } } } }
/** * 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 * @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. * Some rules for the weights are: * 1. The weights must add up to 1 (unless there are extra credit) * 2. The contributed points column must add up to the course * final grade and this column is calculated from these weights. * @param array $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid) * @param array $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid) * @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, &$weights = null, $grademinoverrides = array(), $grademaxoverrides = array()) { $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 $num = count($grade_values); $grades = array_values($grade_values); // The median gets 100% - others get 0. if ($weights !== null && $num > 0) { $count = 0; foreach ($grade_values as $itemid => $grade_value) { if ($num % 2 == 0 && ($count == intval($num / 2) - 1 || $count == intval($num / 2))) { $weights[$itemid] = 0.5; } else { if ($num % 2 != 0 && $count == intval($num / 2 - 0.5)) { $weights[$itemid] = 1.0; } else { $weights[$itemid] = 0; } } $count++; } } if ($num % 2 == 0) { $agg_grade = ($grades[intval($num / 2) - 1] + $grades[intval($num / 2)]) / 2; } else { $agg_grade = $grades[intval($num / 2 - 0.5)]; } 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: // 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 // array_count_values only counts INT and STRING, so if grades are floats we must convert them to string $converted_grade_values = array(); foreach ($grade_values as $k => $gv) { if (!is_int($gv) && !is_string($gv)) { $converted_grade_values[$k] = (string) $gv; } else { $converted_grade_values[$k] = $gv; } if ($weights !== null) { $weights[$k] = 0; } } $freq = array_count_values($converted_grade_values); arsort($freq); // sort by frequency keeping keys $top = reset($freq); // highest frequency count $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 = 0; $sum = 0; foreach ($grade_values as $itemid => $grade_value) { if ($weights !== null) { $weights[$itemid] = $items[$itemid]->aggregationcoef; } if ($items[$itemid]->aggregationcoef <= 0) { continue; } $weightsum += $items[$itemid]->aggregationcoef; $sum += $items[$itemid]->aggregationcoef * $grade_value; } 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; case GRADE_AGGREGATE_WEIGHTED_MEAN2: // Weighted average of all existing final grades with optional extra credit flag, // weight is the range of grade (usually grademax) $this->load_grade_item(); $weightsum = 0; $sum = null; foreach ($grade_values as $itemid => $grade_value) { if ($items[$itemid]->aggregationcoef > 0) { continue; } $weight = $items[$itemid]->grademax - $items[$itemid]->grademin; if ($weight <= 0) { continue; } $weightsum += $weight; $sum += $weight * $grade_value; } // Handle the extra credit items separately to calculate their weight accurately. foreach ($grade_values as $itemid => $grade_value) { if ($items[$itemid]->aggregationcoef <= 0) { continue; } $weight = $items[$itemid]->grademax - $items[$itemid]->grademin; if ($weight <= 0) { $weights[$itemid] = 0; continue; } $oldsum = $sum; $weightedgrade = $weight * $grade_value; $sum += $weightedgrade; if ($weights !== null) { if ($weightsum <= 0) { $weights[$itemid] = 0; continue; } $oldgrade = $oldsum / $weightsum; $grade = $sum / $weightsum; $normoldgrade = grade_grade::standardise_score($oldgrade, 0, 1, $grademin, $grademax); $normgrade = grade_grade::standardise_score($grade, 0, 1, $grademin, $grademax); $boundedoldgrade = $this->grade_item->bounded_grade($normoldgrade); $boundedgrade = $this->grade_item->bounded_grade($normgrade); if ($boundedgrade - $boundedoldgrade <= 0) { // Nothing new was added to the grade. $weights[$itemid] = 0; } else { if ($boundedgrade < $normgrade) { // The grade has been bounded, the extra credit item needs to have a different weight. $gradediff = $boundedgrade - $normoldgrade; $gradediffnorm = grade_grade::standardise_score($gradediff, $grademin, $grademax, 0, 1); $weights[$itemid] = $gradediffnorm / $grade_value; } else { // Default weighting. $weights[$itemid] = $weight / $weightsum; } } } } 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) { // Ignore extra credit items, the weights have already been computed. continue; } if ($weightsum > 0) { $weight = $items[$itemid]->grademax - $items[$itemid]->grademin; $weights[$itemid] = $weight / $weightsum; } else { $weights[$itemid] = 0; } } } break; case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average $this->load_grade_item(); $num = 0; $sum = null; foreach ($grade_values as $itemid => $grade_value) { if ($items[$itemid]->aggregationcoef == 0) { $num += 1; $sum += $grade_value; if ($weights !== null) { $weights[$itemid] = 1; } } } // Treating the extra credit items separately to get a chance to calculate their effective weights. foreach ($grade_values as $itemid => $grade_value) { if ($items[$itemid]->aggregationcoef > 0) { $oldsum = $sum; $sum += $items[$itemid]->aggregationcoef * $grade_value; if ($weights !== null) { if ($num <= 0) { // The category only contains extra credit items, not setting the weight. continue; } $oldgrade = $oldsum / $num; $grade = $sum / $num; $normoldgrade = grade_grade::standardise_score($oldgrade, 0, 1, $grademin, $grademax); $normgrade = grade_grade::standardise_score($grade, 0, 1, $grademin, $grademax); $boundedoldgrade = $this->grade_item->bounded_grade($normoldgrade); $boundedgrade = $this->grade_item->bounded_grade($normgrade); if ($boundedgrade - $boundedoldgrade <= 0) { // Nothing new was added to the grade. $weights[$itemid] = 0; } else { if ($boundedgrade < $normgrade) { // The grade has been bounded, the extra credit item needs to have a different weight. $gradediff = $boundedgrade - $normoldgrade; $gradediffnorm = grade_grade::standardise_score($gradediff, $grademin, $grademax, 0, 1); $weights[$itemid] = $gradediffnorm / $grade_value; } else { // Default weighting. $weights[$itemid] = 1.0 / $num; } } } } } if ($weights !== null && $num > 0) { foreach ($grade_values as $itemid => $grade_value) { if ($items[$itemid]->aggregationcoef > 0) { // Extra credit weights were already calculated. continue; } if ($weights[$itemid]) { $weights[$itemid] = 1.0 / $num; } } } if ($num == 0) { $agg_grade = $sum; // only extra credits or wrong coefs } else { $agg_grade = $sum / $num; } break; case GRADE_AGGREGATE_SUM: // Add up all the items. $this->load_grade_item(); $num = count($grade_values); $sum = 0; // This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights. // Even though old algorith has bugs in it, we need to preserve existing grades. $gradebookcalculationfreeze = (int) get_config('core', 'gradebook_calculations_freeze_' . $this->courseid); $oldextracreditcalculation = $gradebookcalculationfreeze && $gradebookcalculationfreeze <= 20150619; $sumweights = 0; $grademin = 0; $grademax = 0; $extracredititems = array(); foreach ($grade_values as $itemid => $gradevalue) { // We need to check if the grademax/min was adjusted per user because of excluded items. $usergrademin = $items[$itemid]->grademin; $usergrademax = $items[$itemid]->grademax; if (isset($grademinoverrides[$itemid])) { $usergrademin = $grademinoverrides[$itemid]; } if (isset($grademaxoverrides[$itemid])) { $usergrademax = $grademaxoverrides[$itemid]; } // Keep track of the extra credit items, we will need them later on. if ($items[$itemid]->aggregationcoef > 0) { $extracredititems[$itemid] = $items[$itemid]; } // Ignore extra credit and items with a weight of 0. if (!isset($extracredititems[$itemid]) && $items[$itemid]->aggregationcoef2 > 0) { $grademin += $usergrademin; $grademax += $usergrademax; $sumweights += $items[$itemid]->aggregationcoef2; } } $userweights = array(); $totaloverriddenweight = 0; $totaloverriddengrademax = 0; // We first need to rescale all manually assigned weights down by the // percentage of weights missing from the category. foreach ($grade_values as $itemid => $gradevalue) { if ($items[$itemid]->weightoverride) { if ($items[$itemid]->aggregationcoef2 <= 0) { // Records the weight of 0 and continue. $userweights[$itemid] = 0; continue; } $userweights[$itemid] = $sumweights ? $items[$itemid]->aggregationcoef2 / $sumweights : 0; if (!$oldextracreditcalculation && isset($extracredititems[$itemid])) { // Extra credit items do not affect totals. continue; } $totaloverriddenweight += $userweights[$itemid]; $usergrademax = $items[$itemid]->grademax; if (isset($grademaxoverrides[$itemid])) { $usergrademax = $grademaxoverrides[$itemid]; } $totaloverriddengrademax += $usergrademax; } } $nonoverriddenpoints = $grademax - $totaloverriddengrademax; // Then we need to recalculate the automatic weights except for extra credit items. foreach ($grade_values as $itemid => $gradevalue) { if (!$items[$itemid]->weightoverride && ($oldextracreditcalculation || !isset($extracredititems[$itemid]))) { $usergrademax = $items[$itemid]->grademax; if (isset($grademaxoverrides[$itemid])) { $usergrademax = $grademaxoverrides[$itemid]; } if ($nonoverriddenpoints > 0) { $userweights[$itemid] = $usergrademax / $nonoverriddenpoints * (1 - $totaloverriddenweight); } else { $userweights[$itemid] = 0; if ($items[$itemid]->aggregationcoef2 > 0) { // Items with a weight of 0 should not count for the grade max, // though this only applies if the weight was changed to 0. $grademax -= $usergrademax; } } } } // Now when we finally know the grademax we can adjust the automatic weights of extra credit items. if (!$oldextracreditcalculation) { foreach ($grade_values as $itemid => $gradevalue) { if (!$items[$itemid]->weightoverride && isset($extracredititems[$itemid])) { $usergrademax = $items[$itemid]->grademax; if (isset($grademaxoverrides[$itemid])) { $usergrademax = $grademaxoverrides[$itemid]; } $userweights[$itemid] = $grademax ? $usergrademax / $grademax : 0; } } } // We can use our freshly corrected weights below. foreach ($grade_values as $itemid => $gradevalue) { if (isset($extracredititems[$itemid])) { // We skip the extra credit items first. continue; } $sum += $gradevalue * $userweights[$itemid] * $grademax; if ($weights !== null) { $weights[$itemid] = $userweights[$itemid]; } } // No we proceed with the extra credit items. They might have a different final // weight in case the final grade was bounded. So we need to treat them different. // Also, as we need to use the bounded_grade() method, we have to inject the // right values there, and restore them afterwards. $oldgrademax = $this->grade_item->grademax; $oldgrademin = $this->grade_item->grademin; foreach ($grade_values as $itemid => $gradevalue) { if (!isset($extracredititems[$itemid])) { continue; } $oldsum = $sum; $weightedgrade = $gradevalue * $userweights[$itemid] * $grademax; $sum += $weightedgrade; // Only go through this when we need to record the weights. if ($weights !== null) { if ($grademax <= 0) { // There are only extra credit items in this category, // all the weights should be accurate (and be 0). $weights[$itemid] = $userweights[$itemid]; continue; } $oldfinalgrade = $this->grade_item->bounded_grade($oldsum); $newfinalgrade = $this->grade_item->bounded_grade($sum); $finalgradediff = $newfinalgrade - $oldfinalgrade; if ($finalgradediff <= 0) { // This item did not contribute to the category total at all. $weights[$itemid] = 0; } else { if ($finalgradediff < $weightedgrade) { // The weight needs to be adjusted because only a portion of the // extra credit item contributed to the category total. $weights[$itemid] = $finalgradediff / ($gradevalue * $grademax); } else { // The weight was accurate. $weights[$itemid] = $userweights[$itemid]; } } } } $this->grade_item->grademax = $oldgrademax; $this->grade_item->grademin = $oldgrademin; if ($grademax > 0) { $agg_grade = $sum / $grademax; // Re-normalize score. } else { // Every item in the category is extra credit. $agg_grade = $sum; $grademax = $sum; } break; case GRADE_AGGREGATE_MEAN: // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum) // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum) default: $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; } return array('grade' => $agg_grade, 'grademin' => $grademin, 'grademax' => $grademax); }
/** * internal function for category grades aggregation * * @param int $userid * @param array $items * @param array $grade_values * @param object $oldgrade * @param bool $excluded * @return boolean (just plain return;) */ function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded, $grade_max, $grade_min) { global $CFG; if (empty($userid)) { //ignore first call return; } if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade_local($oldgrade, false); $grade->grade_item =& $this->grade_item; } else { // insert final grade - it will be needed later anyway $grade = new grade_grade_local(array('itemid' => $this->grade_item->id, 'userid' => $userid), false); $grade->grade_item =& $this->grade_item; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { 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 // HACK: Bob Puffer 12/8/09 to allow for correct summing of point totals for all aggregation methods if ($this->aggregation == GRADE_AGGREGATE_SUM) { $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded); return; } // CONSIDER REINSTATING THIS IF IT DOESN'T WORK OUT TO REMOVE IT // sum all aggregation methods // $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded); // END OF HACK // 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; if (!is_null($oldfinalgrade)) { $grade->update('aggregation'); } return; } /// normalize the grades first - all will have value 0...1 // ungraded items are not used in aggregation foreach ($grade_values as $itemid => $v) { if (is_null($v)) { // null means no grade unset($grade_values[$itemid]); continue; } else { if (in_array($itemid, $excluded)) { unset($grade_values[$itemid]); continue; } } // $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 0, 1); $items[$itemid]->grademax = $grade_max[$itemid]; $grade_values[$itemid] = grade_grade::standardise_score($v, $grade_min[$itemid], $grade_max[$itemid], 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)) { $grade_values[$itemid] = 0; } } } // limit and sort $this->apply_limit_rules($grade_values, $items); asort($grade_values, SORT_NUMERIC); // HACK 10/29/09 Bob Puffer to allow accurate computation of category maxgrade // has to be done after any dropped grades are dropped $cat_max = 0; // END OF HACK foreach ($grade_values as $itemid => $v) { if ($items[$itemid]->aggregationcoef == 1 and $this->aggregation != GRADE_AGGREGATE_WEIGHTED_MEAN) { } else { if ($items[$itemid]->itemtype == 'category') { // $gradegradesrec = new grade_grade_local(array('itemid'=>$itemid, 'userid'=>$userid), true); // if (isset($gradegradesrec->itemid->itemtype) AND $gradegradesrec->itemid->itemtype == 'category') { // $cat_max += $gradegradesrec->rawgrademax; $cat_max += $grade_max[$itemid]; } else { // $cat_max += $items[$itemid]->grademax; // $cat_max += $gradegradesrec->itemid->grademax; $cat_max += $grade_max[$itemid]; // } } } } // END OF HACK // let's see we have still enough grades to do any statistics if (count($grade_values) == 0) { // not enough attempts yet $grade->finalgrade = null; if (!is_null($oldfinalgrade)) { $grade->update('aggregation'); } return; } // do the maths // HACK: Bob Puffer 10/29/09 to allow proper totalling of points for the category // $agg_grade = $this->aggregate_values($grade_values, $items); // recalculate the grade back to requested range // $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax); $this->grade_item->grademax = $cat_max; $oldmaxgrade = $grade->rawgrademax; $grade->rawgrademax = $cat_max; // HACK we don't want to call the aggregate_values function if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) { $weightsum = 0; $sum = null; foreach ($grade_values as $itemid => $grade_value) { $weight = $grade_max[$itemid] - $grade_min[$itemid]; // $weight = $items[$itemid]->grademax - $items[$itemid]->grademin; if ($weight <= 0) { continue; } $sum += $weight * $grade_value; } if ($weightsum == 0) { $finalgrade = $sum; // only extra credits } else { $finalgrade = $sum / $weightsum; } } else { $agg_grade = $this->aggregate_values($grade_values, $items); // recalculate the grade back to requested range $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax); } // END OF HACK $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade); // update in db if changed // HACK to update category maxes in the db if they change // if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { if (grade_floats_different($grade->finalgrade, $oldfinalgrade) or grade_floats_different($grade->rawgrademax, $oldmaxgrade)) { $grade->update('aggregation'); } return; }
/** * internal function for category grades aggregation */ function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded) { global $CFG; if (empty($userid)) { //ignore first call return; } if ($oldgrade) { $grade = new grade_grade($oldgrade, false); $grade->grade_item =& $this->grade_item; } else { // insert final grade - it will be needed later anyway $grade = new grade_grade(array('itemid' => $this->grade_item->id, 'userid' => $userid), false); $grade->insert('system'); $grade->grade_item =& $this->grade_item; $oldgrade = new object(); $oldgrade->finalgrade = $grade->finalgrade; $oldgrade->rawgrade = $grade->rawgrade; $oldgrade->rawgrademin = $grade->rawgrademin; $oldgrade->rawgrademax = $grade->rawgrademax; $oldgrade->rawscaleid = $grade->rawscaleid; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return; } // can not use own final category grade in calculation unset($grade_values[$this->grade_item->id]); // if no grades calculation possible or grading not allowed clear both final and raw 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; $grade->rawgrade = null; if ($grade->finalgrade !== $oldgrade->finalgrade or $grade->rawgrade !== $oldgrade->rawgrade) { $grade->update('system'); } return; } /// normalize the grades first - all will have value 0...1 // ungraded items are not used in aggregation foreach ($grade_values as $itemid => $v) { if (is_null($v)) { // null means no grade unset($grade_values[$itemid]); continue; } else { if (in_array($itemid, $excluded)) { unset($grade_values[$itemid]); continue; } } $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 0, 1); } // If global aggregateonlygraded is set, override category value if ($CFG->grade_aggregateonlygraded != -1) { $this->aggregateonlygraded = $CFG->grade_aggregateonlygraded; } // 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)) { $grade_values[$itemid] = 0; } } } // limit and sort $this->apply_limit_rules($grade_values); asort($grade_values, SORT_NUMERIC); // let's see we have still enough grades to do any statistics if (count($grade_values) == 0) { // not enough attempts yet $grade->finalgrade = null; $grade->rawgrade = null; if ($grade->finalgrade !== $oldgrade->finalgrade or $grade->rawgrade !== $oldgrade->rawgrade) { $grade->update('system'); } return; } /// start the aggregation switch ($this->aggregation) { case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies $num = count($grade_values); $grades = array_values($grade_values); if ($num % 2 == 0) { $agg_grade = ($grades[intval($num / 2) - 1] + $grades[intval($num / 2)]) / 2; } else { $agg_grade = $grades[intval($num / 2 - 0.5)]; } break; case GRADE_AGGREGATE_MIN: $agg_grade = reset($grade_values); break; case GRADE_AGGREGATE_MAX: $agg_grade = array_pop($grade_values); break; case GRADE_AGGREGATE_MODE: // the most common value, average used if multimode $freq = array_count_values($grade_values); arsort($freq); // sort by frequency keeping keys $top = reset($freq); // highest frequency count $modes = array_keys($freq, $top); // search for all modes (have the same highest count) rsort($modes, SORT_NUMERIC); // get highes mode $agg_grade = reset($modes); break; case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades $weightsum = 0; $sum = 0; foreach ($grade_values as $itemid => $grade_value) { if ($items[$itemid]->aggregationcoef <= 0) { continue; } $weightsum += $items[$itemid]->aggregationcoef; $sum += $items[$itemid]->aggregationcoef * $grade_value; } if ($weightsum == 0) { $agg_grade = null; } else { $agg_grade = $sum / $weightsum; } break; case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average $num = 0; $sum = 0; foreach ($grade_values as $itemid => $grade_value) { if ($items[$itemid]->aggregationcoef == 0) { $num += 1; $sum += $grade_value; } else { if ($items[$itemid]->aggregationcoef > 0) { $sum += $items[$itemid]->aggregationcoef * $grade_value; } } } if ($num == 0) { $agg_grade = $sum; // only extra credits or wrong coefs } else { $agg_grade = $sum / $num; } break; case GRADE_AGGREGATE_MEAN: // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum) // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum) default: $num = count($grade_values); $sum = array_sum($grade_values); $agg_grade = $sum / $num; break; } /// prepare update of new raw grade $grade->rawgrademin = $this->grade_item->grademin; $grade->rawgrademax = $this->grade_item->grademax; $grade->rawscaleid = $this->grade_item->scaleid; $grade->rawgrade = null; // categories do not use raw grades // recalculate the rawgrade back to requested range $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax); if (!is_null($finalgrade)) { $grade->finalgrade = bounded_number($this->grade_item->grademin, $finalgrade, $this->grade_item->grademax); } else { $grade->finalgrade = $finalgrade; } // update in db if changed if ($grade->finalgrade !== $oldgrade->finalgrade or $grade->rawgrade !== $oldgrade->rawgrade or $grade->rawgrademin !== $oldgrade->rawgrademin or $grade->rawgrademax !== $oldgrade->rawgrademax or $grade->rawscaleid !== $oldgrade->rawscaleid) { $grade->update('system'); } return; }