Exemple #1
0
 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);
    }
}
Exemple #3
0
 /**
  * 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}'");
            }
        }
    }
}
Exemple #5
0
 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;
 }
Exemple #6
0
 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);
 }
Exemple #7
0
/**
 * 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);
 }
Exemple #9
0
 /**
  * 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');
     }
 }
Exemple #11
0
 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();
     }
 }