public function test_task_timefilter() { $task = new \local_gradelock\task\lock_grades(); $grade_grade = new grade_grade(); $grade_grade->itemid = $this->grade_items[0]->id; $grade_grade->userid = 10; $grade_grade->rawgrade = 88; $grade_grade->rawgrademax = 110; $grade_grade->rawgrademin = 18; $grade_grade->load_grade_item(); $grade_grade->insert(); $grade_grade->grade_item->update_final_grade($this->user[0]->id, 100, 'gradebook', '', FORMAT_MOODLE); $grade_grade->update(); $task->execute(); $grade_grade = grade_grade::fetch(array('userid' => $this->user[0]->id, 'itemid' => $this->grade_items[0]->id)); $this->assertFalse($grade_grade->is_locked()); }
/** * Grading cron job. Performs background clean up on the gradebook */ function grade_cron() { global $CFG, $DB; $now = time(); $sql = "SELECT i.*\n FROM {grade_items} i\n WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (\n 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 $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}\n FROM {grade_grades} g, {grade_items} i\n WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (\n 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 $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}'"); } } } }
/** * This function migrades all the pre 1.9 gradebook data from xml */ function restore_migrate_old_gradebook($restore, $xml_file) { global $CFG; $status = true; //Check it exists if (!file_exists($xml_file)) { return false; } // Get info from xml // info will contain the number of record to process $info = restore_read_xml_gradebook($restore, $xml_file); // If we have info, then process if (empty($info)) { return $status; } // make sure top course category exists $course_category = grade_category::fetch_course_category($restore->course_id); $course_category->load_grade_item(); // we need to know if all grade items that were backed up are being restored // if that is not the case, we do not restore grade categories nor gradeitems of category type or course type // i.e. the aggregated grades of that category $restoreall = true; // set to false if any grade_item is not selected/restored $importing = !empty($SESSION->restore->importing); // there should not be a way to import old backups, but anyway ;-) if ($importing) { $restoreall = false; } else { $prev_grade_items = grade_item::fetch_all(array('courseid' => $restore->course_id)); $prev_grade_cats = grade_category::fetch_all(array('courseid' => $restore->course_id)); // if any categories already present, skip restore of categories from backup if (count($prev_grade_items) > 1 or count($prev_grade_cats) > 1) { $restoreall = false; } unset($prev_grade_items); unset($prev_grade_cats); } // force creation of all grade_items - the course_modules already exist grade_force_full_regrading($restore->course_id); grade_grab_course_grades($restore->course_id); // Start ul if (!defined('RESTORE_SILENTLY')) { echo '<ul>'; } /// Process letters $context = get_context_instance(CONTEXT_COURSE, $restore->course_id); // respect current grade letters if defined if ($status and $restoreall and !record_exists('grade_letters', 'contextid', $context->id)) { if (!defined('RESTORE_SILENTLY')) { echo '<li>' . get_string('gradeletters', 'grades') . '</li>'; } // Fetch recordset_size records in each iteration $recs = get_records_select("backup_ids", "table_name = 'grade_letter' AND backup_code = {$restore->backup_unique_code}", "", "old_id"); if ($recs) { foreach ($recs as $rec) { // Get the full record from backup_ids $data = backup_getid($restore->backup_unique_code, 'grade_letter', $rec->old_id); if ($data) { $info = $data->info; $dbrec = new object(); $dbrec->contextid = $context->id; $dbrec->lowerboundary = backup_todb($info['GRADE_LETTER']['#']['GRADE_LOW']['0']['#']); $dbrec->letter = backup_todb($info['GRADE_LETTER']['#']['LETTER']['0']['#']); insert_record('grade_letters', $dbrec); } } } } if (!defined('RESTORE_SILENTLY')) { echo '<li>' . get_string('categories', 'grades') . '</li>'; } //Fetch recordset_size records in each iteration $recs = get_records_select("backup_ids", "table_name = 'grade_category' AND backup_code = {$restore->backup_unique_code}", "old_id", "old_id"); $cat_count = count($recs); if ($recs) { foreach ($recs as $rec) { //Get the full record from backup_ids $data = backup_getid($restore->backup_unique_code, 'grade_category', $rec->old_id); if ($data) { //Now get completed xmlized object $info = $data->info; if ($restoreall) { if ($cat_count == 1) { $course_category->fullname = backup_todb($info['GRADE_CATEGORY']['#']['NAME']['0']['#'], false); $course_category->droplow = backup_todb($info['GRADE_CATEGORY']['#']['DROP_X_LOWEST']['0']['#'], false); $course_category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; $course_category->aggregateonlygraded = 0; $course_category->update('restore'); $grade_category = $course_category; } else { $grade_category = new grade_category(); $grade_category->courseid = $restore->course_id; $grade_category->fullname = backup_todb($info['GRADE_CATEGORY']['#']['NAME']['0']['#'], false); $grade_category->droplow = backup_todb($info['GRADE_CATEGORY']['#']['DROP_X_LOWEST']['0']['#'], false); $grade_category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; $grade_category->aggregateonlygraded = 0; $grade_category->insert('restore'); $grade_category->load_grade_item(); // force cretion of grade_item } } else { $grade_category = null; } /// now, restore grade_items $items = array(); if (!empty($info['GRADE_CATEGORY']['#']['GRADE_ITEMS']['0']['#']['GRADE_ITEM'])) { //Iterate over items foreach ($info['GRADE_CATEGORY']['#']['GRADE_ITEMS']['0']['#']['GRADE_ITEM'] as $ite_info) { $modname = backup_todb($ite_info['#']['MODULE_NAME']['0']['#'], false); $olditeminstance = backup_todb($ite_info['#']['CMINSTANCE']['0']['#'], false); if (!($mod = backup_getid($restore->backup_unique_code, $modname, $olditeminstance))) { continue; // not restored } $iteminstance = $mod->new_id; if (!($cm = get_coursemodule_from_instance($modname, $iteminstance, $restore->course_id))) { continue; // does not exist } if (!($grade_item = grade_item::fetch(array('itemtype' => 'mod', 'itemmodule' => $cm->modname, 'iteminstance' => $cm->instance, 'courseid' => $cm->course, 'itemnumber' => 0)))) { continue; // no item yet?? } if ($grade_category) { $grade_item->sortorder = backup_todb($ite_info['#']['SORT_ORDER']['0']['#'], false); $grade_item->set_parent($grade_category->id); } if ($importing or $grade_item->itemtype == 'mod' and !restore_userdata_selected($restore, $grade_item->itemmodule, $olditeminstance)) { // module instance not selected when restored using granular // skip this item continue; } //Now process grade excludes if (empty($ite_info['#']['GRADE_EXCEPTIONS'])) { continue; } foreach ($ite_info['#']['GRADE_EXCEPTIONS']['0']['#']['GRADE_EXCEPTION'] as $exc_info) { if ($u = backup_getid($restore->backup_unique_code, "user", backup_todb($exc_info['#']['USERID']['0']['#']))) { $userid = $u->new_id; $grade_grade = new grade_grade(array('itemid' => $grade_item->id, 'userid' => $userid)); $grade_grade->excluded = 1; if ($grade_grade->id) { $grade_grade->update('restore'); } else { $grade_grade->insert('restore'); } } } } } } } } if (!defined('RESTORE_SILENTLY')) { //End ul echo '</ul>'; } return $status; }
/** * internal function - does the final grade calculation */ function use_formula($userid, $params, $useditems, $oldgrade) { if (empty($userid)) { return true; } // add missing final grade values // not graded (null) is counted as 0 - the spreadsheet way foreach ($useditems as $gi) { if (!array_key_exists('gi' . $gi, $params)) { $params['gi' . $gi] = 0; } else { $params['gi' . $gi] = (double) $params['gi' . $gi]; } } // can not use own final grade during calculation unset($params['gi' . $this->id]); // insert final grade - will be needed later anyway if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); // fetching from db is not needed $grade->grade_item =& $this; } else { $grade = new grade_grade(array('itemid' => $this->id, 'userid' => $userid), false); $grade->grade_item =& $this; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return true; } // do the calculation $this->formula->set_params($params); $result = $this->formula->evaluate(); if ($result === false) { $grade->finalgrade = null; } else { // normalize $result = bounded_number($this->grademin, $result, $this->grademax); if ($this->gradetype == GRADE_TYPE_SCALE) { $result = round($result + 1.0E-5); // round scales upwards } $grade->finalgrade = $result; } // update in db if changed if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->update('compute'); } if ($result !== false) { //lock grade if needed } if ($result === false) { return false; } else { return true; } }
/** * Lock the grade if needed. Make sure this is called only when final grades are valid * * @param array $items array of all grade item ids * @return void */ public static function check_locktime_all($items) { global $CFG, $DB; $now = time(); // no rounding needed, this is not supposed to be called every 10 seconds list($usql, $params) = $DB->get_in_or_equal($items); $params[] = $now; $rs = $DB->get_recordset_select('grade_grades', "itemid {$usql} AND locked = 0 AND locktime > 0 AND locktime < ?", $params); foreach ($rs as $grade) { $grade_grade = new grade_grade($grade, false); $grade_grade->locked = time(); $grade_grade->update('locktime'); } $rs->close(); }
/** * Internal function that does the final grade calculation * * @param int $userid The user ID * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database * @return bool False if an error occurred */ public function use_formula($userid, $params, $useditems, $oldgrade) { if (empty($userid)) { return true; } // add missing final grade values // not graded (null) is counted as 0 - the spreadsheet way $allinputsnull = true; foreach ($useditems as $gi) { if (!array_key_exists('gi' . $gi, $params) || is_null($params['gi' . $gi])) { $params['gi' . $gi] = 0; } else { $params['gi' . $gi] = (double) $params['gi' . $gi]; if ($gi != $this->id) { $allinputsnull = false; } } } // can not use own final grade during calculation unset($params['gi' . $this->id]); // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they // wish to update the grades. $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid); $rawminandmaxchanged = false; // insert final grade - will be needed later anyway if ($oldgrade) { // Only run through this code if the gradebook isn't frozen. if ($gradebookcalculationsfreeze && (int) $gradebookcalculationsfreeze <= 20150627) { // Do nothing. } else { // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the // grade_item grade maximum and minimum respectively. if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) { $rawminandmaxchanged = true; $oldgrade->rawgrademax = $this->grademax; $oldgrade->rawgrademin = $this->grademin; } } $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); // fetching from db is not needed $grade->grade_item =& $this; } else { $grade = new grade_grade(array('itemid' => $this->id, 'userid' => $userid), false); $grade->grade_item =& $this; $rawminandmaxchanged = false; if ($gradebookcalculationsfreeze && (int) $gradebookcalculationsfreeze <= 20150627) { // Do nothing. } else { // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the // grade_item grade maximum and minimum respectively. $rawminandmaxchanged = true; $grade->rawgrademax = $this->grademax; $grade->rawgrademin = $this->grademin; } $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return true; } if ($allinputsnull) { $grade->finalgrade = null; $result = true; } else { // do the calculation $this->formula->set_params($params); $result = $this->formula->evaluate(); if ($result === false) { $grade->finalgrade = null; } else { // normalize $grade->finalgrade = $this->bounded_grade($result); } } // Only run through this code if the gradebook isn't frozen. if ($gradebookcalculationsfreeze && (int) $gradebookcalculationsfreeze <= 20150627) { // Update in db if changed. if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->timemodified = time(); $success = $grade->update('compute'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } } else { // Update in db if changed. if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) { $grade->timemodified = time(); $success = $grade->update('compute'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } } if ($result !== false) { //lock grade if needed } if ($result === false) { return false; } else { return true; } }
/** * Lock the grade if needed - make sure this is called only when final grades are valid * @param array $items array of all grade item ids * @return void */ function check_locktime_all($items) { global $CFG; $items_sql = implode(',', $items); $now = time(); // no rounding needed, this is not supposed to be called every 10 seconds if ($rs = get_recordset_select('grade_grades', "itemid IN ({$items_sql}) AND locked = 0 AND locktime > 0 AND locktime < {$now}")) { while ($grade = rs_fetch_next_record($rs)) { $grade_grade = new grade_grade($grade, false); $grade_grade->locked = time(); $grade_grade->update('locktime'); } rs_close($rs); } }
} else { tlog(' ' . strtoupper($target) . ' (' . $score . ') inserted for user ' . $enrollee->userid . ' on course ' . $course->id . '.'); } } else { // If the row already exists, UPDATE, but don't ever *update* the TAG. if ($target == 'mag' && !$score) { // For MAGs, we don't want to update to a zero or null score as that may overwrite a manually-entered MAG. tlog(' ' . strtoupper($target) . ' of 0 or null (' . $score . ') purposefully not updated for user ' . $enrollee->userid . ' on course ' . $course->id . '.'); $logging['not_updated'][] = $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->studentid . ') [' . $enrollee->userid . '] on course ' . $course->id . ': ' . strtoupper($target) . ' of \'' . $score . '\'.'; } else { if ($target != 'tag') { $grade->id = $gradegrade->id; // We don't want to set this again, but we do want the modified time set. unset($grade->timecreated); $grade->timemodified = time(); if (!($gl = $grade->update())) { tlog(' ' . strtoupper($target) . ' update failed for user ' . $enrollee->userid . ' on course ' . $course->id . '.', 'EROR'); } else { tlog(' ' . strtoupper($target) . ' (' . $score . ') update for user ' . $enrollee->userid . ' on course ' . $course->id . '.'); } } else { tlog(' ' . strtoupper($target) . ' purposefully not updated for user ' . $enrollee->userid . ' on course ' . $course->id . '.', 'skip'); $logging['not_updated'][] = $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->studentid . ') [' . $enrollee->userid . '] on course ' . $course->id . ': ' . strtoupper($target) . ' of \'' . $score . '\'.'; } } // END ignore updating the TAG. } // END insert or update check. } // END foreach loop. }
/** * Internal function that does the final grade calculation * * @param int $userid The user ID * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database * @return bool False if an error occurred */ public function use_formula($userid, $params, $useditems, $oldgrade) { if (empty($userid)) { return true; } // add missing final grade values // not graded (null) is counted as 0 - the spreadsheet way $allinputsnull = true; foreach ($useditems as $gi) { if (!array_key_exists('gi' . $gi, $params) || is_null($params['gi' . $gi])) { $params['gi' . $gi] = 0; } else { $params['gi' . $gi] = (double) $params['gi' . $gi]; if ($gi != $this->id) { $allinputsnull = false; } } } // can not use own final grade during calculation unset($params['gi' . $this->id]); // insert final grade - will be needed later anyway if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); // fetching from db is not needed $grade->grade_item =& $this; } else { $grade = new grade_grade(array('itemid' => $this->id, 'userid' => $userid), false); $grade->grade_item =& $this; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return true; } if ($allinputsnull) { $grade->finalgrade = null; $result = true; } else { // do the calculation $this->formula->set_params($params); $result = $this->formula->evaluate(); if ($result === false) { $grade->finalgrade = null; } else { // normalize $grade->finalgrade = $this->bounded_grade($result); } } // update in db if changed if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->timemodified = time(); $grade->update('compute'); } if ($result !== false) { //lock grade if needed } if ($result === false) { return false; } else { return true; } }
} else { $gradeval = $grEval->finalgrade; } break; } //foreach($gradeEvalcomix as $grEval) } //if($gradeEvalcomix) } //else } //else } //if($rst = get_record('grade_grades_history', 'userid', $userid, 'itemid', $grade->grade_item->id, 'source', 'evalcomixAdd')) //Comprobamos que el item actual no es un "resultado" if ($grade->grade_item->itemnumber == 0) { $modulo = $grade->grade_item->itemmodule; $instancia = $grade->grade_item->iteminstance; $maxgrade = $grade->grade_item->grademax; $student = $userid; $courseid = $courseid; if (!($grade_item = grade_item::fetch(array('iteminstance' => $instancia, 'itemmodule' => $modulo, 'courseid' => $courseid, 'itemnumber' => 0)))) { error('Can not find grade_item 2'); } $grade_old = new grade_grade(array('itemid' => $grade_item->id, 'userid' => $student)); $overridden = $grade_old->overridden; $grade_item->update_final_grade($student, $gradeval, 'evalcomixDelete'); $grade1 = new grade_grade(array('itemid' => $grade_item->id, 'userid' => $student)); $grade1->overridden = $overridden; $grade1->update(); }
if ($category->aggregation >= 100) { // grade >100% hack $grademax = $grademax * $maxcoef; } } } if ($gradeval > $grademax) { $gradeval = $grademax; } if ($gradeval < $grade_item->grademin) { $gradeval = $grade_item->grademin; } } elseif ($gradeval == '-' || $gradeval == '') { $grade_old = new grade_grade(array('itemid' => $grade_item->id, 'userid' => $student)); $gradeval = $finalgrade; $gradeval *= $multfactor; $gradeval += $plusfactor; if ($gradeval > 100) { $gradeval = 100; } } if (!$grade_item->scaleid && $grade->grade_item->itemnumber == 0) { $overridden = $grade_old->overridden; $grade_item->update_final_grade($student, $gradeval, 'evalcomixAdd'); $grade = new grade_grade(array('itemid' => $grade_item->id, 'userid' => $student)); $grade->overridden = $overridden; $grade->update('EvalcomixUpdate'); } } } }
/** * This is the function which runs when cron calls. */ public function execute() { global $CFG, $DB; echo "BEGIN >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n"; /** * A script, to be run overnight, to pull L3VA scores from Leap and generate * the MAG, for each student on specifically-tagged courses, and add it into * our live Moodle. * * @copyright 2014-2015 Paul Vaughan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // Script start time. $time_start = microtime(true); // Null or an int (course's id): run the script only for this course. For testing or one-offs. // TODO: Consider changing $thiscourse as an array, not an integer. $thiscourse = null; // null or e.g. 1234 // TODO: can we use *all* the details in version.php? It would make a lot more sense. $version = '1.0.20'; //$build = '20150128'; $build = get_config('block_leap', 'version'); // Debugging. define('DEBUG', true); // Debugging. define('TRUNCATE_LOG', true); // Truncate the log table. if (TRUNCATE_LOG) { echo 'Truncating block_leap_log...'; $DB->delete_records('block_leap_log', null); echo " done.\n"; } overnight::tlog('GradeTracker script, v' . $version . ', ' . $build . '.', 'hiya'); overnight::tlog('Started at ' . date('c', $time_start) . '.', ' go '); if ($thiscourse) { overnight::tlog('IMPORTANT! Processing only course \'' . $thiscourse . '\'.', 'warn'); } overnight::tlog('', '----'); // Before almost anything has the chance to fail, reset the fail delay setting back to 0. if (DEBUG) { if (!($reset = $DB->set_field('task_scheduled', 'faildelay', 0, array('component' => 'block_leap', 'classname' => '\\block_leap\\task\\overnight')))) { overnight::tlog('Scheduled task "fail delay" could not be reset.', 'warn'); } else { overnight::tlog('Scheduled task "fail delay" reset to 0.', 'dbug'); } } $leap_url = get_config('block_leap', 'leap_url'); $auth_token = get_config('block_leap', 'auth_token'); define('LEAP_API_URL', $leap_url . '/people/%s/views/courses.json?token=' . $auth_token); //overnight::tlog( 'Leap API URL: ' . LEAP_API_URL, 'dbug' ); // Number of decimal places in the processed targets (and elsewhere). define('DECIMALS', 3); // Search term to use when searching for courses to process. define('IDNUMBERLIKE', 'leapcore_%'); //define( 'IDNUMBERLIKE', 'leapcore_test' ); // Category details for the above columns to go into. define('CATNAME', get_string('gradebook:category_title', 'block_leap')); // Include some details. require dirname(__FILE__) . '/../../details.php'; // Logging array for the end-of-script summary. $logging = array('courses' => array(), 'students_processed' => array(), 'students_unique' => array(), 'no_l3va' => array(), 'not_updated' => array(), 'grade_types' => array('btec' => 0, 'a level' => 0, 'gcse' => 0, 'refer and pass' => 0, 'noscale' => 0, 'develop, pass' => 0), 'poor_grades' => array(), 'num' => array('courses' => 0, 'students_processed' => 0, 'students_unique' => 0, 'no_l3va' => 0, 'not_updated' => 0, 'grade_types' => 0, 'grade_types_in_use' => 0, 'poor_grades' => 0)); // Small array to store the GCSE English and maths grades from the JSON. $gcse = array('english' => null, 'maths' => null); // Just for internal use, defines the grade type (int) and what it is (string). $gradetypes = array(0 => 'None', 1 => 'Value', 2 => 'Scale', 3 => 'Text'); // Define the wanted column names (will appear in this order in the Gradebook, initially). // These column names are an integral part of this plugin and should not be changed. $column_names = array(get_string('gradebook:tag', 'block_leap') => get_string('gradebook:tag_desc', 'block_leap')); // Make an array keyed to the column names to store the grades in. $targets = array(); foreach ($column_names as $name => $desc) { $targets[strtolower($name)] = ''; } /** * The next section looks through all courses for those with a properly configured Leap block * and adds it (and the tracking configuration) to the $courses array. */ overnight::tlog('', '----'); $courses = $DB->get_records('course', null, null, 'id,shortname,fullname'); $allcourses = array(); foreach ($courses as $course) { if ($course->id != 1) { $coursecontext = \context_course::instance($course->id); if (!($blockrecord = $DB->get_record('block_instances', array('blockname' => 'leap', 'parentcontextid' => $coursecontext->id)))) { if (DEBUG) { overnight::tlog('No Leap block found for course "' . $course->id . '" (' . $course->shortname . ')', 'dbug'); } continue; } if (!($blockinstance = block_instance('leap', $blockrecord))) { if (DEBUG) { overnight::tlog('No Leap block instance found for course "' . $course->id . '" (' . $course->shortname . ')', 'dbug'); } continue; } if (isset($blockinstance->config->trackertype) && !empty($blockinstance->config->trackertype)) { $course->trackertype = $blockinstance->config->trackertype; $course->scalename = null; $course->scaleid = null; $course->gradeid = null; $allcourses[] = $course; if (DEBUG) { overnight::tlog('Tracker "' . $blockinstance->config->trackertype . '" found in course ' . $course->id . ' (' . $course->shortname . ')', 'dbug'); } } else { if (DEBUG) { overnight::tlog('<Tracker not found in course ' . $course->id . ' (' . $course->shortname . ')', 'dbug'); } } } // END if $course != 1 } // END foreach $courses as $course /* foreach ( $allcourses as $course ) { // Ignore the course with id = 1, as it's the front page. if ( $course->id == 1 ) { continue; } else { // First get course context. $coursecontext = \context_course::instance( $course->id ); $blockrecord = $DB->get_record( 'block_instances', array( 'blockname' => 'leap', 'parentcontextid' => $coursecontext->id ) ); $blockinstance = block_instance( 'leap', $blockrecord ); // Check and add trackertype and coursetype to the $course object. if ( isset( $blockinstance->config->trackertype ) && !empty( $blockinstance->config->trackertype ) ) //!empty( $blockinstance->config->trackertype ) && //isset( $blockinstance->config->coursetype ) && //!empty( $blockinstance->config->coursetype ) ) { $course->trackertype = $blockinstance->config->trackertype; //$course->coursetype = $blockinstance->config->coursetype; // Setting some more variables we'll need in due course. $course->scalename = null; $course->scaleid = null; $course->gradeid = null; // All good, so... $courses[] = $course; overnight::tlog( 'Course \'' . $course->fullname . '\' (' . $course->shortname . ') [' . $course->id . '] added to process list.', 'info'); if ( DEBUG ) { overnight::tlog( json_encode( $course ), 'dbug'); } } } } */ //var_dump($courses); exit(0); /* Example $courses array. array(2) { [0]=> object(stdClass)#91 (7) { ["id"]=> string(1) "2" ["shortname"]=> string(5) "TC101" ["fullname"]=> string(15) "Test Course 101" ["trackertype"]=> string(7) "english" ["coursetype"]=> string(14) "a2_englishlang" ["scalename"]=> string(0) "" ["scaleid"]=> NULL } [1]=> object(stdClass)#92 (7) { ["id"]=> string(1) "3" ["shortname"]=> string(5) "TC201" ["fullname"]=> string(15) "Test course 201" ["trackertype"]=> string(7) "english" ["coursetype"]=> string(6) "a2_law" ["scalename"]=> string(0) "" ["scaleid"]=> NULL } } */ $num_courses = count($allcourses); $cur_courses = 0; if ($num_courses == 0) { overnight::tlog('No courses found to process, so halting.', 'EROR'); // Returning false indicates failure. We didn't fail, just found no courses to process. return true; } overnight::tlog('', '----'); /** * Sets up each configured course with a category and columns within it. */ foreach ($allcourses as $course) { $cur_courses++; overnight::tlog('Processing course (' . $cur_courses . '/' . $num_courses . ') ' . $course->fullname . ' (' . $course->shortname . ') [' . $course->id . '] at ' . date('c', time()) . '.', 'info'); $logging['courses'][] = $course->fullname . ' (' . $course->shortname . ') [' . $course->id . '].'; //overnight::tlog( $course->coursetype, 'PVDB' ); /* We need to give serious thought to NOT doing this, as it's basically impossible to set the correct scale at this point. Grades and that: Develop / Pass - would be used for all pass only type qualifications but develop will be used instead of refer, retake, fail etc. U, G, F, E, D, C, B, A, A* - to be used for GCSE / A-level plus any others that have letter grades. Refer, Pass, Merit, Distinction. Refer, PP, PM, MM, MD, DD Refer, PPP, PPM, PMM, MMM, MMD, MDD, DDD Numbers from 0 - 100 - in traffic light systems the numbers will be compared and anything lower than the target will be red, higher than target will be green. */ /* // Work out the scale from the course type. if ( stristr( $course->coursetype, 'as_' ) || stristr( $course->coursetype, 'a2_' ) ) { $course->scalename = 'A Level'; } else if ( stristr( $course->coursetype, 'gcse_' ) ) { $course->scalename = 'GCSE'; } else if ( stristr( $course->coursetype, 'btec_' ) ) { $course->scalename = 'BTEC'; } overnight::tlog( 'Course ' . $course->id . ' appears to be a ' . $course->scalename . ' course.', 'info' ); // Get the scale ID. if ( !$moodlescale = $DB->get_record( 'scale', array( 'name' => $course->scalename ), 'id' ) ) { overnight::tlog( '- Could not find a scale called \'' . $course->scalename . '\' for course ' . $course->id . '.', 'warn' ); } else { // Scale located. $course->scaleid = $moodlescale->id; overnight::tlog( '- Scale called \'' . $course->scalename . '\' found with ID ' . $moodlescale->id . '.', 'info' ); } overnight::tlog( json_encode( $course ), '>dbg'); //var_dump($courses); exit(0); */ overnight::tlog(json_encode($course), '>dbg'); // Figure out the grade type and scale here, pulled directly from the course's gradebook's course itemtype. $coursegradescale = $DB->get_record('grade_items', array('courseid' => $course->id, 'itemtype' => 'course'), 'gradetype, scaleid'); $course->gradeid = $coursegradescale->gradetype; $course->scaleid = $coursegradescale->scaleid; overnight::tlog(json_encode($course), 'PVDB'); if ($course->gradeid == 2) { if ($coursescale = $DB->get_record('scale', array('id' => $course->scaleid))) { $course->scalename = $coursescale->name; $tolog = '- Scale \'' . $course->scaleid . '\' (' . $coursescale->name . ') found [' . $coursescale->scale . ']'; $tolog .= $coursescale->courseid ? ' (which is specific to course ' . $coursescale->courseid . ').' : ' (which is global).'; overnight::tlog($tolog, 'info'); } else { // If the scale doesn't exist that the course is using, this is a problem. overnight::tlog('- Gradetype \'2\' set, but no matching scale found.', 'warn'); } } overnight::tlog(json_encode($course), 'PVDB'); /* if ( $coursegradescale = $DB->get_record( 'grade_items', array( 'courseid' => $course->id, 'itemtype' => 'course' ), 'gradetype, scaleid' ) ) { $course->gradeid = $coursegradescale->gradetype; $course->scaleid = $coursegradescale->scaleid; overnight::tlog( 'Gradetype \'' . $course->gradeid . '\' (' . $gradetypes[$course->gradeid] . ') found.', 'info' ); // If the grade type is 2 / scale. if ( $course->gradeid == 2 ) { if ( $coursescale = $DB->get_record( 'scale', array( 'id' => $course->scaleid ) ) ) { $course->scalename = $coursescale->name; //$course->scaleid = $scaleid; //$course->coursetype = $coursescale->name; $tolog = '- Scale \'' . $course->scaleid . '\' (' . $course->scalename . ') found'; $tolog .= ( $coursescale->courseid ) ? ' (which is specific to course ' . $coursescale->courseid . ').' : ' (which is global).'; overnight::tlog( $tolog, 'info' ); } else { // If the scale doesn't exist that the course is using, this is a problem. overnight::tlog( '- Gradetype \'2\' set, but no matching scale found.', 'warn' ); } } else if ( $course->gradeid == 1 ) { // If the grade type is 1 / value. $course->scalename = 'noscale'; $course->scaleid = 1; // Already set, above. //$course->coursetype = 'Value'; $tolog = ' Using \'' . $gradetypes[$gradeid] . '\' gradetype.'; } } else { // Set it to default if no good scale could be found/used. $gradeid = 0; $scaleid = 0; overnight::tlog('No \'gradetype\' found, so using defaults instead.', 'info'); } // You may get errors here (unknown index IIRC) if no scalename is generated because a scale (or anything) hasn't been set // for that course (e.g. 'cos it's a new course). Catch this earlier! $logging['grade_types'][strtolower($course->scalename)]++; /** * Category checking: create or skip. */ if ($DB->get_record('grade_categories', array('courseid' => $course->id, 'fullname' => CATNAME))) { // Category exists, so skip creation. overnight::tlog('Category \'' . CATNAME . '\' already exists for course ' . $course->id . '.', 'skip'); } else { $grade_category = new \grade_category(); // Create a category for this course. $grade_category->courseid = $course->id; // Course id. $grade_category->fullname = CATNAME; // Set the category name (no description). $grade_category->sortorder = 1; // Need a better way of changing column order. $grade_category->hidden = 1; // Attempting to hide the totals. // Save all that... if (!($gc = $grade_category->insert())) { overnight::tlog('Category \'' . CATNAME . '\' could not be inserted for course ' . $course->id . '.', 'EROR'); return false; } else { overnight::tlog('Category \'' . CATNAME . '\' (' . $gc . ') created for course ' . $course->id . '.'); } } // We've either checked a category exists or created one, so this *should* always work. $cat_id = $DB->get_record('grade_categories', array('courseid' => $course->id, 'fullname' => CATNAME)); $cat_id = $cat_id->id; // One thing we need to do is set 'gradetype' to 0 on that newly created category, which prevents a category total showing // and the grades counting towards the total course grade. $DB->set_field_select('grade_items', 'gradetype', 0, "courseid = " . $course->id . " AND itemtype = 'category' AND iteminstance = " . $cat_id); /** * Column checking: create or update. */ // Step through each column name. foreach ($column_names as $col_name => $col_desc) { // Need to check for previously-created columns and force an update if they already exist. //if ( $DB->get_record('grade_items', array( 'courseid' => $course->id, 'itemname' => $col_name, 'itemtype' => 'manual' ) ) ) { // // Column exists, so update instead. // overnight::tlog('- Column \'' . $col_name . '\' already exists for course ' . $course->id . '.', 'skip'); //} else { $grade_item = new \grade_item(); // Create a new item object. $grade_item->courseid = $course->id; // Course id. $grade_item->itemtype = 'manual'; // Set the category name (no description). $grade_item->itemname = $col_name; // The item's name. $grade_item->iteminfo = $col_desc; // Description of the item. $grade_item->categoryid = $cat_id; // Set the immediate parent category. $grade_item->hidden = 0; // Don't want it hidden (by default). $grade_item->locked = 1; // Lock it (by default). // Per-column specifics. if ($col_name == 'TAG') { $grade_item->sortorder = 1; // In-category sort order. //$grade_item->gradetype = $course->gradeid; $grade_item->gradetype = 3; // Text field. //$grade_item->scaleid = $course->scaleid; //$grade_item->display = 1; // 'Real'. MIGHT need to seperate out options for BTEC and A Level. $grade_item->display = 0; // No frills. } //if ( $col_name == 'L3VA' ) { // // Lock the L3VA col as it's calculated elsewhere. // $grade_item->sortorder = 2; // $grade_item->locked = 1; // $grade_item->decimals = 0; // $grade_item->display = 1; // 'Real'. //} //if ( $col_name == 'MAG' ) { // $grade_item->sortorder = 3; // //$grade_item->locked = 1; // $grade_item->gradetype = $gradeid; // $grade_item->scaleid = $scaleid; // $grade_item->display = 1; // 'Real'. //} // Scale ID, generated earlier. An int, 0 or greater. // TODO: Check if we need this any more!! //$grade_item->scale = $course->scaleid; // Check to see if this record already exists, determining if we insert or update. if (!($grade_items_exists = $DB->get_record('grade_items', array('courseid' => $course->id, 'itemname' => $col_name, 'itemtype' => 'manual')))) { // INSERT a new record. if (!($gi = $grade_item->insert())) { overnight::tlog('- Column \'' . $col_name . '\' could not be inserted for course ' . $course->id . '.', 'EROR'); return false; } else { overnight::tlog('- Column \'' . $col_name . '\' created for course ' . $course->id . '.'); } } else { // UPDATE the existing record. $grade_item->id = $grade_items_exists->id; if (!($gi = $grade_item->update())) { overnight::tlog('- Column \'' . $col_name . '\' could not be updated for course ' . $course->id . '.', 'EROR'); return false; } else { overnight::tlog('- Column \'' . $col_name . '\' updated for course ' . $course->id . '.'); } } //} // END skip processing if manual column(s) already found in course. } // END while working through each rquired column. // Good to here. /** * Move the category to the first location in the gradebook if it isn't already. */ //$gtree = new grade_tree($course->id, false, false); //$temp = grade_edit_tree::move_elements(1, '') /** * Collect enrolments based on each of those courses */ // EPIC 'get enrolled students' query from Stack Overflow: // http://stackoverflow.com/questions/22161606/sql-query-for-courses-enrolment-on-moodle // Only selects manually enrolled, not self-enrolled student roles (redacted!). $sql = "SELECT DISTINCT u.id AS userid, firstname, lastname, username\n FROM mdl_user u\n JOIN mdl_user_enrolments ue ON ue.userid = u.id\n JOIN mdl_enrol e ON e.id = ue.enrolid\n -- AND e.enrol = 'leap'\n JOIN mdl_role_assignments ra ON ra.userid = u.id\n JOIN mdl_context ct ON ct.id = ra.contextid\n AND ct.contextlevel = 50\n JOIN mdl_course c ON c.id = ct.instanceid\n AND e.courseid = c.id\n JOIN mdl_role r ON r.id = ra.roleid\n AND r.shortname = 'student'\n WHERE courseid = " . $course->id . "\n AND e.status = 0\n AND u.suspended = 0\n AND u.deleted = 0\n AND (\n ue.timeend = 0\n OR ue.timeend > NOW()\n )\n AND ue.status = 0\n ORDER BY userid ASC;"; if (!($enrollees = $DB->get_records_sql($sql))) { overnight::tlog('No enrolled students found for course ' . $course->id . '.', 'warn'); } else { $num_enrollees = count($enrollees); overnight::tlog('Found ' . $num_enrollees . ' students enrolled onto course ' . $course->id . '.', 'info'); // A variable to store which enrollee we're processing. $cur_enrollees = 0; foreach ($enrollees as $enrollee) { $cur_enrollees++; // Attempt to extract the student ID from the username. $tmp = explode('@', $enrollee->username); $enrollee->studentid = $tmp[0]; // A proper student, hopefully. overnight::tlog('- Processing user (' . $cur_enrollees . '/' . $num_enrollees . ') ' . $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->userid . ') [' . $enrollee->studentid . '] on course ' . $course->id . '.', 'info'); $logging['students_processed'][] = $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->studentid . ') [' . $enrollee->userid . '] on course ' . $course->id . '.'; $logging['students_unique'][$enrollee->userid] = $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->studentid . ') [' . $enrollee->userid . '].'; // Assemble the URL with the correct data. $leapdataurl = sprintf(LEAP_API_URL, $enrollee->studentid); //if ( DEBUG ) { // overnight::tlog('-- Leap URL: ' . $leapdataurl, 'dbug'); //} // Use fopen to read from the API. if (!($handle = fopen($leapdataurl, 'r'))) { // If the API can't be reached for some reason. overnight::tlog('- Cannot open ' . $leapdataurl . '.', 'EROR'); } else { // API reachable, get the data. $leapdata = fgets($handle); fclose($handle); if (DEBUG) { overnight::tlog('-- Returned JSON: ' . $leapdata, 'dbug'); } // Handle an empty result from the API. if (strlen($leapdata) == 0) { overnight::tlog('-- API returned 0 bytes.', 'EROR'); } else { // Decode the JSON into an object. $leapdata = json_decode($leapdata); // Checking for JSON decoding errors, seems only right. if (json_last_error()) { overnight::tlog('-- JSON decoding returned error code ' . json_last_error() . ' for user ' . $enrollee->studentid . '.', 'EROR'); } else { // We have a L3VA score! And possibly GCSE English and maths grades too. //$targets['l3va'] = number_format( $leapdata->person->l3va, DECIMALS ); //$gcse['english'] = $leapdata->person->gcse_english; //$gcse['maths'] = $leapdata->person->gcse_maths; //if ( $targets['l3va'] == '' || !is_numeric( $targets['l3va'] ) || $targets['l3va'] <= 0 ) { // // If the L3VA isn't good. // overnight::tlog('-- L3VA is not good: \'' . $targets['l3va'] . '\'.', 'warn'); // $logging['no_l3va'][$enrollee->userid] = $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->studentid . ') [' . $enrollee->userid . '].'; // //} else { //overnight::tlog('-- ' . $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->userid . ') [' . $enrollee->studentid . '] L3VA score: ' . $targets['l3va'] . '.', 'info'); overnight::tlog('-- ' . $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->userid . ') [' . $enrollee->studentid . '].', 'info'); // If this course is tagged as a GCSE English or maths course, use the grades supplied in the JSON. /* if ( $course->coursetype == 'leapcore_gcse_english' ) { $magtemp = overnight::make_mag( $gcse['english'], $course->coursetype, $course->scalename ); $tagtemp = array( null, null ); } else if ( $course->coursetype == 'leapcore_gcse_maths' ) { $magtemp = overnight::make_mag( $gcse['maths'], $course->coursetype, $course->scalename ); $tagtemp = array( null, null ); } else { // Make the MAG from the L3VA. $magtemp = overnight::make_mag( $targets['l3va'], $course->coursetype, $course->scalename ); // Make the TAG in the same way, setting 'true' at the end for the next grade up. $tagtemp = overnight::make_mag( $targets['l3va'], $course->coursetype, $course->scalename, true ); } $targets['mag'] = $magtemp[0]; $targets['tag'] = $tagtemp[0]; */ /* if ( $course->coursetype == 'leapcore_gcse_english' || $course->coursetype == 'leapcore_gcse_maths' ) { overnight::tlog('--- GCSEs passed through from Leap JSON: MAG: \'' . $targets['mag'] . '\' ['. $magtemp[1] .']. TAG: \'' . $targets['tag'] . '\' ['. $tagtemp[1] .'].', 'info'); } else { overnight::tlog('--- Generated data: MAG: \'' . $targets['mag'] . '\' ['. $magtemp[1] .']. TAG: \'' . $targets['tag'] . '\' ['. $tagtemp[1] .'].', 'info'); } if ( $targets['mag'] == '0' || $targets['mag'] == '1' ) { $logging['poor_grades'][] = 'MAG ' . $targets['mag'] . ' assigned to ' . $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->studentid . ') [' . $enrollee->userid . '] on course ' . $course->id . '.'; } if ( $targets['tag'] == '0' || $targets['tag'] == '1' ) { $logging['poor_grades'][] = 'TAG ' . $targets['tag'] . ' assigned to ' . $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->studentid . ') [' . $enrollee->userid . '] on course ' . $course->id . '.'; } */ // Loop through all settable, updateable grades. foreach ($targets as $target => $score) { // Need the grade_items.id for grade_grades.itemid. $gradeitem = $DB->get_record('grade_items', array('courseid' => $course->id, 'itemname' => strtoupper($target)), 'id, categoryid'); // Check to see if this data already exists in the database, so we can insert or update. $gradegrade = $DB->get_record('grade_grades', array('itemid' => $gradeitem->id, 'userid' => $enrollee->userid), 'id'); // New grade_grade object. $grade = new \grade_grade(); $grade->userid = $enrollee->userid; $grade->itemid = $gradeitem->id; $grade->categoryid = $gradeitem->categoryid; $grade->rawgrade = $score; // Will stay as set. $grade->finalgrade = $score; // Will change with the grade, e.g. 3. $grade->timecreated = time(); $grade->timemodified = $grade->timecreated; // TODO: "excluded" is a thing and prevents a grade being aggregated. $grade->excluded = true; // If no id exists, INSERT. if (!$gradegrade) { if (!($gl = $grade->insert())) { overnight::tlog('--- ' . strtoupper($target) . ' insert failed for user ' . $enrollee->userid . ' on course ' . $course->id . '.', 'EROR'); } else { overnight::tlog('--- ' . strtoupper($target) . ' (' . $score . ') inserted for user ' . $enrollee->userid . ' on course ' . $course->id . '.'); } } else { // If the row already exists, UPDATE, but don't ever *update* the TAG. //if ( $target == 'mag' && !$score ) { // // For MAGs, we don't want to update to a zero or null score as that may overwrite a manually-entered MAG. // overnight::tlog('--- ' . strtoupper( $target ) . ' of 0 or null (' . $score . ') purposefully not updated for user ' . $enrollee->userid . ' on course ' . $course->id . '.' ); // $logging['not_updated'][] = $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->studentid . ') [' . $enrollee->userid . '] on course ' . $course->id . ': ' . strtoupper( $target ) . ' of \'' . $score . '\'.'; //} else if ( $target != 'tag' ) { $grade->id = $gradegrade->id; // We don't want to set this again, but we do want the modified time set. unset($grade->timecreated); $grade->timemodified = time(); if (!($gl = $grade->update())) { overnight::tlog('--- ' . strtoupper($target) . ' update failed for user ' . $enrollee->userid . ' on course ' . $course->id . '.', 'EROR'); } else { overnight::tlog('--- ' . strtoupper($target) . ' (' . $score . ') update for user ' . $enrollee->userid . ' on course ' . $course->id . '.'); } //} else { // overnight::tlog('--- ' . strtoupper( $target ) . ' purposefully not updated for user ' . $enrollee->userid . ' on course ' . $course->id . '.', 'skip' ); // $logging['not_updated'][] = $enrollee->firstname . ' ' . $enrollee->lastname . ' (' . $enrollee->studentid . ') [' . $enrollee->userid . '] on course ' . $course->id . ': ' . strtoupper( $target ) . ' of \'' . $score . '\'.'; //} // END ignore updating the TAG. } // END insert or update check. } // END foreach loop. //} // END L3VA check. } // END any json_decode errors. } // END empty API result. } // END open leap API for reading. } // END cycle through each course enrollee. } // END enrollee query. // Final blank-ish log entry to separate out one course from another. overnight::tlog('', '----'); } // END foreach course tagged 'leapcore_*'. // Sort and dump the summary log. overnight::tlog('Summary of all performed operations.', 'smry'); asort($logging['courses']); asort($logging['students_processed']); asort($logging['students_unique']); asort($logging['no_l3va']); asort($logging['not_updated']); arsort($logging['grade_types']); asort($logging['poor_grades']); // Processing. $logging['num']['courses'] = count($logging['courses']); $logging['num']['students_processed'] = count($logging['students_processed']); $logging['num']['students_unique'] = count($logging['students_unique']); $logging['num']['no_l3va'] = count($logging['no_l3va']); $logging['num']['not_updated'] = count($logging['not_updated']); $logging['num']['grade_types'] = count($logging['grade_types']); foreach ($logging['grade_types'] as $value) { $logging['num']['grade_types_in_use'] += $value; } $logging['num']['poor_grades'] = count($logging['poor_grades']); if ($logging['num']['courses']) { overnight::tlog($logging['num']['courses'] . ' courses:', 'smry'); $count = 0; foreach ($logging['courses'] as $course) { overnight::tlog('- ' . sprintf('%4s', ++$count) . ': ' . $course, 'smry'); } } else { overnight::tlog('No courses processed.', 'warn'); } if ($logging['num']['students_processed']) { overnight::tlog($logging['num']['students_processed'] . ' student-courses processed:', 'smry'); $count = 0; foreach ($logging['students_processed'] as $student) { overnight::tlog('- ' . sprintf('%4s', ++$count) . ': ' . $student, 'smry'); } } else { overnight::tlog('No student-courses processed.', 'warn'); } if ($logging['num']['students_unique']) { overnight::tlog($logging['num']['students_unique'] . ' unique students:', 'smry'); $count = 0; foreach ($logging['students_unique'] as $student) { echo sprintf('%4s', ++$count) . ': ' . $student . "\n"; overnight::tlog('- ' . sprintf('%4s', $count) . ': ' . $student, 'smry'); } } else { overnight::tlog('No unique students processed.', 'warn'); } if ($logging['num']['no_l3va']) { overnight::tlog($logging['num']['no_l3va'] . ' students with no L3VA:', 'smry'); $count = 0; foreach ($logging['no_l3va'] as $no_l3va) { echo sprintf('%4s', ++$count) . ': ' . $no_l3va . "\n"; overnight::tlog('- ' . sprintf('%4s', $count) . ': ' . $no_l3va, 'smry'); } } else { overnight::tlog('No missing L3VAs.', 'warn'); } if ($logging['num']['not_updated']) { overnight::tlog($logging['num']['not_updated'] . ' students purposefully not updated (0 or null grade):', 'smry'); $count = 0; foreach ($logging['not_updated'] as $not_updated) { overnight::tlog('- ' . sprintf('%4s', ++$count) . ': ' . $not_updated, 'smry'); } } else { overnight::tlog('No students purposefully not updated.', 'warn'); } if ($logging['num']['grade_types']) { overnight::tlog($logging['num']['grade_types'] . ' grade types with ' . $logging['num']['grade_types_in_use'] . ' grades set:', 'smry'); $count = 0; foreach ($logging['grade_types'] as $grade_type => $num_grades) { overnight::tlog('- ' . sprintf('%4s', ++$count) . ': ' . $grade_type . ': ' . $num_grades, 'smry'); } } else { overnight::tlog('No grade_types found.', 'warn'); } if ($logging['num']['poor_grades']) { overnight::tlog($logging['num']['poor_grades'] . ' poor grades:', 'smry'); $count = 0; foreach ($logging['poor_grades'] as $poorgrade) { overnight::tlog('- ' . sprintf('%4s', ++$count) . ': ' . $poorgrade, 'smry'); } } else { overnight::tlog('No poor grades found. Good!', 'smry'); } // Finish time. $time_end = microtime(true); $duration = $time_end - $time_start; $mins = floor($duration / 60) == 0 ? '' : floor($duration / 60) . ' minutes'; $secs = $duration % 60 == 0 ? '' : $duration % 60 . ' seconds'; $secs = $mins == '' ? $secs : ' ' . $secs; overnight::tlog('', '----'); overnight::tlog('Finished at ' . date('c', $time_end) . ', took ' . $mins . $secs . ' (' . number_format($duration, DECIMALS) . ' seconds).', 'byby'); //exit(0); return true; echo "\nEND >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"; }
/** * Updates final grade value for given user, this is a only way to update final * grades from gradebook and import because it logs the change in history table * and deals with overridden flag. This flag is set to prevent later overriding * from raw grades submitted from modules. * * @param int $userid the graded user * @param mixed $finalgrade float value of final grade - false means do not change * @param string $howmodified modification source * @param string $note optional note * @param mixed $feedback teachers feedback as string - false means do not change * @param int $feedbackformat * @return boolean success */ function update_final_grade($userid, $finalgrade = false, $source = NULL, $feedback = false, $feedbackformat = FORMAT_MOODLE, $usermodified = null) { global $USER, $CFG; $result = true; // no grading used or locked if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) { return false; } $grade = new grade_grade(array('itemid' => $this->id, 'userid' => $userid)); $grade->grade_item =& $this; // prevent db fetching of this grade_item if (empty($usermodified)) { $grade->usermodified = $USER->id; } else { $grade->usermodified = $usermodified; } if ($grade->is_locked()) { // do not update locked grades at all return false; } $locktime = $grade->get_locktime(); if ($locktime and $locktime < time()) { // do not update grades that should be already locked, force regrade instead $this->force_regrading(); return false; } $oldgrade = new object(); $oldgrade->finalgrade = $grade->finalgrade; $oldgrade->overridden = $grade->overridden; $oldgrade->feedback = $grade->feedback; $oldgrade->feedbackformat = $grade->feedbackformat; // changed grade? if ($finalgrade !== false) { if ($this->is_overridable_item()) { $grade->overridden = time(); } else { $grade->overridden = 0; } $grade->finalgrade = $this->bounded_grade($finalgrade); } // do we have comment from teacher? if ($feedback !== false) { if ($this->is_overridable_item_feedback()) { // external items (modules, plugins) may have own feedback $grade->overridden = time(); } $grade->feedback = $feedback; $grade->feedbackformat = $feedbackformat; } // HACK: Bob Puffer to allow accurate max score to be inserted into the grade_item record $grade->rawgrademax = $this->grademax; // END OF HACK if (empty($grade->id)) { $grade->timecreated = null; // hack alert - date submitted - no submission yet $grade->timemodified = time(); // hack alert - date graded $result = (bool) $grade->insert($source); } else { if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade) or $grade->feedback !== $oldgrade->feedback or $grade->feedbackformat != $oldgrade->feedbackformat or $grade->overridden != $oldgrade->overridden) { $grade->timemodified = time(); // hack alert - date graded $result = $grade->update($source); } else { // no grade change return $result; } } if (!$result) { // something went wrong - better force final grade recalculation $this->force_regrading(); } else { if ($this->is_course_item() and !$this->needsupdate) { if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) { $this->force_regrading(); } } else { if (!$this->needsupdate) { $course_item = grade_item_local::fetch_course_item($this->courseid); if (!$course_item->needsupdate) { if (grade_regrade_final_grades_local($this->courseid, $userid, $this) !== true) { $this->force_regrading(); } } else { $this->force_regrading(); } } } } return $result; }
/** * Grading cron job */ function grade_cron() { global $CFG; $now = time(); $sql = "SELECT i.*\n FROM {$CFG->prefix}grade_items i\n WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < {$now} AND EXISTS (\n SELECT 'x' FROM {$CFG->prefix}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 = get_recordset_sql($sql)) { if ($rs->RecordCount() > 0) { while ($item = rs_fetch_next_record($rs)) { $grade_item = new grade_item($item, false); $grade_item->locked = $now; $grade_item->update('locktime'); } } rs_close($rs); } $grade_inst = new grade_grade(); $fields = 'g.' . implode(',g.', $grade_inst->required_fields); $sql = "SELECT {$fields}\n FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items i\n WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < {$now} AND g.itemid=i.id AND EXISTS (\n SELECT 'x' FROM {$CFG->prefix}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 = get_recordset_sql($sql)) { if ($rs->RecordCount() > 0) { while ($grade = rs_fetch_next_record($rs)) { $grade_grade = new grade_grade($grade, false); $grade_grade->locked = $now; $grade_grade->update('locktime'); } } rs_close($rs); } }
/** * Internal function for grade category grade aggregation * * @param int $userid The User ID * @param array $items Grade items * @param array $grade_values Array of grade values * @param object $oldgrade Old grade * @param array $excluded Excluded * @param array $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid) * @param array $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid) */ private function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded, $grademinoverrides, $grademaxoverrides) { global $CFG, $DB; // Remember these so we can set flags on them to describe how they were used in the aggregation. $novalue = array(); $dropped = array(); $extracredit = array(); $usedweights = array(); if (empty($userid)) { //ignore first call return; } if ($oldgrade) { $oldfinalgrade = $oldgrade->finalgrade; $grade = new grade_grade($oldgrade, false); $grade->grade_item =& $this->grade_item; } else { // insert final grade - it will be needed later anyway $grade = new grade_grade(array('itemid' => $this->grade_item->id, 'userid' => $userid), false); $grade->grade_item =& $this->grade_item; $grade->insert('system'); $oldfinalgrade = null; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return; } // can not use own final category grade in calculation unset($grade_values[$this->grade_item->id]); // Make sure a grade_grade exists for every grade_item. // We need to do this so we can set the aggregationstatus // with a set_field call instead of checking if each one exists and creating/updating. if (!empty($items)) { list($ggsql, $params) = $DB->get_in_or_equal(array_keys($items), SQL_PARAMS_NAMED, 'g'); $params['userid'] = $userid; $sql = "SELECT itemid\n FROM {grade_grades}\n WHERE itemid {$ggsql} AND userid = :userid"; $existingitems = $DB->get_records_sql($sql, $params); $notexisting = array_diff(array_keys($items), array_keys($existingitems)); foreach ($notexisting as $itemid) { $gradeitem = $items[$itemid]; $gradegrade = new grade_grade(array('itemid' => $itemid, 'userid' => $userid, 'rawgrademin' => $gradeitem->grademin, 'rawgrademax' => $gradeitem->grademax), false); $gradegrade->grade_item = $gradeitem; $gradegrade->insert('system'); } } // if no grades calculation possible or grading not allowed clear final grade if (empty($grade_values) or empty($items) or $this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE) { $grade->finalgrade = null; if (!is_null($oldfinalgrade)) { $success = $grade->update('aggregation'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } $dropped = $grade_values; $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); return; } // Normalize the grades first - all will have value 0...1 // ungraded items are not used in aggregation. foreach ($grade_values as $itemid => $v) { if (is_null($v)) { // If null, it means no grade. if ($this->aggregateonlygraded) { unset($grade_values[$itemid]); // Mark this item as "excluded empty" because it has no grade. $novalue[$itemid] = 0; continue; } } if (in_array($itemid, $excluded)) { unset($grade_values[$itemid]); $dropped[$itemid] = 0; continue; } // Check for user specific grade min/max overrides. $usergrademin = $items[$itemid]->grademin; $usergrademax = $items[$itemid]->grademax; if (isset($grademinoverrides[$itemid])) { $usergrademin = $grademinoverrides[$itemid]; } if (isset($grademaxoverrides[$itemid])) { $usergrademax = $grademaxoverrides[$itemid]; } if ($this->aggregation == GRADE_AGGREGATE_SUM) { // Assume that the grademin is 0 when standardising the score, to preserve negative grades. $grade_values[$itemid] = grade_grade::standardise_score($v, 0, $usergrademax, 0, 1); } else { $grade_values[$itemid] = grade_grade::standardise_score($v, $usergrademin, $usergrademax, 0, 1); } } // For items with no value, and not excluded - either set their grade to 0 or exclude them. foreach ($items as $itemid => $value) { if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) { if (!$this->aggregateonlygraded) { $grade_values[$itemid] = 0; } else { // We are specifically marking these items as "excluded empty". $novalue[$itemid] = 0; } } } // limit and sort $allvalues = $grade_values; if ($this->can_apply_limit_rules()) { $this->apply_limit_rules($grade_values, $items); } $moredropped = array_diff($allvalues, $grade_values); foreach ($moredropped as $drop => $unused) { $dropped[$drop] = 0; } foreach ($grade_values as $itemid => $val) { if (self::is_extracredit_used() && $items[$itemid]->aggregationcoef > 0) { $extracredit[$itemid] = 0; } } asort($grade_values, SORT_NUMERIC); // let's see we have still enough grades to do any statistics if (count($grade_values) == 0) { // not enough attempts yet $grade->finalgrade = null; if (!is_null($oldfinalgrade)) { $success = $grade->update('aggregation'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); return; } // do the maths $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items, $usedweights, $grademinoverrides, $grademaxoverrides); $agg_grade = $result['grade']; // Set the actual grademin and max to bind the grade properly. $this->grade_item->grademin = $result['grademin']; $this->grade_item->grademax = $result['grademax']; if ($this->aggregation == GRADE_AGGREGATE_SUM) { // The natural aggregation always displays the range as coming from 0 for categories. // However, when we bind the grade we allow for negative values. $result['grademin'] = 0; } // Recalculate the grade back to requested range. $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $result['grademin'], $result['grademax']); $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade); $oldrawgrademin = $grade->rawgrademin; $oldrawgrademax = $grade->rawgrademax; $grade->rawgrademin = $result['grademin']; $grade->rawgrademax = $result['grademax']; // Update in db if changed. if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || grade_floats_different($grade->rawgrademax, $oldrawgrademax) || grade_floats_different($grade->rawgrademin, $oldrawgrademin)) { $success = $grade->update('aggregation'); // If successful trigger a user_graded event. if ($success) { \core\event\user_graded::create_from_grade($grade)->trigger(); } } $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); return; }
/** * Internal function for category grades summing * * @param grade_grade $grade The grade item * @param float $oldfinalgrade Old Final grade * @param array $items Grade items * @param array $grade_values Grade values * @param array $excluded Excluded */ private function sum_grades(&$grade, $oldfinalgrade, $items, $grade_values, $excluded) { if (empty($items)) { return null; } // ungraded and excluded items are not used in aggregation foreach ($grade_values as $itemid => $v) { if (is_null($v)) { unset($grade_values[$itemid]); } else { if (in_array($itemid, $excluded)) { unset($grade_values[$itemid]); } } } // use 0 if grade missing, droplow used and aggregating all items if (!$this->aggregateonlygraded and !empty($this->droplow)) { foreach ($items as $itemid => $value) { if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) { $grade_values[$itemid] = 0; } } } $this->apply_limit_rules($grade_values, $items); $sum = array_sum($grade_values); $grade->finalgrade = $this->grade_item->bounded_grade($sum); // update in db if changed if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { $grade->update('aggregation'); } return; }
/** * internal function for category grades aggregation */ function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded) { global $CFG; if (empty($userid)) { //ignore first call return; } if ($oldgrade) { $grade = new grade_grade($oldgrade, false); $grade->grade_item =& $this->grade_item; } else { // insert final grade - it will be needed later anyway $grade = new grade_grade(array('itemid' => $this->grade_item->id, 'userid' => $userid), false); $grade->insert('system'); $grade->grade_item =& $this->grade_item; $oldgrade = new object(); $oldgrade->finalgrade = $grade->finalgrade; $oldgrade->rawgrade = $grade->rawgrade; $oldgrade->rawgrademin = $grade->rawgrademin; $oldgrade->rawgrademax = $grade->rawgrademax; $oldgrade->rawscaleid = $grade->rawscaleid; } // no need to recalculate locked or overridden grades if ($grade->is_locked() or $grade->is_overridden()) { return; } // can not use own final category grade in calculation unset($grade_values[$this->grade_item->id]); // if no grades calculation possible or grading not allowed clear both final and raw if (empty($grade_values) or empty($items) or $this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE) { $grade->finalgrade = null; $grade->rawgrade = null; if ($grade->finalgrade !== $oldgrade->finalgrade or $grade->rawgrade !== $oldgrade->rawgrade) { $grade->update('system'); } return; } /// normalize the grades first - all will have value 0...1 // ungraded items are not used in aggregation foreach ($grade_values as $itemid => $v) { if (is_null($v)) { // null means no grade unset($grade_values[$itemid]); continue; } else { if (in_array($itemid, $excluded)) { unset($grade_values[$itemid]); continue; } } $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 0, 1); } // If global aggregateonlygraded is set, override category value if ($CFG->grade_aggregateonlygraded != -1) { $this->aggregateonlygraded = $CFG->grade_aggregateonlygraded; } // use min grade if grade missing for these types if (!$this->aggregateonlygraded) { foreach ($items as $itemid => $value) { if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) { $grade_values[$itemid] = 0; } } } // limit and sort $this->apply_limit_rules($grade_values); asort($grade_values, SORT_NUMERIC); // let's see we have still enough grades to do any statistics if (count($grade_values) == 0) { // not enough attempts yet $grade->finalgrade = null; $grade->rawgrade = null; if ($grade->finalgrade !== $oldgrade->finalgrade or $grade->rawgrade !== $oldgrade->rawgrade) { $grade->update('system'); } return; } /// start the aggregation switch ($this->aggregation) { case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies $num = count($grade_values); $grades = array_values($grade_values); if ($num % 2 == 0) { $agg_grade = ($grades[intval($num / 2) - 1] + $grades[intval($num / 2)]) / 2; } else { $agg_grade = $grades[intval($num / 2 - 0.5)]; } break; case GRADE_AGGREGATE_MIN: $agg_grade = reset($grade_values); break; case GRADE_AGGREGATE_MAX: $agg_grade = array_pop($grade_values); break; case GRADE_AGGREGATE_MODE: // the most common value, average used if multimode $freq = array_count_values($grade_values); arsort($freq); // sort by frequency keeping keys $top = reset($freq); // highest frequency count $modes = array_keys($freq, $top); // search for all modes (have the same highest count) rsort($modes, SORT_NUMERIC); // get highes mode $agg_grade = reset($modes); break; case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades $weightsum = 0; $sum = 0; foreach ($grade_values as $itemid => $grade_value) { if ($items[$itemid]->aggregationcoef <= 0) { continue; } $weightsum += $items[$itemid]->aggregationcoef; $sum += $items[$itemid]->aggregationcoef * $grade_value; } if ($weightsum == 0) { $agg_grade = null; } else { $agg_grade = $sum / $weightsum; } break; case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average $num = 0; $sum = 0; foreach ($grade_values as $itemid => $grade_value) { if ($items[$itemid]->aggregationcoef == 0) { $num += 1; $sum += $grade_value; } else { if ($items[$itemid]->aggregationcoef > 0) { $sum += $items[$itemid]->aggregationcoef * $grade_value; } } } if ($num == 0) { $agg_grade = $sum; // only extra credits or wrong coefs } else { $agg_grade = $sum / $num; } break; case GRADE_AGGREGATE_MEAN: // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum) // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum) default: $num = count($grade_values); $sum = array_sum($grade_values); $agg_grade = $sum / $num; break; } /// prepare update of new raw grade $grade->rawgrademin = $this->grade_item->grademin; $grade->rawgrademax = $this->grade_item->grademax; $grade->rawscaleid = $this->grade_item->scaleid; $grade->rawgrade = null; // categories do not use raw grades // recalculate the rawgrade back to requested range $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax); if (!is_null($finalgrade)) { $grade->finalgrade = bounded_number($this->grade_item->grademin, $finalgrade, $this->grade_item->grademax); } else { $grade->finalgrade = $finalgrade; } // update in db if changed if ($grade->finalgrade !== $oldgrade->finalgrade or $grade->rawgrade !== $oldgrade->rawgrade or $grade->rawgrademin !== $oldgrade->rawgrademin or $grade->rawgrademax !== $oldgrade->rawgrademax or $grade->rawscaleid !== $oldgrade->rawscaleid) { $grade->update('system'); } return; }