2023-05-17 21:19:14 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
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 $thresh_excellent = 1.0; // Minimum fraction that must be completed to aggregate as excellent (usually 1.0)
|
|
|
|
private $thresh_good = 0.8; // Minimum fraction that must be completed to aggregate as good
|
|
|
|
private $thresh_completed = 0.66; // Minimum fraction that must be completed to aggregate as completed
|
|
|
|
private $use_failed = True; // Support failed completion yes/no
|
|
|
|
private $thresh_progress = 0.33; // Minimum fraction that must be failed to aggregate as failed instead of progress
|
|
|
|
private $accept_pending_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 {
|
2023-06-26 21:44:31 +02:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 $thresh_excellent fraction of goals are complete and all required goals are met
|
|
|
|
// - completion::GOOD - At least $thresh_good fraction of goals are complete and all required goals are met
|
|
|
|
// - completion::COMPLETED - At least $thresh_complete fraction of goals are completed and all required goals are met
|
|
|
|
// - completion::FAILED - At least $thresh_progress 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;
|
|
|
|
|
|
|
|
$total_required = 0;
|
|
|
|
$required_met = 0;
|
|
|
|
|
|
|
|
$MIN_PROGRESS = ($this->accept_pending_as_submitted)?completion::PENDING:completion::PROGRESS;
|
|
|
|
|
|
|
|
foreach($completions as $index => $c) {
|
|
|
|
|
|
|
|
$completed += ($c >= completion::COMPLETED)?1:0;
|
|
|
|
$progress += ($c >= $MIN_PROGRESS)?1:0;
|
|
|
|
$failed += ($c <= completion::FAILED)?1:0;
|
|
|
|
}
|
|
|
|
$started = $progress + $failed;
|
|
|
|
$allrequiredmet = ($required_met >= $total_required);
|
|
|
|
|
|
|
|
$fraction_completed = ($total >0)?(floatval($completed)/floatval($total)):0.0;
|
|
|
|
$fraction_progress = ($total >0)?(floatval($progress)/floatval($total)):0.0;
|
|
|
|
$fraction_failed = ($total >0)?(floatval($failed)/floatval($total)):0.0;
|
|
|
|
$fraction_started = ($total >0)?(floatval($started)/floatval($total)):0.0;
|
|
|
|
|
|
|
|
if($total == 0){
|
|
|
|
return completion::INCOMPLETE;
|
|
|
|
}
|
|
|
|
if($fraction_completed >= $this->thresh_excellent && $allrequiredmet){
|
|
|
|
return completion::EXCELLENT;
|
|
|
|
}
|
|
|
|
else if($fraction_completed >= $this->thresh_good && $allrequiredmet){
|
|
|
|
return completion::GOOD;
|
|
|
|
}
|
|
|
|
else if($fraction_completed >= $this->thresh_completed && $allrequiredmet){
|
|
|
|
return completion::COMPLETED;
|
|
|
|
}
|
|
|
|
else if($started == 0){
|
|
|
|
return completion::INCOMPLETE;
|
|
|
|
}
|
|
|
|
else if($this->use_failed && ($fraction_failed >= $this->thresh_progress)){
|
|
|
|
return completion::FAILED;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return completion::PROGRESS;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid){
|
|
|
|
// 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
|
|
|
|
return self::aggregate_binary_goals($completions,$required);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
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 {
|
2023-06-16 23:12:17 +02:00
|
|
|
$grade = $gradeitem->get_final($userid);
|
2023-05-17 21:19:14 +02:00
|
|
|
// 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
|
2023-06-16 23:12:17 +02:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
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 && $finalgrade < $gradecfg->min_progress)
|
|
|
|
{
|
|
|
|
// 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 && $gradeitem->gradepass >= 3 && $range >= 3 && $finalgrade == 1)
|
|
|
|
{
|
|
|
|
// 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 && $range >= 3 && $finalgrade == 1)
|
|
|
|
{
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-16 23:12:17 +02:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|