. /** * Library of functions for gradebook - both public and internal * * @package moodlecore * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** Include essential files */ require_once($CFG->libdir . '/grade/constants.php'); require_once($CFG->libdir . '/grade/grade_category.php'); require_once($CFG->libdir . '/grade/grade_item.php'); require_once($CFG->libdir . '/grade/grade_grade.php'); require_once($CFG->libdir . '/grade/grade_scale.php'); require_once($CFG->libdir . '/grade/grade_outcome.php'); ///////////////////////////////////////////////////////////////////// ///// Start of public API for communication with modules/blocks ///// ///////////////////////////////////////////////////////////////////// /** * Submit new or update grade; update/create grade_item definition. Grade must have userid specified, * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded', missing property * or key means do not change existing. * * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax', * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones. * * Manual, course or category items can not be updated by this function. * @access public * @global object * @global object * @global object * @param string $source source of the grade such as 'mod/assignment' * @param int $courseid id of course * @param string $itemtype type of grade item - mod, block * @param string $itemmodule more specific then $itemtype - assignment, forum, etc.; maybe NULL for some item types * @param int $iteminstance instance it of graded subject * @param int $itemnumber most probably 0, modules can use other numbers when having more than one grades for each user * @param mixed $grades grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only * @param mixed $itemdetails object or array describing the grading item, NULL if no change */ function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades=NULL, $itemdetails=NULL) { global $USER, $CFG, $DB; // only following grade_item properties can be changed in this function $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden'); // list of 10,5 numeric fields $floats = array('grademin', 'grademax', 'multfactor', 'plusfactor'); // grade item identification $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber'); if (is_null($courseid) or is_null($itemtype)) { debugging('Missing courseid or itemtype'); return GRADE_UPDATE_FAILED; } if (!$grade_items = grade_item::fetch_all($params)) { // create a new one $grade_item = false; } else if (count($grade_items) == 1){ $grade_item = reset($grade_items); unset($grade_items); //release memory } else { debugging('Found more than one grade item'); return GRADE_UPDATE_MULTIPLE; } if (!empty($itemdetails['deleted'])) { if ($grade_item) { if ($grade_item->delete($source)) { return GRADE_UPDATE_OK; } else { return GRADE_UPDATE_FAILED; } } return GRADE_UPDATE_OK; } /// Create or update the grade_item if needed if (!$grade_item) { if ($itemdetails) { $itemdetails = (array)$itemdetails; // grademin and grademax ignored when scale specified if (array_key_exists('scaleid', $itemdetails)) { if ($itemdetails['scaleid']) { unset($itemdetails['grademin']); unset($itemdetails['grademax']); } } foreach ($itemdetails as $k=>$v) { if (!in_array($k, $allowed)) { // ignore it continue; } if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) { // no grade item needed! return GRADE_UPDATE_OK; } $params[$k] = $v; } } $grade_item = new grade_item($params); $grade_item->insert(); } else { if ($grade_item->is_locked()) { // no notice() here, test returned value instead! return GRADE_UPDATE_ITEM_LOCKED; } if ($itemdetails) { $itemdetails = (array)$itemdetails; $update = false; foreach ($itemdetails as $k=>$v) { if (!in_array($k, $allowed)) { // ignore it continue; } if (in_array($k, $floats)) { if (grade_floats_different($grade_item->{$k}, $v)) { $grade_item->{$k} = $v; $update = true; } } else { if ($grade_item->{$k} != $v) { $grade_item->{$k} = $v; $update = true; } } } if ($update) { $grade_item->update(); } } } /// reset grades if requested if (!empty($itemdetails['reset'])) { $grade_item->delete_all_grades('reset'); return GRADE_UPDATE_OK; } /// Some extra checks // do we use grading? if ($grade_item->gradetype == GRADE_TYPE_NONE) { return GRADE_UPDATE_OK; } // no grade submitted if (empty($grades)) { return GRADE_UPDATE_OK; } /// Finally start processing of grades if (is_object($grades)) { $grades = array($grades->userid=>$grades); } else { if (array_key_exists('userid', $grades)) { $grades = array($grades['userid']=>$grades); } } /// normalize and verify grade array foreach($grades as $k=>$g) { if (!is_array($g)) { $g = (array)$g; $grades[$k] = $g; } if (empty($g['userid']) or $k != $g['userid']) { debugging('Incorrect grade array index, must be user id! Grade ignored.'); unset($grades[$k]); } } if (empty($grades)) { return GRADE_UPDATE_FAILED; } $count = count($grades); if ($count > 0 and $count < 200) { list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED, $start='uid0'); $params['gid'] = $grade_item->id; $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids"; } else { $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid"; $params = array('gid'=>$grade_item->id); } $rs = $DB->get_recordset_sql($sql, $params); $failed = false; while (count($grades) > 0) { $grade_grade = null; $grade = null; foreach ($rs as $gd) { $userid = $gd->userid; if (!isset($grades[$userid])) { // this grade not requested, continue continue; } // existing grade requested $grade = $grades[$userid]; $grade_grade = new grade_grade($gd, false); unset($grades[$userid]); break; } if (is_null($grade_grade)) { if (count($grades) == 0) { // no more grades to process break; } $grade = reset($grades); $userid = $grade['userid']; $grade_grade = new grade_grade(array('itemid'=>$grade_item->id, 'userid'=>$userid), false); $grade_grade->load_optional_fields(); // add feedback and info too unset($grades[$userid]); } $rawgrade = false; $feedback = false; $feedbackformat = FORMAT_MOODLE; $usermodified = $USER->id; $datesubmitted = null; $dategraded = null; if (array_key_exists('rawgrade', $grade)) { $rawgrade = $grade['rawgrade']; } if (array_key_exists('feedback', $grade)) { $feedback = $grade['feedback']; } if (array_key_exists('feedbackformat', $grade)) { $feedbackformat = $grade['feedbackformat']; } if (array_key_exists('usermodified', $grade)) { $usermodified = $grade['usermodified']; } if (array_key_exists('datesubmitted', $grade)) { $datesubmitted = $grade['datesubmitted']; } if (array_key_exists('dategraded', $grade)) { $dategraded = $grade['dategraded']; } // update or insert the grade if (!$grade_item->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified, $dategraded, $datesubmitted, $grade_grade)) { $failed = true; } } if ($rs) { $rs->close(); } if (!$failed) { return GRADE_UPDATE_OK; } else { return GRADE_UPDATE_FAILED; } } /** * Updates outcomes of user * Manual outcomes can not be updated. * * @access public * @param string $source source of the grade such as 'mod/assignment' * @param int $courseid id of course * @param string $itemtype 'mod', 'block' * @param string $itemmodule 'forum, 'quiz', etc. * @param int $iteminstance id of the item module * @param int $userid ID of the graded user * @param array $data array itemnumber=>outcomegrade */ function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) { if ($items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) { foreach ($items as $item) { if (!array_key_exists($item->itemnumber, $data)) { continue; } $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber]; $item->update_final_grade($userid, $grade, $source); } } } /** * Returns grading information for given activity - optionally with users grades * Manual, course or category items can not be queried. * * @access public * @global object * @param int $courseid id of course * @param string $itemtype 'mod', 'block' * @param string $itemmodule 'forum, 'quiz', etc. * @param int $iteminstance id of the item module * @param int $userid_or_ids optional id of the graded user or array of ids; if userid not used, returns only information about grade_item * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers */ function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) { global $CFG; $return = new object(); $return->items = array(); $return->outcomes = array(); $course_item = grade_item::fetch_course_item($courseid); $needsupdate = array(); if ($course_item->needsupdate) { $result = grade_regrade_final_grades($courseid); if ($result !== true) { $needsupdate = array_keys($result); } } if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) { foreach ($grade_items as $grade_item) { $decimalpoints = null; if (empty($grade_item->outcomeid)) { // prepare information about grade item $item = new object(); $item->itemnumber = $grade_item->itemnumber; $item->scaleid = $grade_item->scaleid; $item->name = $grade_item->get_name(); $item->grademin = $grade_item->grademin; $item->grademax = $grade_item->grademax; $item->gradepass = $grade_item->gradepass; $item->locked = $grade_item->is_locked(); $item->hidden = $grade_item->is_hidden(); $item->grades = array(); switch ($grade_item->gradetype) { case GRADE_TYPE_NONE: continue; case GRADE_TYPE_VALUE: $item->scaleid = 0; break; case GRADE_TYPE_TEXT: $item->scaleid = 0; $item->grademin = 0; $item->grademax = 0; $item->gradepass = 0; break; } if (empty($userid_or_ids)) { $userids = array(); } else if (is_array($userid_or_ids)) { $userids = $userid_or_ids; } else { $userids = array($userid_or_ids); } if ($userids) { $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true); foreach ($userids as $userid) { $grade_grades[$userid]->grade_item =& $grade_item; $grade = new object(); $grade->grade = $grade_grades[$userid]->finalgrade; $grade->locked = $grade_grades[$userid]->is_locked(); $grade->hidden = $grade_grades[$userid]->is_hidden(); $grade->overridden = $grade_grades[$userid]->overridden; $grade->feedback = $grade_grades[$userid]->feedback; $grade->feedbackformat = $grade_grades[$userid]->feedbackformat; $grade->usermodified = $grade_grades[$userid]->usermodified; $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted(); $grade->dategraded = $grade_grades[$userid]->get_dategraded(); // create text representation of grade if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) { $grade->grade = null; $grade->str_grade = '-'; $grade->str_long_grade = $grade->str_grade; } else if (in_array($grade_item->id, $needsupdate)) { $grade->grade = false; $grade->str_grade = get_string('error'); $grade->str_long_grade = $grade->str_grade; } else if (is_null($grade->grade)) { $grade->str_grade = '-'; $grade->str_long_grade = $grade->str_grade; } else { $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item); if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) { $grade->str_long_grade = $grade->str_grade; } else { $a = new object(); $a->grade = $grade->str_grade; $a->max = grade_format_gradevalue($grade_item->grademax, $grade_item); $grade->str_long_grade = get_string('gradelong', 'grades', $a); } } // create html representation of feedback if (is_null($grade->feedback)) { $grade->str_feedback = ''; } else { $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat); } $item->grades[$userid] = $grade; } } $return->items[$grade_item->itemnumber] = $item; } else { if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) { debugging('Incorect outcomeid found'); continue; } // outcome info $outcome = new object(); $outcome->itemnumber = $grade_item->itemnumber; $outcome->scaleid = $grade_outcome->scaleid; $outcome->name = $grade_outcome->get_name(); $outcome->locked = $grade_item->is_locked(); $outcome->hidden = $grade_item->is_hidden(); if (empty($userid_or_ids)) { $userids = array(); } else if (is_array($userid_or_ids)) { $userids = $userid_or_ids; } else { $userids = array($userid_or_ids); } if ($userids) { $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true); foreach ($userids as $userid) { $grade_grades[$userid]->grade_item =& $grade_item; $grade = new object(); $grade->grade = $grade_grades[$userid]->finalgrade; $grade->locked = $grade_grades[$userid]->is_locked(); $grade->hidden = $grade_grades[$userid]->is_hidden(); $grade->feedback = $grade_grades[$userid]->feedback; $grade->feedbackformat = $grade_grades[$userid]->feedbackformat; $grade->usermodified = $grade_grades[$userid]->usermodified; // create text representation of grade if (in_array($grade_item->id, $needsupdate)) { $grade->grade = false; $grade->str_grade = get_string('error'); } else if (is_null($grade->grade)) { $grade->grade = 0; $grade->str_grade = get_string('nooutcome', 'grades'); } else { $grade->grade = (int)$grade->grade; $scale = $grade_item->load_scale(); $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]); } // create html representation of feedback if (is_null($grade->feedback)) { $grade->str_feedback = ''; } else { $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat); } $outcome->grades[$userid] = $grade; } } if (isset($return->outcomes[$grade_item->itemnumber])) { // itemnumber duplicates - lets fix them! $newnumber = $grade_item->itemnumber + 1; while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) { $newnumber++; } $outcome->itemnumber = $newnumber; $grade_item->itemnumber = $newnumber; $grade_item->update('system'); } $return->outcomes[$grade_item->itemnumber] = $outcome; } } } // sort results using itemnumbers ksort($return->items, SORT_NUMERIC); ksort($return->outcomes, SORT_NUMERIC); return $return; } /////////////////////////////////////////////////////////////////// ///// End of public API for communication with modules/blocks ///// /////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////// ///// Internal API: used by gradebook plugins and Moodle core ///// /////////////////////////////////////////////////////////////////// /** * Returns course gradebook setting * * @global object * @param int $courseid * @param string $name of setting, maybe null if reset only * @param string $default * @param bool $resetcache force reset of internal static cache * @return string value, NULL if no setting */ function grade_get_setting($courseid, $name, $default=null, $resetcache=false) { global $DB; static $cache = array(); if ($resetcache or !array_key_exists($courseid, $cache)) { $cache[$courseid] = array(); } else if (is_null($name)) { return null; } else if (array_key_exists($name, $cache[$courseid])) { return $cache[$courseid][$name]; } if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) { $result = null; } else { $result = $data->value; } if (is_null($result)) { $result = $default; } $cache[$courseid][$name] = $result; return $result; } /** * Returns all course gradebook settings as object properties * * @global object * @param int $courseid * @return object */ function grade_get_settings($courseid) { global $DB; $settings = new object(); $settings->id = $courseid; if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) { foreach ($records as $record) { $settings->{$record->name} = $record->value; } } return $settings; } /** * Add/update course gradebook setting * * @global object * @param int $courseid * @param string $name of setting * @param string value, NULL means no setting==remove * @return void */ function grade_set_setting($courseid, $name, $value) { global $DB; if (is_null($value)) { $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name)); } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) { $data = new object(); $data->courseid = $courseid; $data->name = $name; $data->value = $value; $DB->insert_record('grade_settings', $data); } else { $data = new object(); $data->id = $existing->id; $data->value = $value; $DB->update_record('grade_settings', $data); } grade_get_setting($courseid, null, null, true); // reset the cache } /** * 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 $displaytype type of display - GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER * @param int $decimals 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: return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized); case GRADE_DISPLAY_TYPE_PERCENTAGE: return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized); case GRADE_DISPLAY_TYPE_LETTER: return grade_format_gradevalue_letter($value, $grade_item); case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE: return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' . grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')'; case GRADE_DISPLAY_TYPE_REAL_LETTER: return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' . grade_format_gradevalue_letter($value, $grade_item) . ')'; case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL: return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' . grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')'; case GRADE_DISPLAY_TYPE_LETTER_REAL: return grade_format_gradevalue_letter($value, $grade_item) . ' (' . grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')'; case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE: return grade_format_gradevalue_letter($value, $grade_item) . ' (' . grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')'; case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER: return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' . grade_format_gradevalue_letter($value, $grade_item) . ')'; default: return ''; } } /** * @todo Document this function */ function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) { if ($grade_item->gradetype == GRADE_TYPE_SCALE) { if (!$scale = $grade_item->load_scale()) { return get_string('error'); } $value = $grade_item->bounded_grade($value); return format_string($scale->scale_items[$value-1]); } else { return format_float($value, $decimals, $localized); } } /** * @todo Document this function */ function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) { $min = $grade_item->grademin; $max = $grade_item->grademax; if ($min == $max) { return ''; } $value = $grade_item->bounded_grade($value); $percentage = (($value-$min)*100)/($max-$min); return format_float($percentage, $decimals, $localized).' %'; } /** * @todo Document this function */ 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 } /** * Returns grade options for gradebook category menu * * @param int $courseid * @param bool $includenew include option for new category (-1) * @return array of grade categories in course */ function grade_get_categories_menu($courseid, $includenew=false) { $result = array(); if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) { //make sure course category exists if (!grade_category::fetch_course_category($courseid)) { debugging('Can not create course grade category!'); return $result; } $categories = grade_category::fetch_all(array('courseid'=>$courseid)); } foreach ($categories as $key=>$category) { if ($category->is_course_category()) { $result[$category->id] = get_string('uncategorised', 'grades'); unset($categories[$key]); } } if ($includenew) { $result[-1] = get_string('newcategory', 'grades'); } $cats = array(); foreach ($categories as $category) { $cats[$category->id] = $category->get_name(); } asort($cats, SORT_LOCALE_STRING); return ($result+$cats); } /** * Returns grade letters array used in context * * @param object $context object or null for defaults * @return array of grade_boundary=>letter_string */ function grade_get_letters($context=null) { global $DB; if (empty($context)) { //default grading letters return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F'); } static $cache = array(); if (array_key_exists($context->id, $cache)) { return $cache[$context->id]; } if (count($cache) > 100) { $cache = array(); // cache size limit } $letters = array(); $contexts = get_parent_contexts($context); array_unshift($contexts, $context->id); foreach ($contexts as $ctxid) { if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) { foreach ($records as $record) { $letters[$record->lowerboundary] = $record->letter; } } if (!empty($letters)) { $cache[$context->id] = $letters; return $letters; } } $letters = grade_get_letters(null); $cache[$context->id] = $letters; return $letters; } /** * Verify new value of idnumber - checks for uniqueness of new idnumbers, old are kept intact * * @global object * @param string idnumber string (with magic quotes) * @param int $courseid id numbers are course unique only * @param object $grade_item is item idnumber * @param object $cm used for course module idnumbers and items attached to modules * @return boolean true means idnumber ok */ function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) { global $DB; if ($idnumber == '') { //we allow empty idnumbers return true; } // keep existing even when not unique if ($cm and $cm->idnumber == $idnumber) { return true; } else if ($grade_item and $grade_item->idnumber == $idnumber) { return true; } if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) { return false; } if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) { return false; } return true; } /** * Force final grade recalculation in all course items * * @global object * @param int $courseid */ function grade_force_full_regrading($courseid) { global $DB; $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid)); } /** * Forces regrading of all site grades - usualy when chanign site setings * @global object * @global object */ function grade_force_site_regrading() { global $CFG, $DB; $DB->set_field('grade_items', 'needsupdate', 1); } /** * Updates all final grades in course. * * @param int $courseid * @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_regrade_final_grades($courseid, $userid=null, $updated_item=null) { $course_item = grade_item::fetch_course_item($courseid); if ($userid) { // one raw grade updated for one user if (empty($updated_item)) { print_error("cannotbenull", 'debug', '', "updated_item"); } if ($course_item->needsupdate) { $updated_item->force_regrading(); return array($course_item->id =>'Can not do fast regrading after updating of raw grades'); } } else { if (!$course_item->needsupdate) { // nothing to do :-) return true; } } $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 foreach ($grade_items as $gid=>$gitem) { if (!empty($updated_item) and $updated_item->id == $gid) { $grade_items[$gid]->needsupdate = 1; } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) { $grade_items[$gid]->needsupdate = 1; } // construct depends_on lookup array $depends_on[$gid] = $grade_items[$gid]->depends_on(); } $errors = array(); $finalids = array(); $gids = array_keys($grade_items); $failed = 0; while (count($finalids) < count($gids)) { // work until all grades are final or error found $count = 0; foreach ($gids as $gid) { if (in_array($gid, $finalids)) { continue; // already final } if (!$grade_items[$gid]->needsupdate) { $finalids[] = $gid; // we can make it final - does not need update continue; } $doupdate = true; foreach ($depends_on[$gid] as $did) { if (!in_array($did, $finalids)) { $doupdate = false; continue; // this item depends on something that is not yet in finals array } } //oki - let's update, calculate or aggregate :-) if ($doupdate) { $result = $grade_items[$gid]->regrade_final_grades($userid); if ($result === true) { $grade_items[$gid]->regrading_finished(); $grade_items[$gid]->check_locktime(); // do the locktime item locking $count++; $finalids[] = $gid; } else { $grade_items[$gid]->force_regrading(); $errors[$gid] = $result; } } } if ($count == 0) { $failed++; } else { $failed = 0; } if ($failed > 1) { foreach($gids as $gid) { if (in_array($gid, $finalids)) { continue; // this one is ok } $grade_items[$gid]->force_regrading(); $errors[$grade_items[$gid]->id] = 'Probably circular reference or broken calculation formula'; // TODO: localize } break; // oki, found error } } if (count($errors) == 0) { if (empty($userid)) { // do the locktime locking of grades, but only when doing full regrading grade_grade::check_locktime_all($gids); } return true; } else { return $errors; } } /** * Refetches data from all course activities * * @global object * @global object * @param int $courseid * @param string $modname * @return void */ function grade_grab_course_grades($courseid, $modname=null) { global $CFG, $DB; if ($modname) { $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname FROM {".$modname."} a, {course_modules} cm, {modules} m WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid"; $params = array('modname'=>$modname, 'courseid'=>$courseid); if ($modinstances = $DB->get_records_sql($sql, $params)) { foreach ($modinstances as $modinstance) { grade_update_mod_grades($modinstance); } } return; } if (!$mods = get_plugin_list('mod') ) { print_error('nomodules', 'debug'); } foreach ($mods as $mod => $fullmod) { if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it continue; } // include the module lib once if (file_exists($fullmod.'/lib.php')) { // get all instance of the activity $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname FROM {".$mod."} a, {course_modules} cm, {modules} m WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid"; $params = array('mod'=>$mod, 'courseid'=>$courseid); if ($modinstances = $DB->get_records_sql($sql, $params)) { foreach ($modinstances as $modinstance) { grade_update_mod_grades($modinstance); } } } } } /** * Force full update of module grades in central gradebook * * @global object * @global object * @param object $modinstance object with extra cmidnumber and modname property * @param int $userid * @return boolean success */ function grade_update_mod_grades($modinstance, $userid=0) { global $CFG, $DB; $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname; if (!file_exists($fullmod.'/lib.php')) { debugging('missing lib.php file in module ' . $modinstance->modname); return false; } include_once($fullmod.'/lib.php'); $updategradesfunc = $modinstance->modname.'_update_grades'; $updateitemfunc = $modinstance->modname.'_grade_item_update'; if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) { //new grading supported, force updating of grades $updateitemfunc($modinstance); $updategradesfunc($modinstance, $userid); } else { // mudule does not support grading?? } return true; } /** * Remove grade letters for given context * * @global object * @param object $context * @param bool $showfeedback */ function remove_grade_letters($context, $showfeedback) { global $DB, $OUTPUT; $strdeleted = get_string('deleted'); $DB->delete_records('grade_letters', array('contextid'=>$context->id)); if ($showfeedback) { echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades')); } } /** * Remove all grade related course data - history is kept * * @global object * @param int $courseid * @param bool $showfeedback print feedback */ function remove_course_grades($courseid, $showfeedback) { global $DB, $OUTPUT; $strdeleted = get_string('deleted'); $course_category = grade_category::fetch_course_category($courseid); $course_category->delete('coursedelete'); if ($showfeedback) { echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades')); } if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) { foreach ($outcomes as $outcome) { $outcome->delete('coursedelete'); } } $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid)); if ($showfeedback) { echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades')); } if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) { foreach ($scales as $scale) { $scale->delete('coursedelete'); } } if ($showfeedback) { echo $OUTPUT->notification($strdeleted.' - '.get_string('scales')); } $DB->delete_records('grade_settings', array('courseid'=>$courseid)); if ($showfeedback) { echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades')); } } /** * Called when course category deleted - cleanup gradebook * * @global object * @param int $categoryid course category id * @param int $newparentid empty means everything deleted, otherwise id of category where content moved * @param bool $showfeedback print feedback */ function grade_course_category_delete($categoryid, $newparentid, $showfeedback) { global $DB; $context = get_context_instance(CONTEXT_COURSECAT, $categoryid); $DB->delete_records('grade_letters', array('contextid'=>$context->id)); } /** * Does gradebook cleanup when module uninstalled. * * @global object * @global object * @param string $modname */ function grade_uninstalled_module($modname) { global $CFG, $DB; $sql = "SELECT * FROM {grade_items} WHERE itemtype='mod' AND itemmodule=?"; // go all items for this module and delete them including the grades if ($rs = $DB->get_recordset_sql($sql, array($modname))) { foreach ($rs as $item) { $grade_item = new grade_item($item, false); $grade_item->delete('moduninstall'); } $rs->close(); } } /** * Grading cron job * * @global object * @global object */ function grade_cron() { global $CFG, $DB; $now = time(); $sql = "SELECT i.* FROM {grade_items} i WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS ( SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)"; // go through all courses that have proper final grades and lock them if needed if ($rs = $DB->get_recordset_sql($sql, array($now))) { foreach ($rs as $item) { $grade_item = new grade_item($item, false); $grade_item->locked = $now; $grade_item->update('locktime'); } $rs->close(); } $grade_inst = new grade_grade(); $fields = 'g.'.implode(',g.', $grade_inst->required_fields); $sql = "SELECT $fields FROM {grade_grades} g, {grade_items} i WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS ( SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)"; // go through all courses that have proper final grades and lock them if needed if ($rs = $DB->get_recordset_sql($sql, array($now))) { foreach ($rs as $grade) { $grade_grade = new grade_grade($grade, false); $grade_grade->locked = $now; $grade_grade->update('locktime'); } $rs->close(); } //TODO: do not run this cleanup every cron invocation // cleanup history tables if (!empty($CFG->gradehistorylifetime)) { // value in days $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24); $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history'); foreach ($tables as $table) { if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) { mtrace(" Deleted old grade history records from '$table'"); } } } } /** * Resel all course grades * * @param int $courseid * @return bool success */ function grade_course_reset($courseid) { // no recalculations grade_force_full_regrading($courseid); $grade_items = grade_item::fetch_all(array('courseid'=>$courseid)); foreach ($grade_items as $gid=>$grade_item) { $grade_item->delete_all_grades('reset'); } //refetch all grades grade_grab_course_grades($courseid); // recalculate all grades grade_regrade_final_grades($courseid); return true; } /** * Convert number to 5 decimalfloat, empty string or null db compatible format * (we need this to decide if db value changed) * * @param mixed $number * @return mixed float or null */ function grade_floatval($number) { if (is_null($number) or $number === '') { return null; } // we must round to 5 digits to get the same precision as in 10,5 db fields // note: db rounding for 10,5 is different from php round() function return round($number, 5); } /** * Compare two float numbers safely. Uses 5 decimals php precision. Nulls accepted too. * Used for skipping of db updates * * @param float $f1 * @param float $f2 * @return bool true if different */ function grade_floats_different($f1, $f2) { // note: db rounding for 10,5 is different from php round() function return (grade_floatval($f1) !== grade_floatval($f2)); } /** * Compare two float numbers safely. Uses 5 decimals php precision. * * Do not use rounding for 10,5 at the database level as the results may be * different from php round() function. * * @since 2.0 * @param float $f1 * @param float $f2 * @return bool true if the values should be considered as the same grades */ function grade_floats_equal($f1, $f2) { return (grade_floatval($f1) === grade_floatval($f2)); }