. /** * Scan gradables for a pending grading action * @package local_treestudyplan * @copyright 2023 P.M. Kuipers * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace local_treestudyplan; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/externallib.php'); use grade_item; /** * Scan gradables for a pending grading action */ class gradingscanner { /** * Cache of supported mods * @var array */ private static $modsupported = []; /** * Cache of enrolled students in a particular course * @var array */ private static $coursestudents = []; /** The internally used grading scanner * @var local\ungradedscanners\scanner_base */ private $scanner = null; /** @var grade_item */ private $gi = null; /** * Cache of pending ungraded results per user * @var array */ private $pendingcache = []; /** * Check if a certain activity type is supported for scanning pending results * @param string $mod name of activity module */ public static function supported($mod): bool { if (!array_key_exists($mod, self::$modsupported)) { self::$modsupported[$mod] = class_exists("\local_treestudyplan\\local\\ungradedscanners\\{$mod}_scanner"); } return self::$modsupported[$mod]; } /** * List all users enrolled in a course as student by userid * @param int $courseid Course id of the course to check * @return int[] Array if user ids */ public static function get_course_students($courseid): array { global $CFG; if (!array_key_exists($courseid, self::$coursestudents)) { $students = []; $context = \context_course::instance($courseid); foreach (explode(',', $CFG->gradebookroles) as $roleid) { $roleid = trim($roleid); $students = array_keys(get_role_users($roleid, $context, false, 'u.id', 'u.id ASC')); } self::$coursestudents[$courseid] = $students; } return self::$coursestudents[$courseid]; } /** * Construct new scanner based on grade item * @param grade_item $gi Grade item */ public function __construct(grade_item $gi) { $this->courseid = $gi->courseid; $this->gi = $gi; if (self::supported($gi->itemmodule)) { $scannerclass = "\local_treestudyplan\\local\ungradedscanners\\{$gi->itemmodule}_scanner"; $this->scanner = new $scannerclass($gi); } } /** * Check if this scanner is usable (has an internal activity specific scanner) */ public function is_available(): bool { return $this->scanner !== null; } /** * Check if the gradable item this scanner scans has pending submissions for a specific user * @param int $userid ID of the user to check for */ public function pending($userid): bool { if (!array_key_exists($userid, $this->pendingcache)) { if ($this->scanner === null) { $this->pendingcache[$userid] = false; } else { $this->pendingcache[$userid] = $this->scanner->has_ungraded_submission($userid);; } } return $this->pendingcache[$userid]; } /** * Webservice structure for basic info * @param int $value Webservice requirement constant */ public static function structure($value = VALUE_OPTIONAL): \external_description { return new \external_single_structure([ "ungraded" => new \external_value(PARAM_INT, 'number of ungraded submissions'), "completed" => new \external_value(PARAM_INT, 'number of completed students'), "completed_pass" => new \external_value(PARAM_INT, 'number of completed-pass students'), "completed_fail" => new \external_value(PARAM_INT, 'number of completed-fail students'), "students" => new \external_value(PARAM_INT, 'number of students that should submit'), ], "details about gradable submissions", $value); } /** * Webservice model for basic info */ public function model(): array { // Upda. $students = self::get_course_students($this->courseid); $completed = 0; $ungraded = 0; $completedpass = 0; $completedfail = 0; foreach ($students as $userid) { if ($this->pending($userid)) { // First check if the completion needs grading. $ungraded++; } else { $grade = $this->gi->get_final($userid); if ((!empty($grade->finalgrade)) && is_numeric($grade->finalgrade)) { // Compare grade to minimum grade. if ($this->grade_passed($grade)) { $completedpass++; } else { $completedfail++; } } } } return [ 'ungraded' => $ungraded, 'completed' => $completed, 'completed_pass' => $completedpass, 'completed_fail' => $completedfail, 'students' => count($students), ]; } /** * Check if a grade is considered passed according to the rules * @param grade_grade $grade */ private function grade_passed($grade): bool { // Function copied from bistate aggregator to avoid reference mazes. global $DB; $table = "local_treestudyplan_gradecfg"; // First determine if we have a grade_config for this scale or this maximum grade. $finalgrade = $grade->finalgrade; $scale = $this->gi->load_scale(); if (isset($scale)) { $gradecfg = $DB->get_record($table, ["scale_id" => $scale->id]); } else if ($this->gi->grademin == 0) { $gradecfg = $DB->get_record($table, ["grade_points" => $this->gi->grademax]); } else { $gradecfg = null; } // For point grades, a provided grade pass overrides the defaults in the gradeconfig. // For scales, the configuration in the gradeconfig is leading. if ($gradecfg && (isset($scale) || $this->gi->gradepass == 0)) { // If so, we need to know if the grade is . if ($finalgrade >= $gradecfg->min_completed) { return true; } else { return false; } } else if ($this->gi->gradepass > 0) { $range = floatval($this->gi->grademax - $this->gi->grademin); // If no gradeconfig and gradepass is set, use that one to determine config. if ($finalgrade >= $this->gi->gradepass) { return true; } else { return false; } } else { // Blind assumptions if nothing is provided. // Over 55% of range is completed. // If range >= 3 and failed is enabled, assume that this means failed. $g = floatval($finalgrade - $this->gi->grademin); $range = floatval($this->gi->grademax - $this->gi->grademin); $score = $g / $range; if ($score > 0.55) { return true; } else { return false; } } } }