MDL-10226 improved regrading of final grades - optimised db access, partial regrading when raw grade updated

This commit is contained in:
skodak 2007-07-08 14:57:19 +00:00
parent 9aa1e44853
commit f8e6e4dbea
4 changed files with 360 additions and 362 deletions

View file

@ -177,16 +177,11 @@ class grade_category extends grade_object {
// Recalculate grades if needed
if ($this->qualifies_for_regrading()) {
if (!parent::update($source)) {
return false;
$this->force_regrading();
}
$this->grade_item->force_regrading($source);
return true;
} else {
return parent::update($source);
}
}
/**
* If parent::delete() is successful, send force_regrading message to parent category.
@ -199,6 +194,8 @@ class grade_category extends grade_object {
return false;
}
$this->force_regrading();
$grade_item = $this->load_grade_item();
$parent = $this->load_parent_category();
@ -248,6 +245,8 @@ class grade_category extends grade_object {
return false;
}
$this->force_regrading();
// build path and depth
$this->update($source);
@ -293,48 +292,12 @@ class grade_category extends grade_object {
}
/**
* Sets this category's and its parent's grade_item.needsupdate to true.
* This is triggered whenever any change in any lower level may cause grade_finals
* for this category to require an update. The flag needs to be propagated up all
* levels until it reaches the top category. This is then used to determine whether or not
* to regenerate the raw and final grades for each category grade_item. This is accomplished
* thanks to the path variable, so we don't need to use recursion.
* @param string $source from where was the object updated (mod/forum, manual, etc.)
* @return boolean Success or failure
* Marks the category and course item as needing update - categories are always regraded.
* @return void
*/
function force_regrading($source=null) {
if (empty($this->id)) {
debugging("Needsupdate requested before inserting grade category.");
return true;
}
$this->load_grade_item();
if ($this->grade_item->needsupdate) {
// this grade_item (and category) already needs update, no need to set it again here or in parent categories
return true;
}
$paths = explode('/', $this->path);
// Remove the first index, which is always empty
unset($paths[0]);
$result = true;
if (!empty($paths)) {
$wheresql = '';
foreach ($paths as $categoryid) {
$wheresql .= "iteminstance = $categoryid OR ";
}
$wheresql = substr($wheresql, 0, strrpos($wheresql, 'OR'));
$grade_items = set_field_select('grade_items', 'needsupdate', '1', $wheresql . ' AND courseid = ' . $this->courseid);
$this->grade_item->update_from_db();
}
return $result;
function force_regrading() {
$grade_item = $this->load_grade_item();
$grade_item->force_regrading();
}
/**
@ -352,7 +315,7 @@ class grade_category extends grade_object {
* 3. Aggregate these grades
* 4. Save them in raw grades of associated category grade item
*/
function generate_grades() {
function generate_grades($userid=null) {
global $CFG;
$this->load_grade_item();
@ -363,44 +326,51 @@ class grade_category extends grade_object {
$this->grade_item->load_scale();
// find grade items of immediate children (category or grade items)
$depends_on = $this->grade_item->depends_on();
$items = array();
foreach($depends_on as $dep) {
$items[$dep] = grade_item::fetch(array('id'=>$dep));
if (empty($depends_on)) {
$items = false;
} else {
$gis = implode(',', $depends_on);
$sql = "SELECT *
FROM {$CFG->prefix}grade_items
WHERE id IN ($gis)";
$items = get_records_sql($sql);
}
// where to look for final grades - include or grade item too
$gis = implode(',', array_merge($depends_on, array($this->grade_item->id)));
if ($userid) {
$usersql = "AND g.userid=$userid";
} else {
$usersql = "";
}
// where to look for final grades - include grade of this item too, we will store the results there
$gis = implode(',', array_merge($depends_on, array($this->grade_item->id)));
$sql = "SELECT g.*
FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi
WHERE gi.id = g.itemid AND gi.courseid={$this->grade_item->courseid} AND gi.id IN ($gis)
WHERE gi.id = g.itemid AND gi.id IN ($gis) $usersql
ORDER BY g.userid";
// group the results by userid and aggregate the grades in this group
if ($rs = get_recordset_sql($sql)) {
if ($rs->RecordCount() > 0) {
$prevuser = 0;
$grades = array();
$final = null;
$grade_records = array();
$oldgrade = null;
while ($used = rs_fetch_next_record($rs)) {
if ($used->userid != $prevuser) {
$this->aggregate_grades($prevuser, $items, $grades, $depends_on, $final);
$this->aggregate_grades($prevuser, $items, $grade_records, $oldgrade);
$prevuser = $used->userid;
$grades = array();
$final = null;
$grade_records = array();
$oldgrade = null;
}
if ($used->itemid == $this->grade_item->id) {
$final = new grade_grades($used, false);
$final->grade_item =& $this->grade_item;
$grade_records[$used->itemid] = $used->finalgrade;
if ($this->grade_item->id == $used->itemid) {
$oldgrade = $used;
}
$grades[$used->itemid] = $used->finalgrade;
}
$this->aggregate_grades($prevuser, $items, $grades, $depends_on, $final);
$this->aggregate_grades($prevuser, $items, $grade_records, $oldgrade);//the last one
}
}
@ -410,88 +380,103 @@ class grade_category extends grade_object {
/**
* internal function for category grades aggregation
*/
function aggregate_grades($userid, $items, $grades, $depends_on, $final) {
function aggregate_grades($userid, $items, $grade_records, $oldgrade) {
if (empty($userid)) {
//ignore first run
//ignore first call
return;
}
// no circular references allowed
unset($grades[$this->grade_item->id]);
if ($oldgrade) {
$grade = new grade_grades($oldgrade, false);
$grade->grade_item =& $this->grade_item;
} else {
// insert final grade - it will be needed later anyway
if (empty($final)) {
$final = new grade_grades(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false);
$final->insert();
$final->grade_item =& $this->grade_item;
$grade = new grade_grades(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false);
$grade->insert('system');
$grade->grade_item =& $this->grade_item;
} else if ($final->is_locked()) {
// no need to recalculate locked grades
$oldgrade = new object();
$oldgrade->finalgrade = $grade->finalgrade;
$oldgrade->rawgrade = $grade->rawgrade;
$oldgrade->rawgrademin = $grade->rawgrademin;
$oldgrade->rawgrademax = $grade->rawgrademax;
$oldgrade->rawscaleid = $grade->rawscaleid;
}
// locked grades are not regraded
if ($grade->is_locked()) {
return;
}
// can not use own final category grade in calculation
unset($grade_records[$this->grade_item->id]);
// if no grades calculation possible or grading not allowed clear both final and raw
if (empty($grades) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) {
$final->finalgrade = null;
$final->rawgrade = null;
$final->update();
if (empty($grade_records) 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
/// normalize the grades first - all will have value 0...1
// ungraded items are not used in aggreagation
foreach ($grades as $k=>$v) {
foreach ($grade_records as $k=>$v) {
if (is_null($v)) {
// null means no grade
unset($grades[$k]);
unset($grade_records[$k]);
continue;
}
$grades[$k] = grade_grades::standardise_score($v, $items[$k]->grademin, $items[$k]->grademax, 0, 1);
$grade_records[$k] = grade_grades::standardise_score($v, $items[$k]->grademin, $items[$k]->grademax, 0, 1);
}
//limit and sort
$this->apply_limit_rules($grades);
sort($grades, SORT_NUMERIC);
$this->apply_limit_rules($grade_records);
sort($grade_records, SORT_NUMERIC);
// let's see we have still enough grades to do any statisctics
if (count($grades) == 0) {
if (count($grade_records) == 0) {
// not enough attempts yet
if (!is_null($final->finalgrade) or !is_null($final->rawgrade)) {
$final->finalgrade = null;
$final->rawgrade = null;
$final->update();
$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($grades);
$num = count($grade_records);
$halfpoint = intval($num / 2);
if($num % 2 == 0) {
$rawgrade = ($grades[ceil($halfpoint)] + $grades[floor($halfpoint)]) / 2;
$rawgrade = ($grade_records[ceil($halfpoint)] + $grade_records[floor($halfpoint)]) / 2;
} else {
$rawgrade = $grades[$halfpoint];
$rawgrade = $grade_records[$halfpoint];
}
break;
case GRADE_AGGREGATE_MIN:
$rawgrade = reset($grades);
$rawgrade = reset($grade_records);
break;
case GRADE_AGGREGATE_MAX:
$rawgrade = array_pop($grades);
$rawgrade = array_pop($grade_records);
break;
case GRADE_AGGREGATE_MEAN_ALL: // Arithmetic average of all grade items including even NULLs; NULL grade caunted as minimum
$num = count($depends_on); // you can calculate sum from this one if you multiply it with count($this->depends_on() ;-)
$sum = array_sum($grades);
$num = count($items); // you can calculate sum from this one if you multiply it with count($this->depends_on() ;-)
$sum = array_sum($grade_records);
$rawgrade = $sum / $num;
break;
case GRADE_AGGREGATE_MODE: // the most common value, the highest one if multimode
$freq = array_count_values($grades);
$freq = array_count_values($grade_records);
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)
@ -500,24 +485,34 @@ class grade_category extends grade_object {
case GRADE_AGGREGATE_MEAN_GRADED: // Arithmetic average of all final grades, unfinished are not calculated
default:
$num = count($grades);
$sum = array_sum($grades);
$num = count($grade_records);
$sum = array_sum($grade_records);
$rawgrade = $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;
// recalculate the rawgrade back to requested range
$rawgrade = $this->grade_item->adjust_grade($rawgrade, 0, 1);
$grade->rawgrade = grade_grades::standardise_score($rawgrade, 0, 1, $grade->rawgrademin, $grade->rawgrademax);
// prepare update of new raw grade
$final->rawgrade = $rawgrade;
$final->finalgrade = null;
$final->rawgrademin = $this->grade_item->grademin;
$final->rawgrademax = $this->grade_item->grademax;
$final->rawscaleid = $this->grade_item->scaleid;
// calculate final grade
$grade->finalgrade = $this->grade_item->adjust_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
// TODO - add some checks to prevent updates when not needed
$final->update();
// 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;
}
/**
@ -739,7 +734,7 @@ class grade_category extends grade_object {
// create a new one
$grade_item = new grade_item($params, false);
$grade_item->gradetype = GRADE_TYPE_VALUE;
$grade_item->insert();
$grade_item->insert('system');
} else if (count($grade_items) == 1){
// found existing one
@ -797,7 +792,7 @@ class grade_category extends grade_object {
* @param int parentid
* @return boolean success
*/
function set_parent($parentid) {
function set_parent($parentid, $source=null) {
if ($this->parent == $parentid) {
return true;
}
@ -815,17 +810,16 @@ class grade_category extends grade_object {
return false;
}
$this->force_regrading(); // mark old parent as needing regrading
$this->force_regrading();
// set new parent category
$this->parent = $parentid;
$this->parent = $parent_category->id;
$this->parent_category =& $parent_category;
$this->path = null; // remove old path and depth - will be recalculated in update()
$this->parent_category = null;
$this->update();
$this->depth = null; // remove old path and depth - will be recalculated in update()
$this->update($source);
$grade_item = $this->load_grade_item();
$grade_item->parent_category = null;
return $grade_item->update(); // marks new parent as needing regrading too
return $grade_item->update($source);
}
/**

View file

@ -210,12 +210,6 @@ class grade_item extends grade_object {
*/
var $locktime = 0;
/**
* Whether or not the module instance referred to by this grade_item has been deleted.
* @var int $deleted
*/
var $deleted = 0;
/**
* If set, the whole column will be recalculated, then this flag will be switched off.
* @var boolean $needsupdate
@ -233,11 +227,10 @@ class grade_item extends grade_object {
$this->load_scale();
if ($this->qualifies_for_regrading()) {
return $this->force_regrading();
} else {
return parent::update($source);
$this->force_regrading();
}
return parent::update($source);
}
/**
@ -262,13 +255,12 @@ class grade_item extends grade_object {
$outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
$multfactordiff = $db_item->multfactor != $this->multfactor;
$plusfactordiff = $db_item->plusfactor != $this->plusfactor;
$deleteddiff = $db_item->deleted != $this->deleted;
$needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
$lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
|| $outcomeiddiff || $multfactordiff || $plusfactordiff || $deleteddiff || $needsupdatediff
|| $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
|| $lockeddiff);
}
@ -305,9 +297,7 @@ class grade_item extends grade_object {
return false;
}
if (!$this->is_category_item() and $category = $this->get_parent_category()) {
$category->force_regrading($source);
}
$this->force_regrading();
if ($grades = grade_grades::fetch_all(array('itemid'=>$this->id))) {
foreach ($grades as $grade) {
@ -319,7 +309,7 @@ class grade_item extends grade_object {
}
/**
* In addition to perform parent::insert(), this calls the grade_item's category's (if applicable) force_regrading() method.
* In addition to perform parent::insert(), calls force_regrading() method too.
* @param string $source from where was the object inserted (mod/forum, manual, etc.)
* @return int PK ID if successful, false otherwise
*/
@ -333,7 +323,7 @@ class grade_item extends grade_object {
// load scale if needed
$this->load_scale();
// add parent categroy if needed
// add parent category if needed
if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
$course_category = grade_category::fetch_course_category($this->courseid);
$this->categoryid = $course_category->id;
@ -508,107 +498,92 @@ class grade_item extends grade_object {
}
}
/**
* Mark regrading as finished successfully.
*/
function regrading_finished() {
$this->needsupdate = 0;
//do not use $this->update() because we do not want this logged in grade_item_history
set_field('grade_items', 'needsupdate', 0, 'id', $this->id);
if (!empty($this->locktime) and empty($this->locked) and $this->locktime < time()) {
// time to lock this grade_item
$this->set_locked(true);
}
}
/**
* Performs the necessary calculations on the grades_final referenced by this grade_item.
* Also resets the needsupdate flag once successfully performed.
*
* This function must be use ONLY from lib/gradeslib.php/grade_update_final_grades(),
* because the calculation must be done in correct order!!
* This function must be used ONLY from lib/gradeslib.php/grade_update_final_grades(),
* because the regrading must be done in correct order!!
*
* @return boolean true if ok, array of errors otherwise
* @return boolean true if ok, error string otherwise
*/
function update_final_grades() {
function update_final_grades($userid=null) {
global $CFG;
if ($this->is_locked()) {
// locked grade items already have correct final grades
$this->needsupdate = false;
$this->update();
if ($this->is_locked()) {
return true;
}
// calculation produces final value using formula from other final values
if ($this->is_calculated()) {
if ($this->compute()) {
$this->needsupdate = false;
$this->update();
if ($this->compute($userid)) {
return true;
} else {
return array("Could not calculate grades for grade item id:".$this->id); // TODO: improve and localize
return "Could not calculate grades for grade item"; // TODO: improve and localize
}
// aggregate the category grade
} else if ($this->is_category_item() or $this->is_course_item()) {
// aggregate category grade item
$category = $this->get_item_category();
if (!$category->generate_grades()) {
return ("Could not calculate raw category grades id:".$this->id); // TODO: improve and localize
$category->grade_item =& $this;
if ($category->generate_grades($userid)) {
return true;
} else {
return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
}
}
$errors = array();
// we need it to be really fast here ==> sql only
if ($rs = get_recordset('grade_grades', 'itemid', $this->id)) {
// normal grade item - just new final grades
$result = true;
if ($userid) {
$rs = get_recordset_select('grade_grades', "itemid={$this->id} AND userid=$userid");
} else {
$rs = get_recordset('grade_grades', 'itemid', $this->id);
}
if ($rs) {
if ($rs->RecordCount() > 0) {
while ($grade = rs_fetch_next_record($rs)) {
if (!empty($grade->locked)) {
while ($grade_record = rs_fetch_next_record($rs)) {
if (!empty($grade_record->locked)) {
// this grade is locked - final grade must be ok
continue;
}
if (!empty($errors) or is_null($grade->rawgrade)) {
// unset existing final grade when no raw present or error
if (!is_null($grade->finalgrade)) {
$g = new object();
$g->id = $grade->id;
$g->finalgrade = null;
if (!update_record('grade_grades', $g)) {
$errors[] = "Could not remove final grade for grade item:".$this->id;
$grade = new grade_grades($grade_record, false);
$grade->finalgrade = $this->adjust_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
if ($grade_record->finalgrade !== $grade->finalgrade) {
if (!$grade->update('system')) {
$result = "Internal error updating final grade";
}
}
} else {
$finalgrade = $this->adjust_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
if ($finalgrade != $grade->finalgrade) {
$g = new object();
$g->id = $grade->id;
$g->finalgrade = $finalgrade;
if (!update_record('grade_grades', $g)) {
$errors[] = "Could not update final grade for grade item:".$this->id;
}
}
// do not use $grade->is_locked() bacause item may be still locked!
// time to lock this grade?
if (!empty($grade->locktime) and empty($grade->locked) and $grade->locktime < time()) {
// time to lock this grade
$g = new object();
$g->id = $grade->id;
$g->locked = time();
update_record('grade_grades', $g);
}
$grade->locked = time();
$grade->grade_item =& $this;
$grade->set_locked(true);
}
}
}
}
if (!empty($errors)) {
$this->force_regrading();
return $errors;
} else {
// reset the regrading flag
$this->needsupdate = false;
$this->update();
// recheck the needsupdate just to make sure ;-)
if (empty($this->needsupdate) and !empty($this->locktime)
and empty($this->locked) and $this->locktime < time()) {
// time to lock this grade_item
$this->set_locked(true);
}
return true;
}
return $result;
}
/**
@ -679,32 +654,14 @@ class grade_item extends grade_object {
}
/**
* Sets this grade_item's needsupdate to true. Also looks at parent category, if any, and calls
* its force_regrading() method.
* This is triggered whenever any change in any raw grade may cause grade_finals
* for this grade_item to require an update. The flag needs to be propagated up all
* levels until it reaches the top category. This is then used to determine whether or not
* to regenerate the raw and final grades for each category grade_item.
* @param string $source from where was the object updated (mod/forum, manual, etc.)
* @return boolean Success or failure
* Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
* @return void
*/
function force_regrading($source=null) {
$this->needsupdate = true;
if (!parent::update($source)) {
return false;
}
if ($this->is_course_item()) {
// no parent
} else {
$parent = $this->load_parent_category();
$parent->force_regrading($source);
}
return true;
function force_regrading() {
$this->needsupdate = 1;
//mark this item and course item only - categories and calculated items are always regraded
$wheresql = "(itemtype='course' OR id={$this->id}) AND courseid={$this->courseid}";
set_field_select('grade_items', 'needsupdate', 1, $wheresql);
}
/**
@ -799,20 +756,41 @@ class grade_item extends grade_object {
return $this->item_category;
}
/**
* Is the grade item associated with category?
* @return boolean
*/
function is_category_item() {
return ($this->itemtype == 'category');
}
/**
* Is the grade item associated with course?
* @return boolean
*/
function is_course_item() {
return ($this->itemtype == 'course');
}
/**
* Is the grade item normal - associated with module, plugin or something else?
* @return boolean
*/
function is_normal_item() {
return ($this->itemtype != 'course' and $this->itemtype != 'category');
}
/**
* Returns grade item associated with the course
* @param int $courseid
* @return course item object
*/
function fetch_course_item($courseid) {
if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
return $course_item;
}
// first call - let category insert one
// first get category - it creates the associated grade item
$course_category = grade_category::fetch_course_category($courseid);
return grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'));
@ -998,13 +976,13 @@ class grade_item extends grade_object {
return false;
}
$this->force_regrading(); // mark old parent as needing regrading
$this->force_regrading();
// set new parent
$this->categoryid = $parentid;
$this->parent_category = null;
$this->categoryid = $parent_category->id;
$this->parent_category =& $parent_category;
return $this->update(); // mark new parent as needing regrading too
return $this->update();
}
/**
@ -1078,15 +1056,26 @@ class grade_item extends grade_object {
return false;
}
// TODO: we should IMO prevent modification of raw grades for course and categroy item too because
// there is no way to prevent overriding of it
// do not allow grade updates when item locked - this prevents fetching of grade from db
if ($this->is_locked()) {
return false;
}
$grade = new grade_grades(array('itemid'=>$this->id, 'userid'=>$userid, 'usermodified'=>$usermodified));
$grade->grade_item =& $this; // prevent db fetching of cached grade_item
if (!$grade = grade_grades::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
$grade = new grade_grades(array('itemid'=>$this->id, 'userid'=>$userid), false);
}
$grade->grade_item =& $this; // prevent db fetching of 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;
if (!empty($grade->id)) {
if ($grade->is_locked()) {
// do not update locked grades at all
return false;
@ -1098,43 +1087,42 @@ class grade_item extends grade_object {
return false;
}
}
//TODO: if grade tree does not need to be recalculated, try to update grades of all users in course and force_regrading only if failed
// fist copy current grademin/max and scale
$grade->rawgrademin = $this->grademin;
$grade->rawgrademax = $this->grademax;
$grade->rawscaleid = $this->scaleid;
if ($rawgrade !== false) {
// change of grade value requested
if (empty($grade->id)) {
$oldgrade = null;
$grade->rawgrade = $rawgrade;
$result = $grade->insert($source);
} else {
$oldgrade = $grade->rawgrade;
$grade->rawgrade = $rawgrade;
$result = $grade->update($source);
}
if (empty($grade->id)) {
$result = (boolean)$grade->insert($source);
} else 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) {
$result = $grade->update($source);
}
// do we have comment from teacher?
if ($result and $feedback !== false) {
if (empty($grade->id)) {
// create new grade
$oldgrade = null;
$result = $grade->insert($source);
}
$result = $result && $grade->update_feedback($feedback, $feedbackformat, $usermodified);
$result = $grade->update_feedback($feedback, $feedbackformat, $usermodified);
}
// TODO Handle history recording error, such as displaying a notice, but still return true
// This grade item needs update
if (!$this->needsupdate) {
$course_item = grade_item::fetch_course_item($this->courseid);
if (!$course_item->needsupdate) {
if (!grade_update_final_grades($this->courseid, $userid, $this)) {
$this->force_regrading();
}
} else {
$this->force_regrading();
}
}
if ($result) {
@ -1174,7 +1162,7 @@ class grade_item extends grade_object {
* The parameteres are taken from final grades of grade items in current course only.
* @return boolean false if error
*/
function compute() {
function compute($userid=null) {
global $CFG;
if (!$this->is_calculated()) {
@ -1198,9 +1186,15 @@ class grade_item extends grade_object {
// this itemid is added so that we use only one query for source and final grades
$gis = implode(',', array_merge($useditems, array($this->id)));
if ($userid) {
$usersql = "AND g.userid=$userid";
} else {
$usersql = "";
}
$sql = "SELECT g.*
FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi
WHERE gi.id = g.itemid AND gi.courseid={$this->courseid} AND gi.id IN ($gis)
WHERE gi.id = g.itemid AND gi.courseid={$this->courseid} AND gi.id IN ($gis) $usersql
ORDER BY g.userid";
$return = true;
@ -1209,37 +1203,35 @@ class grade_item extends grade_object {
if ($rs = get_recordset_sql($sql)) {
if ($rs->RecordCount() > 0) {
$prevuser = 0;
$grades = array();
$final = null;
$grade_records = array();
$oldgrade = null;
while ($used = rs_fetch_next_record($rs)) {
if ($used->userid != $prevuser) {
if (!$this->use_formula($prevuser, $grades, $useditems, $final)) {
if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
$return = false;
}
$prevuser = $used->userid;
$grades = array();
$final = null;
$grade_records = array();
$oldgrade = null;
}
if ($used->itemid == $this->id) {
$final = new grade_grades($used, false); // fetching from db is not needed
$final->grade_item =& $this;
$oldgrade = $used;
}
$grades['gi'.$used->itemid] = $used->finalgrade;
$grade_records['gi'.$used->itemid] = $used->finalgrade;
}
if (!$this->use_formula($prevuser, $grades, $useditems, $final)) {
if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
$return = false;
}
}
}
//TODO: we could return array of errors here
return $return;
}
/**
* internal function - does the final grade calculation
*/
function use_formula($userid, $params, $useditems, $final) {
function use_formula($userid, $params, $useditems, $oldgrade) {
if (empty($userid)) {
return true;
}
@ -1258,30 +1250,41 @@ class grade_item extends grade_object {
unset($params['gi'.$this->id]);
// insert final grade - will be needed later anyway
if (empty($final)) {
$final = new grade_grades(array('itemid'=>$this->id, 'userid'=>$userid), false);
$final->insert();
$final->grade_item =& $this;
if ($oldgrade) {
$grade = new grade_grades($oldgrade, false); // fetching from db is not needed
$grade->grade_item =& $this;
} else if ($final->is_locked()) {
// no need to recalculate locked grades
return;
} else {
$grade = new grade_grades(array('itemid'=>$this->id, 'userid'=>$userid, 'rawgrademin'=>null, 'rawgrademax'=>null, 'rawscaledi'=>null), false);
$grade->insert('system');
$grade->grade_item =& $this;
$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 grades
if ($grade->is_locked()) {
return;
}
// do the calculation
$this->formula->set_params($params);
$result = $this->formula->evaluate();
// store the result
// no raw grade for calculated grades - only final
$grade->rawgrademin = null;
$grade->rawgrademax = null;
$grade->rawscaleid = null;
$grade->rawgrade = null;
if ($result === false) {
// error during calculation
if (!is_null($final->finalgrade) or !is_null($final->rawgrade)) {
$final->finalgrade = null;
$final->rawgrade = null;
$final->update();
}
return false;
$grade->finalgrade = null;
} else {
// normalize
@ -1289,15 +1292,25 @@ class grade_item extends grade_object {
if ($this->gradetype == GRADE_TYPE_SCALE) {
$result = round($result+0.00001); // round scales upwards
}
// store only if final grade changed, remove raw grade because we do not need it
if ($final->finalgrade != $result or !is_null($final->rawgrade)) {
$final->finalgrade = $result;
$final->rawgrade = null;
$final->update();
$grade->finalgrade = $result;
}
// 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');
}
if ($result === false) {
return false;
} else {
return true;
}
}
}

View file

@ -299,74 +299,62 @@ function grade_is_locked($courseid, $itemtype, $itemmodule, $iteminstance, $item
/***** END OF PUBLIC API *****/
function grade_force_full_regrading($courseid) {
set_field('grade_items', 'needsupdate', 1, 'courseid', $courseid);
}
/**
* Updates all final grades in course.
*
* @param int $courseid
* @param boolean $regradeall force regrading of all items
*
* @return boolean true if ok, array of errors if problems found
* @param int $userid if specified, try to do a quick regrading of grades of this user only
* @param object $updated_item the item in which
* @return boolean true if ok, array of errors if problems found (item id is used as key)
*/
function grade_update_final_grades($courseid, $regradeall=false) {
function grade_update_final_grades($courseid, $userid=null, $updated_item=null) {
if ($regradeall) {
set_field('grade_items', 'needsupdate', 1, 'courseid', $courseid);
$course_item = grade_item::fetch_course_item($courseid);
if ($userid) {
// one raw grade updated for one user
if (empty($updated_item)) {
error("updated_item_id can not be null!");
}
if ($course_item->needsupdate) {
$updated_item->force_regrading();
return 'Can not do fast regrading after updating of raw grades';
}
if (!$grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
} else {
if (!$course_item->needsupdate) {
// nothing to do :-)
return true;
}
}
if (!$regradeall) {
$needsupdate = false;
$calculated = false;
$grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
$depends_on = array();
// first mark all category and calculated items as needing regrading
// this is slower, but 100% accurate - this function is called only when there is
// a change in grading setup, update of individual grade does not trigger this function
foreach ($grade_items as $gid=>$gitem) {
$grade_item =& $grade_items[$gid];
if ($grade_item->needsupdate) {
$needsupdate = true;
}
if ($grade_item->is_calculated()) {
$calculated = true;
}
if (!empty($updated_item) and $updated_item->id = $gid) {
$grade_items[$gid]->needsupdate = 1;
} else if ($grade_items[$gid]->is_category_item() or $grade_items[$gid]->is_calculated()) {
$grade_items[$gid]->needsupdate = 1;
}
if (!$needsupdate) {
// no update needed
return true;
} else if ($calculated) {
// flag all calculated grade items with needsupdate
// we want to make sure all are ok, this can be improved later with proper dependency calculation
foreach ($grade_items as $gid=>$gitem) {
$grade_item =& $grade_items[$gid];
if (!$grade_item->is_calculated()) {
continue;
// construct depends_on lookup array
$depends_on[$gid] = $grade_items[$gid]->depends_on();
}
$grade_item->update_from_db(); // make sure we have current data, it might have been updated in this loop already
if (!$grade_item->needsupdate) {
//force recalculation and forced update of all parents
$grade_item->force_regrading();
}
}
// again make sure all date is up-to-date - the needsupdate flag might have changed
foreach ($grade_items as $gid=>$gitem) {
$grade_item =& $grade_items[$gid];
$grade_item->update_from_db();
unset($grade_item->category);
}
}
}
$errors = array();
// now the hard way with calculated grade_items or categories
$finalitems = array();
$finalids = array();
while (count($grade_items) > 0) {
$count = 0;
$count = 0; // count how many items were updated in this cycle
foreach ($grade_items as $gid=>$gitem) {
$grade_item =& $grade_items[$gid];
if (!$grade_item->needsupdate) {
@ -376,32 +364,35 @@ function grade_update_final_grades($courseid, $regradeall=false) {
continue;
}
//do we have all data for finalizing of this item?
$depends_on = $grade_item->depends_on();
$doupdate = true;
foreach ($depends_on as $did) {
foreach ($depends_on[$gid] as $did) {
if (!in_array($did, $finalids)) {
$doupdate = false;
break;
}
}
//oki - let's update, calculate or aggregate :-)
if ($doupdate) {
$result = $grade_item->update_final_grades();
if ($result !== true) {
$errors = array_merge($errors, $result);
} else {
$result = $grade_item->update_final_grades($userid);
if ($result === true) {
$grade_item->regrading_finished();
$count++;
$finalitems[$gid] = $grade_item;
$finalids[] = $gid;
unset($grade_items[$gid]);
} else {
$grade_item->force_regrading();
$errors[$gid] = $result;
}
}
}
if ($count == 0) {
foreach($grade_items as $grade_item) {
$errors[] = 'Probably circular reference or broken calculation formula in grade_item id:'.$grade_item->id; // TODO: localize
$grade_item->force_regrading();
$errors[$grade_item->id] = 'Probably circular reference or broken calculation formula'; // TODO: localize
}
break;
}

View file

@ -548,7 +548,7 @@ function addslashes_recursive($var) {
} else if (is_string($var)) {
$new_var = addslashes($var);
} else {
} else { // nulls, integers, etc.
$new_var = $var;
}