. /** * * @package local_treestudyplan * @copyright 2023 P.M. Kuipers * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace local_treestudyplan\local\aggregators; use \local_treestudyplan\courseinfo; use \local_treestudyplan\gradeinfo; use \local_treestudyplan\studyitem; use \local_treestudyplan\completion; use \local_treestudyplan\debug; class bistate_aggregator extends \local_treestudyplan\aggregator { public const DEPRECATED = false; private const DEFAULT_CONDITION = "50"; private $threshexcellent = 1.0; // Minimum fraction that must be completed to aggregate as excellent (usually 1.0). private $threshgood = 0.8; // Minimum fraction that must be completed to aggregate as good. private $threshcompleted = 0.66; // Minimum fraction that must be completed to aggregate as completed. private $usefailed = True; // Support failed completion yes/no. private $threshprogress = 0.33; // Minimum fraction that must be failed to aggregate as failed instead of progress. private $acceptpending_as_submitted = False; // Also count ungraded but submitted . public function __construct($configstr) { // allow public constructor for testing purposes. $this->initialize($configstr); } protected function initialize($configstr) { // First initialize with the defaults. foreach (["thresh_excellent", "thresh_good", "thresh_completed", "thresh_progress", ] as $key) { $val = intval(get_config('local_treestudyplan', "bistate_{$key}")); if ($val >= 0 && $val <= 100) { $this->$key = floatval($val)/100; } } foreach (["use_failed", "accept_pending_as_submitted"] as $key) { $this->$key = boolval(get_config('local_treestudyplan', "bistate_{$key}")); } // Next, decode json. $config = \json_decode($configstr, true); if (is_array($config)) { // copy all valid config settings to this item. foreach (["thresh_excellent", "thresh_good", "thresh_completed", "thresh_progress", ] as $key) { if (array_key_exists($key, $config)) { $val = $config[$key]; if ($val >= 0 && $val <= 100) { $this->$key = floatval($val)/100; } } } foreach (["use_failed", "accept_pending_as_submitted"] as $key) { if (array_key_exists($key, $config)) { $this->$key = boolval($config[$key]); } } } else { } } // Return active configuration model. public function config_string() { return json_encode([ "thresh_excellent" => 100*$this->thresh_excellent, "thresh_good" => 100*$this->thresh_good, "thresh_completed" => 100*$this->thresh_completed, "thresh_progress" => 100*$this->thresh_progress, "use_failed" => $this->use_failed, "accept_pending_as_submitted" => $this->accept_pending_as_submitted, ]); } public function needSelectGradables() { return True;} public function isDeprecated() { return self::DEPRECATED;} public function useRequiredGrades() { return True;} public function useItemConditions() { return False;} public function aggregate_binary_goals(array $completions, array $required = []) { // function is public to allow access for the testing code. // return te following conditions. // Possible states:. // - completion::EXCELLENT - At least $threshexcellent fraction of goals are complete and all required goals are met. // - completion::GOOD - At least $threshgood fraction of goals are complete and all required goals are met. // - completion::COMPLETED - At least $threshcomplete fraction of goals are completed and all required goals are met. // - completion::FAILED - At least $threshprogress fraction of goals is not failed. // - completion::INCOMPLETE - No goals have been started. // - completion::PROGRESS - All other states. $total = count($completions); $completed = 0; $progress = 0; $failed = 0; $started = 0; $totalrequired = 0; $requiredmet = 0; $MINPROGRESS = ($this->accept_pending_as_submitted)?completion::PENDING:completion::PROGRESS; foreach ($completions as $index => $c) { $completed += ($c >= completion::COMPLETED)?1:0; $progress += ($c >= $MINPROGRESS)?1:0; $failed += ($c <= completion::FAILED)?1:0; } $started = $progress + $failed; $allrequiredmet = ($requiredmet >= $totalrequired); $fractioncompleted = ($total >0)?(floatval($completed)/floatval($total)):0.0; $fractionprogress = ($total >0)?(floatval($progress)/floatval($total)):0.0; $fractionfailed = ($total >0)?(floatval($failed)/floatval($total)):0.0; $fractionstarted = ($total >0)?(floatval($started)/floatval($total)):0.0; if ($total == 0) { return completion::INCOMPLETE; } if ($fractioncompleted >= $this->thresh_excellent && $allrequiredmet) { return completion::EXCELLENT; } else if ($fractioncompleted >= $this->thresh_good && $allrequiredmet) { return completion::GOOD; } else if ($fractioncompleted >= $this->thresh_completed && $allrequiredmet) { return completion::COMPLETED; } else if ($started == 0) { return completion::INCOMPLETE; } else { return completion::PROGRESS; } } public function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid) { $course = $courseinfo->course(); $coursefinished = ($course->enddate)?($course->enddate < time()):false; // Note: studyitem condition config is not used in this aggregator. // loop through all associated gradables and count the totals, completed, etc.. $completions = []; $required = []; foreach (gradeinfo::list_studyitem_gradables($studyitem) as $gi) { $completions[] = $this->grade_completion($gi, $userid); if ($gi->is_required()) { // if it's a required grade . // also add it's index in the completion list to the list of required grades . $required[] = count($completions) - 1; } } // Combine the aquired completions into one. $result = self::aggregate_binary_goals($completions, $required); if ($this->use_failed && $result == completion::PROGRESS && $coursefinished) { return completion::FAILED; } else { return $result; } } public function aggregate_junction(array $completion, studyitem $studyitem = null, $userid = 0) { // Aggregate multiple incoming states into one junction or finish. // Possible states:. // - completion::EXCELLENT - All incoming states are excellent. // - completion::GOOD - All incoming states are at least good. // - completion::COMPLETED - All incoming states are at least completed. // - completion::FAILED - All incoming states are failed. // - completion::INCOMPLETE - All incoming states are incomplete. // - completion::PROGRESS - All other states. // First count all states. $statecount = completion::count_states($completion); $total = count($completion); if ( $total == $statecount[completion::EXCELLENT]) { return completion::EXCELLENT; } else if ( $total == $statecount[completion::EXCELLENT] + $statecount[completion::GOOD]) { return completion::GOOD; } else if ( $total == $statecount[completion::EXCELLENT] + $statecount[completion::GOOD] + $statecount[completion::COMPLETED]) { return completion::COMPLETED; } else if ( $statecount[completion::FAILED]) { return completion::FAILED; } else if ( $total == $statecount[completion::INCOMPLETE]) { return completion::INCOMPLETE; } else { return completion::PROGRESS; } } public function grade_completion(gradeinfo $gradeinfo, $userid) { global $DB; $table = "local_treestudyplan_gradecfg"; $gradeitem = $gradeinfo->getGradeitem(); $grade = $gradeitem->get_final($userid); $course = \get_course($gradeitem->courseid); // Fetch course from cache. $coursefinished = ($course->enddate)?($course->enddate < time()):false; if (empty($grade)) { return completion::INCOMPLETE; } else if ($grade->finalgrade === NULL) { // on assignments, grade NULL means a submission has not yet been graded,. // but on quizes this can also mean a quiz might have been started. // Therefor, we treat a NULL result as a reason to check the relevant gradingscanner for presence of pending items. // Since we want old results to be visible until a pending item was graded, we only use this state here. // Pending items are otherwise expressly indicated by the "pendingsubmission" field in the user model. if ($gradeinfo->getGradingscanner()->pending($userid)) { return completion::PENDING; } else { return completion::INCOMPLETE; } } else { $grade = $gradeitem->get_final($userid); // first determine if we have a grade_config for this scale or this maximum grade. $finalgrade = $grade->finalgrade; $scale = $gradeinfo->getScale(); if ( isset($scale)) { $gradecfg = $DB->get_record($table, ["scale_id"=>$scale->id]); } else if ($gradeitem->grademin == 0) { $gradecfg = $DB->get_record($table, ["grade_points"=>$gradeitem->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) || $gradeitem->gradepass == 0)) { // if so, we need to know if the grade is . if ($finalgrade >= $gradecfg->min_completed) { // return completed if completed. return completion::COMPLETED; } else if ($this->use_failed && $coursefinished) { // return failed if failed is enabled and the grade is less than the minimum grade for progress. return completion::FAILED; } else { return completion::PROGRESS; } } else if ($gradeitem->gradepass > 0) { $range = floatval($gradeitem->grademax - $gradeitem->grademin); // if no gradeconfig and gradepass is set, use that one to determine config. if ($finalgrade >= $gradeitem->gradepass) { return completion::COMPLETED; } else if ($this->use_failed && $coursefinished) { // return failed if failed is enabled and the grade is 1, while there are at leas 3 states. return completion::FAILED; } else { return completion::PROGRESS; } } 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 - $gradeitem->grademin); $range = floatval($gradeitem->grademax - $gradeitem->grademin); $score = $g / $range; if ($score > 0.55) { return completion::COMPLETED; } else if ($this->use_failed && $coursefinished) { // return failed if failed is enabled and the grade is 1, while there are at leas 3 states. return completion::FAILED; } else { return completion::PROGRESS; } } } } }