// Recalculate grades if needed
if ($this->qualifies_for_regrading()) {
- if (!parent::update($source)) {
- return false;
- }
- $this->grade_item->force_regrading($source);
- return true;
-
- } else {
- return parent::update($source);
+ $this->force_regrading();
}
+
+ return parent::update($source);
}
/**
return false;
}
+ $this->force_regrading();
+
$grade_item = $this->load_grade_item();
$parent = $this->load_parent_category();
return false;
}
+ $this->force_regrading();
+
// build path and depth
$this->update($source);
}
/**
- * 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();
}
/**
* 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();
$this->grade_item->load_scale();
-
// find grade items of immediate children (category or grade items)
$depends_on = $this->grade_item->depends_on();
- $items = array();
+ 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);
+ }
- foreach($depends_on as $dep) {
- $items[$dep] = grade_item::fetch(array('id'=>$dep));
+ if ($userid) {
+ $usersql = "AND g.userid=$userid";
+ } else {
+ $usersql = "";
}
- // where to look for final grades - include or grade item too
+ // 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
}
}
/**
* 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;
- // 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;
+ } else {
+ // insert final grade - it will be needed later anyway
+ $grade = new grade_grades(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;
+ }
- } else if ($final->is_locked()) {
- // no need to recalculate locked grades
+ // 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)
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;
}
/**
// 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
* @param int parentid
* @return boolean success
*/
- function set_parent($parentid) {
+ function set_parent($parentid, $source=null) {
if ($this->parent == $parentid) {
return true;
}
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);
}
/**
*/
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
$this->load_scale();
if ($this->qualifies_for_regrading()) {
- return $this->force_regrading();
-
- } else {
- return parent::update($source);
+ $this->force_regrading();
}
+
+ return parent::update($source);
}
/**
$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);
}
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) {
}
/**
- * 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
*/
// 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;
}
}
+ /**
+ * 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;
+ // locked grade items already have correct final grades
if ($this->is_locked()) {
- // locked grade items already have correct final grades
- $this->needsupdate = false;
- $this->update();
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;
- }
- }
-
- } else {
- $finalgrade = $this->adjust_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
+ $grade = new grade_grades($grade_record, false);
+ $grade->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;
- }
+ if ($grade_record->finalgrade !== $grade->finalgrade) {
+ if (!$grade->update('system')) {
+ $result = "Internal error updating final grade";
}
+ }
- // do not use $grade->is_locked() bacause item may be still locked!
- 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);
- }
+ // time to lock this grade?
+ if (!empty($grade->locktime) and empty($grade->locked) and $grade->locktime < time()) {
+ $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;
}
/**
}
/**
- * 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);
}
/**
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'));
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();
}
/**
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 (!empty($grade->id)) {
- if ($grade->is_locked()) {
- // do not update locked grades at all
- return false;
- }
+ if (!$grade = grade_grades::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
+ $grade = new grade_grades(array('itemid'=>$this->id, 'userid'=>$userid), false);
+ }
- if (!empty($grade->locktime) and $grade->locktime < time()) {
- // do not update grades that should be already locked
- // this does not solve all problems, cron is still needed to recalculate the final grades periodically
- return 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 ($grade->is_locked()) {
+ // do not update locked grades at all
+ 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
+ if (!empty($grade->locktime) and $grade->locktime < time()) {
+ // do not update grades that should be already locked
+ // this does not solve all problems, cron is still needed to recalculate the final grades periodically
+ return false;
+ }
// fist copy current grademin/max and scale
$grade->rawgrademin = $this->grademin;
$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);
+ $grade->rawgrade = $rawgrade;
+ }
- } 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
- $this->force_regrading();
+ 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) {
* 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()) {
// 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;
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;
}
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
if ($this->gradetype == GRADE_TYPE_SCALE) {
$result = round($result+0.00001); // round scales upwards
}
+ $grade->finalgrade = $result;
+ }
- // 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();
- }
+ // 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;
}
+
}
}
/***** 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) {
-
- if ($regradeall) {
- set_field('grade_items', 'needsupdate', 1, 'courseid', $courseid);
- }
+function grade_update_final_grades($courseid, $userid=null, $updated_item=null) {
- if (!$grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
- return true;
- }
+ $course_item = grade_item::fetch_course_item($courseid);
- if (!$regradeall) {
- $needsupdate = false;
- $calculated = false;
- 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 ($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 (!$needsupdate) {
- // no update needed
+ } else {
+ if (!$course_item->needsupdate) {
+ // nothing to do :-)
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;
- }
- $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);
- }
}
}
+ $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) {
+ 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;
+ }
+ // construct depends_on lookup array
+ $depends_on[$gid] = $grade_items[$gid]->depends_on();
+ }
$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) {
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;
}