/** * 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; }
/** * internal function - does the final grade calculation */ function use_formula($userid, $params, $useditems, $oldgrade) { if (empty($userid)) { return true; } // add missing final grade values // not graded (null) is counted as 0 - the spreadsheet way foreach ($useditems as $gi) { if (!array_key_exists('gi' . $gi, $params)) { $params['gi' . $gi] = 0; } else { $params['gi' . $gi] = (double) $params['gi' . $gi]; } } // can not use own final grade during calculation unset($params['gi' . $this->id]); // insert final grade - will be needed later anyway if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); // fetching from db is not needed $grade->grade_item =& $this; } else { $grade = new grade_grade(array('itemid' => $this->id, 'userid' => $userid), false); $grade->grade_item =& $this; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return true; } // do the calculation $this->formula->set_params($params); $result = $this->formula->evaluate(); if ($result === false) { $grade->finalgrade = null; } else { // normalize $result = bounded_number($this->grademin, $result, $this->grademax); if ($this->gradetype == GRADE_TYPE_SCALE) { $result = round($result + 1.0E-5); // round scales upwards } $grade->finalgrade = $result; } // update in db if changed if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->update('compute'); } if ($result !== false) { //lock grade if needed } if ($result === false) { return false; } else { return true; } }
/** * Internal function that does the final grade calculation * * @param int $userid The user ID * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database * @return bool False if an error occurred */ public function use_formula($userid, $params, $useditems, $oldgrade) { if (empty($userid)) { return true; } // add missing final grade values // not graded (null) is counted as 0 - the spreadsheet way $allinputsnull = true; foreach ($useditems as $gi) { if (!array_key_exists('gi' . $gi, $params) || is_null($params['gi' . $gi])) { $params['gi' . $gi] = 0; } else { $params['gi' . $gi] = (double) $params['gi' . $gi]; if ($gi != $this->id) { $allinputsnull = false; } } } // can not use own final grade during calculation unset($params['gi' . $this->id]); // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they // wish to update the grades. $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid); $rawminandmaxchanged = false; // insert final grade - will be needed later anyway if ($oldgrade) { // Only run through this code if the gradebook isn't frozen. if ($gradebookcalculationsfreeze && (int) $gradebookcalculationsfreeze <= 20150627) { // Do nothing. } else { // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the // grade_item grade maximum and minimum respectively. if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) { $rawminandmaxchanged = true; $oldgrade->rawgrademax = $this->grademax; $oldgrade->rawgrademin = $this->grademin; } } $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); // fetching from db is not needed $grade->grade_item =& $this; } else { $grade = new grade_grade(array('itemid' => $this->id, 'userid' => $userid), false); $grade->grade_item =& $this; $rawminandmaxchanged = false; if ($gradebookcalculationsfreeze && (int) $gradebookcalculationsfreeze <= 20150627) { // Do nothing. } else { // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the // grade_item grade maximum and minimum respectively. $rawminandmaxchanged = true; $grade->rawgrademax = $this->grademax; $grade->rawgrademin = $this->grademin; } $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return true; } if ($allinputsnull) { $grade->finalgrade = null; $result = true; } else { // do the calculation $this->formula->set_params($params); $result = $this->formula->evaluate(); if ($result === false) { $grade->finalgrade = null; } else { // normalize $grade->finalgrade = $this->bounded_grade($result); } } // Only run through this code if the gradebook isn't frozen. if ($gradebookcalculationsfreeze && (int) $gradebookcalculationsfreeze <= 20150627) { // Update in db if changed. if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->timemodified = time(); $success = $grade->update('compute'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } } else { // Update in db if changed. if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) { $grade->timemodified = time(); $success = $grade->update('compute'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } } if ($result !== false) { //lock grade if needed } if ($result === false) { return false; } else { return true; } }
/** * 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; }
/** * Internal function that does the final grade calculation * * @param int $userid The user ID * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database * @return bool False if an error occurred */ public function use_formula($userid, $params, $useditems, $oldgrade) { if (empty($userid)) { return true; } // add missing final grade values // not graded (null) is counted as 0 - the spreadsheet way $allinputsnull = true; foreach ($useditems as $gi) { if (!array_key_exists('gi' . $gi, $params) || is_null($params['gi' . $gi])) { $params['gi' . $gi] = 0; } else { $params['gi' . $gi] = (double) $params['gi' . $gi]; if ($gi != $this->id) { $allinputsnull = false; } } } // can not use own final grade during calculation unset($params['gi' . $this->id]); // insert final grade - will be needed later anyway if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); // fetching from db is not needed $grade->grade_item =& $this; } else { $grade = new grade_grade(array('itemid' => $this->id, 'userid' => $userid), false); $grade->grade_item =& $this; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return true; } if ($allinputsnull) { $grade->finalgrade = null; $result = true; } else { // do the calculation $this->formula->set_params($params); $result = $this->formula->evaluate(); if ($result === false) { $grade->finalgrade = null; } else { // normalize $grade->finalgrade = $this->bounded_grade($result); } } // update in db if changed if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->timemodified = time(); $grade->update('compute'); } if ($result !== false) { //lock grade if needed } if ($result === false) { return false; } else { return true; } }
/** * 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; }