moodle_local_treestudyplan/classes/local/aggregators/bistate_aggregator.php
2023-08-25 12:16:51 +02:00

298 lines
13 KiB
PHP

<?php
// This file is part of the Studyplan plugin for Moodle
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
/**
*
* @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;
defined('MOODLE_INTERNAL') || die();
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]);
}
}
}
}
// 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;
}
}
}
}
}