protected function process_grade_item($data) { global $DB; $data = (object) $data; $oldid = $data->id; // We'll need these later $oldparentid = $data->categoryid; $courseid = $this->get_courseid(); // make sure top course category exists, all grade items will be associated // to it. Later, if restoring the whole gradebook, categories will be introduced $coursecat = grade_category::fetch_course_category($courseid); $coursecatid = $coursecat->id; // Get the categoryid to be used $idnumber = null; if (!empty($data->idnumber)) { // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop) // so the best is to keep the ones already in the gradebook // Potential problem: duplicates if same items are restored more than once. :-( // This needs to be fixed in some way (outcomes & activities with multiple items) // $data->idnumber = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber; // In any case, verify always for uniqueness $sql = "SELECT cm.id\n FROM {course_modules} cm\n WHERE cm.course = :courseid AND\n cm.idnumber = :idnumber AND\n cm.id <> :cmid"; $params = array('courseid' => $courseid, 'idnumber' => $data->idnumber, 'cmid' => $this->task->get_moduleid()); if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) { $idnumber = $data->idnumber; } } unset($data->id); $data->categoryid = $coursecatid; $data->courseid = $this->get_courseid(); $data->iteminstance = $this->task->get_activityid(); $data->idnumber = $idnumber; $data->scaleid = $this->get_mappingid('scale', $data->scaleid); $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid); $data->timecreated = $this->apply_date_offset($data->timecreated); $data->timemodified = $this->apply_date_offset($data->timemodified); $gradeitem = new grade_item($data, false); $gradeitem->insert('restore'); //sortorder is automatically assigned when inserting. Re-instate the previous sortorder $gradeitem->sortorder = $data->sortorder; $gradeitem->update('restore'); // Set mapping, saving the original category id into parentitemid // gradebook restore (final task) will need it to reorganise items $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid); }
/** * 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); } }
/** * Some aggregation types may automatically update max grade * * @param array $items sub items */ private function auto_update_max($items) { if ($this->aggregation != GRADE_AGGREGATE_SUM) { // not needed at all return; } if (!$items) { if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) { $this->grade_item->grademax = 0; $this->grade_item->grademin = 0; $this->grade_item->gradetype = GRADE_TYPE_VALUE; $this->grade_item->update('aggregation'); } return; } //find max grade possible $maxes = array(); foreach ($items as $item) { if ($item->aggregationcoef > 0) { // extra credit from this activity - does not affect total continue; } if ($item->gradetype == GRADE_TYPE_VALUE) { $maxes[$item->id] = $item->grademax; } else { if ($item->gradetype == GRADE_TYPE_SCALE) { $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item } } } // apply droplow and keephigh $this->apply_limit_rules($maxes, $items); $max = array_sum($maxes); // update db if anything changed if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) { $this->grade_item->grademax = $max; $this->grade_item->grademin = 0; $this->grade_item->gradetype = GRADE_TYPE_VALUE; $this->grade_item->update('aggregation'); } }
/** * 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}'"); } } } }
protected function sub_test_grade_item_depends_on() { global $CFG; $origenableoutcomes = $CFG->enableoutcomes; $CFG->enableoutcomes = 0; $grade_item = new grade_item($this->grade_items[1], false); // Calculated grade dependency. $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); // For comparison. $this->assertEquals(array($this->grade_items[0]->id), $deps); // Simulate depends on returns none when locked. $grade_item->locked = time(); $grade_item->update(); $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); // For comparison. $this->assertEquals(array(), $deps); // Category dependency. $grade_item = new grade_item($this->grade_items[3], false); $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); // For comparison. $res = array($this->grade_items[4]->id, $this->grade_items[5]->id); $this->assertEquals($res, $deps); $CFG->enableoutcomes = 1; $origgradeincludescalesinaggregation = $CFG->grade_includescalesinaggregation; $CFG->grade_includescalesinaggregation = 1; // Item in category with aggregate sub categories + $CFG->grade_includescalesinaggregation = 1. $grade_item = new grade_item($this->grade_items[12], false); $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); $res = array($this->grade_items[15]->id, $this->grade_items[16]->id); $this->assertEquals($res, $deps); // Item in category with aggregate sub categories + $CFG->grade_includescalesinaggregation = 0. $CFG->grade_includescalesinaggregation = 0; $grade_item = new grade_item($this->grade_items[12], false); $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); $res = array($this->grade_items[15]->id); $this->assertEquals($res, $deps); $CFG->grade_includescalesinaggregation = 1; // Outcome item in category with with aggregate sub categories. $CFG->enableoutcomes = 0; $grade_item = new grade_item($this->grade_items[12], false); $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); $res = array($this->grade_items[15]->id, $this->grade_items[16]->id, $this->grade_items[17]->id); $this->assertEquals($res, $deps); $CFG->enableoutcomes = $origenableoutcomes; $CFG->grade_includescalesinaggregation = $origgradeincludescalesinaggregation; }
protected function process_grade_item($data) { global $DB; $data = (object) $data; $oldid = $data->id; // We'll need these later $oldparentid = $data->categoryid; $courseid = $this->get_courseid(); $idnumber = null; if (!empty($data->idnumber)) { // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop) // so the best is to keep the ones already in the gradebook // Potential problem: duplicates if same items are restored more than once. :-( // This needs to be fixed in some way (outcomes & activities with multiple items) // $data->idnumber = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber; // In any case, verify always for uniqueness $sql = "SELECT cm.id\n FROM {course_modules} cm\n WHERE cm.course = :courseid AND\n cm.idnumber = :idnumber AND\n cm.id <> :cmid"; $params = array('courseid' => $courseid, 'idnumber' => $data->idnumber, 'cmid' => $this->task->get_moduleid()); if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) { $idnumber = $data->idnumber; } } if (!empty($data->categoryid)) { // If the grade category id of the grade item being restored belongs to this course // then it is a fair assumption that this is the correct grade category for the activity // and we should leave it in place, if not then unset it. // TODO MDL-34790 Gradebook does not import if target course has gradebook categories. $conditions = array('id' => $data->categoryid, 'courseid' => $courseid); if (!$this->task->is_samesite() || !$DB->record_exists('grade_categories', $conditions)) { unset($data->categoryid); } } unset($data->id); $data->courseid = $this->get_courseid(); $data->iteminstance = $this->task->get_activityid(); $data->idnumber = $idnumber; $data->scaleid = $this->get_mappingid('scale', $data->scaleid); $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid); $data->timecreated = $this->apply_date_offset($data->timecreated); $data->timemodified = $this->apply_date_offset($data->timemodified); $gradeitem = new grade_item($data, false); $gradeitem->insert('restore'); //sortorder is automatically assigned when inserting. Re-instate the previous sortorder $gradeitem->sortorder = $data->sortorder; $gradeitem->update('restore'); // Set mapping, saving the original category id into parentitemid // gradebook restore (final task) will need it to reorganise items $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid); }
/** * Common create/update module module actions that need to be processed as soon as a module is created/updaded. * For example:create grade parent category, add outcomes, rebuild caches, regrade, save plagiarism settings... * Please note this api does not trigger events as of MOODLE 2.6. Please trigger events before calling this api. * * @param object $moduleinfo the module info * @param object $course the course of the module * * @return object moduleinfo update with grading management info */ function edit_module_post_actions($moduleinfo, $course) { global $CFG; require_once $CFG->libdir . '/gradelib.php'; $modcontext = context_module::instance($moduleinfo->coursemodule); $hasgrades = plugin_supports('mod', $moduleinfo->modulename, FEATURE_GRADE_HAS_GRADE, false); $hasoutcomes = plugin_supports('mod', $moduleinfo->modulename, FEATURE_GRADE_OUTCOMES, true); // Sync idnumber with grade_item. if ($hasgrades && ($grade_item = grade_item::fetch(array('itemtype' => 'mod', 'itemmodule' => $moduleinfo->modulename, 'iteminstance' => $moduleinfo->instance, 'itemnumber' => 0, 'courseid' => $course->id)))) { if ($grade_item->idnumber != $moduleinfo->cmidnumber) { $grade_item->idnumber = $moduleinfo->cmidnumber; $grade_item->update(); } } if ($hasgrades) { $items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $moduleinfo->modulename, 'iteminstance' => $moduleinfo->instance, 'courseid' => $course->id)); } else { $items = array(); } // Create parent category if requested and move to correct parent category. if ($items and isset($moduleinfo->gradecat)) { if ($moduleinfo->gradecat == -1) { $grade_category = new grade_category(); $grade_category->courseid = $course->id; $grade_category->fullname = $moduleinfo->name; $grade_category->insert(); if ($grade_item) { $parent = $grade_item->get_parent_category(); $grade_category->set_parent($parent->id); } $moduleinfo->gradecat = $grade_category->id; } $gradecategory = $grade_item->get_parent_category(); foreach ($items as $itemid => $unused) { $items[$itemid]->set_parent($moduleinfo->gradecat); if ($itemid == $grade_item->id) { // Use updated grade_item. $grade_item = $items[$itemid]; } if (!empty($moduleinfo->add)) { if (grade_category::aggregation_uses_aggregationcoef($gradecategory->aggregation)) { if ($gradecategory->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) { $grade_item->aggregationcoef = 1; } else { $grade_item->aggregationcoef = 0; } $grade_item->update(); } } } } require_once $CFG->libdir . '/grade/grade_outcome.php'; // Add outcomes if requested. if ($hasoutcomes && ($outcomes = grade_outcome::fetch_all_available($course->id))) { $grade_items = array(); // Outcome grade_item.itemnumber start at 1000, there is nothing above outcomes. $max_itemnumber = 999; if ($items) { foreach ($items as $item) { if ($item->itemnumber > $max_itemnumber) { $max_itemnumber = $item->itemnumber; } } } foreach ($outcomes as $outcome) { $elname = 'outcome_' . $outcome->id; if (property_exists($moduleinfo, $elname) and $moduleinfo->{$elname}) { // So we have a request for new outcome grade item? if ($items) { $outcomeexists = false; foreach ($items as $item) { if ($item->outcomeid == $outcome->id) { $outcomeexists = true; break; } } if ($outcomeexists) { continue; } } $max_itemnumber++; $outcome_item = new grade_item(); $outcome_item->courseid = $course->id; $outcome_item->itemtype = 'mod'; $outcome_item->itemmodule = $moduleinfo->modulename; $outcome_item->iteminstance = $moduleinfo->instance; $outcome_item->itemnumber = $max_itemnumber; $outcome_item->itemname = $outcome->fullname; $outcome_item->outcomeid = $outcome->id; $outcome_item->gradetype = GRADE_TYPE_SCALE; $outcome_item->scaleid = $outcome->scaleid; $outcome_item->insert(); // Move the new outcome into correct category and fix sortorder if needed. if ($grade_item) { $outcome_item->set_parent($grade_item->categoryid); $outcome_item->move_after_sortorder($grade_item->sortorder); } else { if (isset($moduleinfo->gradecat)) { $outcome_item->set_parent($moduleinfo->gradecat); } } $gradecategory = $outcome_item->get_parent_category(); if ($outcomeexists == false) { if (grade_category::aggregation_uses_aggregationcoef($gradecategory->aggregation)) { if ($gradecategory->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) { $outcome_item->aggregationcoef = 1; } else { $outcome_item->aggregationcoef = 0; } $outcome_item->update(); } } } } } if (plugin_supports('mod', $moduleinfo->modulename, FEATURE_ADVANCED_GRADING, false) and has_capability('moodle/grade:managegradingforms', $modcontext)) { require_once $CFG->dirroot . '/grade/grading/lib.php'; $gradingman = get_grading_manager($modcontext, 'mod_' . $moduleinfo->modulename); $showgradingmanagement = false; foreach ($gradingman->get_available_areas() as $areaname => $aretitle) { $formfield = 'advancedgradingmethod_' . $areaname; if (isset($moduleinfo->{$formfield})) { $gradingman->set_area($areaname); $methodchanged = $gradingman->set_active_method($moduleinfo->{$formfield}); if (empty($moduleinfo->{$formfield})) { // Going back to the simple direct grading is not a reason to open the management screen. $methodchanged = false; } $showgradingmanagement = $showgradingmanagement || $methodchanged; } } // Update grading management information. $moduleinfo->gradingman = $gradingman; $moduleinfo->showgradingmanagement = $showgradingmanagement; } rebuild_course_cache($course->id, true); if ($hasgrades) { grade_regrade_final_grades($course->id); } require_once $CFG->libdir . '/plagiarismlib.php'; plagiarism_save_form_elements($moduleinfo); return $moduleinfo; }
protected function sub_test_grade_item_depends_on() { $grade_item = new grade_item($this->grade_items[1], false); // calculated grade dependency $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); // for comparison $this->assertEquals(array($this->grade_items[0]->id), $deps); // simulate depends on returns none when locked $grade_item->locked = time(); $grade_item->update(); $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); // for comparison $this->assertEquals(array(), $deps); // category dependency $grade_item = new grade_item($this->grade_items[3], false); $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); // for comparison $res = array($this->grade_items[4]->id, $this->grade_items[5]->id); $this->assertEquals($res, $deps); }
/** * Some aggregation types may need to update their max grade. * * This must be executed after updating the weights as it relies on them. * * @return void */ private function auto_update_max() { global $DB; if ($this->aggregation != GRADE_AGGREGATE_SUM) { // not needed at all return; } // Find grade items of immediate children (category or grade items) and force site settings. $this->load_grade_item(); $depends_on = $this->grade_item->depends_on(); $items = false; if (!empty($depends_on)) { list($usql, $params) = $DB->get_in_or_equal($depends_on); $sql = "SELECT *\n FROM {grade_items}\n WHERE id {$usql}"; $items = $DB->get_records_sql($sql, $params); } if (!$items) { if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) { $this->grade_item->grademax = 0; $this->grade_item->grademin = 0; $this->grade_item->gradetype = GRADE_TYPE_VALUE; $this->grade_item->update('aggregation'); } return; } //find max grade possible $maxes = array(); foreach ($items as $item) { if ($item->aggregationcoef > 0) { // extra credit from this activity - does not affect total continue; } else { if ($item->aggregationcoef2 <= 0) { // Items with a weight of 0 do not affect the total. continue; } } if ($item->gradetype == GRADE_TYPE_VALUE) { $maxes[$item->id] = $item->grademax; } else { if ($item->gradetype == GRADE_TYPE_SCALE) { $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item } } } if ($this->can_apply_limit_rules()) { // Apply droplow and keephigh. $this->apply_limit_rules($maxes, $items); } $max = array_sum($maxes); // update db if anything changed if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) { $this->grade_item->grademax = $max; $this->grade_item->grademin = 0; $this->grade_item->gradetype = GRADE_TYPE_VALUE; $this->grade_item->update('aggregation'); } }
/** * Some aggregation types may need to update their max grade. * * This must be executed after updating the weights as it relies on them. * * @return void */ private function auto_update_max() { global $DB; if ($this->aggregation != GRADE_AGGREGATE_SUM) { // not needed at all return; } // Find grade items of immediate children (category or grade items) and force site settings. $this->load_grade_item(); $depends_on = $this->grade_item->depends_on(); // 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); // Only run if the gradebook isn't frozen. if ($gradebookcalculationsfreeze && (int) $gradebookcalculationsfreeze <= 20150627) { // Do nothing. } else { // Don't automatically update the max for calculated items. if ($this->grade_item->is_calculated()) { return; } } $items = false; if (!empty($depends_on)) { list($usql, $params) = $DB->get_in_or_equal($depends_on); $sql = "SELECT *\n FROM {grade_items}\n WHERE id {$usql}"; $items = $DB->get_records_sql($sql, $params); } if (!$items) { if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) { $this->grade_item->grademax = 0; $this->grade_item->grademin = 0; $this->grade_item->gradetype = GRADE_TYPE_VALUE; $this->grade_item->update('aggregation'); } return; } //find max grade possible $maxes = array(); foreach ($items as $item) { if ($item->aggregationcoef > 0) { // extra credit from this activity - does not affect total continue; } else { if ($item->aggregationcoef2 <= 0) { // Items with a weight of 0 do not affect the total. continue; } } if ($item->gradetype == GRADE_TYPE_VALUE) { $maxes[$item->id] = $item->grademax; } else { if ($item->gradetype == GRADE_TYPE_SCALE) { $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item } } } if ($this->can_apply_limit_rules()) { // Apply droplow and keephigh. $this->apply_limit_rules($maxes, $items); } $max = array_sum($maxes); // update db if anything changed if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) { $this->grade_item->grademax = $max; $this->grade_item->grademin = 0; $this->grade_item->gradetype = GRADE_TYPE_VALUE; $this->grade_item->update('aggregation'); } }
protected function sub_test_grade_item_depends_on() { global $CFG; $origenableoutcomes = $CFG->enableoutcomes; $CFG->enableoutcomes = 0; $grade_item = new grade_item($this->grade_items[1], false); // Calculated grade dependency. $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); // For comparison. $this->assertEquals(array($this->grade_items[0]->id), $deps); // Simulate depends on returns none when locked. $grade_item->locked = time(); $grade_item->update(); $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); // For comparison. $this->assertEquals(array(), $deps); // Category dependency. $grade_item = new grade_item($this->grade_items[3], false); $deps = $grade_item->depends_on(); sort($deps, SORT_NUMERIC); // For comparison. $res = array($this->grade_items[4]->id, $this->grade_items[5]->id); $this->assertEquals($res, $deps); }
private static function save_grade_item($grade_item) { if (!$grade_item) { throw new InvalidArgumentException("grade_item must be set"); } if (!$grade_item->courseid) { throw new InvalidArgumentException("grade_item->courseid must be set"); } if (!$grade_item->categoryid) { throw new InvalidArgumentException("grade_item->categoryid must be set"); } if (!$grade_item->name) { throw new InvalidArgumentException("grade_item->name must be set"); } if (!isset($grade_item->item_number)) { $grade_item->item_number = 0; } // check for an existing item and update or create $grade_item_tosave = grade_item::fetch(array('courseid' => $grade_item->courseid, 'itemmodule' => self::GRADE_ITEM_MODULE, 'itemname' => $grade_item->name)); if (!$grade_item_tosave) { // create new one $grade_item_tosave = new grade_item(); $grade_item_tosave->itemmodule = self::GRADE_ITEM_MODULE; $grade_item_tosave->courseid = $grade_item->courseid; $grade_item_tosave->categoryid = $grade_item->categoryid; $grade_item_tosave->iteminfo = $grade_item->typename; //$grade_item_tosave->iteminfo = $grade_item->name.' '.$grade_item->type.' '.self::GRADE_CATEGORY_NAME; $grade_item_tosave->itemnumber = $grade_item->item_number; //$grade_item_tosave->idnumber = $grade_item->name; $grade_item_tosave->itemname = $grade_item->name; $grade_item_tosave->itemtype = self::GRADE_ITEM_TYPE; //$grade_item_tosave->itemmodule = self::GRADE_ITEM_MODULE; if (isset($grade_item->points_possible) && $grade_item->points_possible > 0) { $grade_item_tosave->grademax = $grade_item->points_possible; } $grade_item_tosave->insert(self::GRADE_LOCATION_STR); } else { // update if (isset($grade_item->points_possible) && $grade_item->points_possible > 0) { $grade_item_tosave->grademax = $grade_item->points_possible; } $grade_item_tosave->categoryid = $grade_item->categoryid; $grade_item_tosave->iteminfo = $grade_item->typename; $grade_item_tosave->update(self::GRADE_LOCATION_STR); } $grade_item_id = $grade_item_tosave->id; $grade_item_pp = $grade_item_tosave->grademax; // now save the related scores if (isset($grade_item->scores) && !empty($grade_item->scores)) { // get the existing scores $current_scores = array(); $existing_grades = grade_grade::fetch_all(array('itemid' => $grade_item_id)); if ($existing_grades) { foreach ($existing_grades as $grade) { $current_scores[$grade->userid] = $grade; } } // run through the scores in the gradeitem and try to save them $errors_count = 0; $processed_scores = array(); foreach ($grade_item->scores as $score) { $user = self::get_users($score->user_id); if (!$user) { $score->error = self::USER_DOES_NOT_EXIST_ERROR; $processed_scores[] = $score; $errors_count++; continue; } $user_id = $user->id; // null/blank scores are not allowed if (!isset($score->score)) { $score->error = 'NO_SCORE_ERROR'; $processed_scores[] = $score; $errors_count++; continue; } if (!is_numeric($score->score)) { $score->error = 'SCORE_INVALID'; $processed_scores[] = $score; $errors_count++; continue; } $score->score = floatval($score->score); // Student Score should not be greater than the total points possible if ($score->score > $grade_item_pp) { $score->error = self::POINTS_POSSIBLE_UPDATE_ERRORS; $processed_scores[] = $score; $errors_count++; continue; } try { $grade_tosave = null; if (isset($current_scores[$user_id])) { // existing score $grade_tosave = $current_scores[$user_id]; // check against existing score if ($score->score < $grade_tosave->rawgrade) { $score->error = self::SCORE_UPDATE_ERRORS; $processed_scores[] = $score; $errors_count++; continue; } $grade_tosave->finalgrade = $score->score; $grade_tosave->rawgrade = $score->score; $grade_tosave->timemodified = time(); /** @noinspection PhpUndefinedMethodInspection */ $grade_tosave->update(self::GRADE_LOCATION_STR); } else { // new score $grade_tosave = new grade_grade(); $grade_tosave->itemid = $grade_item_id; $grade_tosave->userid = $user_id; $grade_tosave->finalgrade = $score->score; $grade_tosave->rawgrade = $score->score; $grade_tosave->rawgrademax = $grade_item_pp; $now = time(); $grade_tosave->timecreated = $now; $grade_tosave->timemodified = $now; $grade_tosave->insert(self::GRADE_LOCATION_STR); } /** @noinspection PhpUndefinedFieldInspection */ $grade_tosave->user_id = $score->user_id; $processed_scores[] = $grade_tosave; } catch (Exception $e) { // General errors, caused while performing updates (Tag: generalerrors) $score->error = self::GENERAL_ERRORS; $processed_scores[] = $score; $errors_count++; } } /** @noinspection PhpUndefinedFieldInspection */ $grade_item_tosave->scores = $processed_scores; // put the errors in the item if ($errors_count > 0) { $errors = array(); foreach ($processed_scores as $score) { if (isset($score->error)) { $errors[$score->user_id] = $score->error; } } /** @noinspection PhpUndefinedFieldInspection */ $grade_item_tosave->errors = $errors; } $grade_item_tosave->force_regrading(); } return $grade_item_tosave; }
/** * 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"; }
/** * Adds the grade item to the category specified by fullname. * If the category does not it is first created. This may create a performance hit * as the service call locks the database table until it completes adding the category. * Adding the category is delegated to an ad-hoc task. * If desired the code can be adjusted to queue the task for cron instead of executing * it here. This can consist of a mode switch by a config setting and when in background * mode, calling \core\task\manager::queue_adhoc_task($addcat) to queue the task. * * @param \grade_item $gitem * @param string $catnam * @return void. */ protected static function update_grade_item_category($gitem, $catname) { $courseid = $gitem->courseid; // Fetch the grade category item that matches the target grade category by fullname. // There could be more than one grade category with the same name, so fetch all and // sort by id so that we always use the oldest one. $fetchparams = array('fullname' => $catname, 'courseid' => $courseid); if ($categories = \grade_category::fetch_all($fetchparams)) { // Categories found. if (count($categories) > 1) { // Sort by key which is the category id, // to put the oldest first. ksort($categories); } // Take the first. $category = reset($categories); if ($gitem->categoryid != $category->id) { // Item needs update. $gitem->categoryid = $category->id; $gitem->update(); } } else { // Category not found so we task it. $addcat = new \block_mhaairs\task\add_grade_category_task(); // We don't set blocking by set_blocking(true). // We add custom data. $addcat->set_custom_data(array('catname' => $catname, 'courseid' => $courseid, 'itemid' => $gitem->id)); // We execute the task. // This will throw an exception if fails to create the category. $addcat->execute(); } }