399 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			399 lines
		
	
	
	
		
			17 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/>.
 | |
| 
 | |
| /**
 | |
|  * Aggregate course results based on failed/completed states for grades
 | |
|  * @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;
 | |
| 
 | |
| /**
 | |
|  * Aggregate course results based on failed/completed states for grades
 | |
|  */
 | |
| class bistate_aggregator extends \local_treestudyplan\aggregator {
 | |
|     /** @var bool */
 | |
|     public const DEPRECATED = false;
 | |
|     /** @var stdClass */
 | |
|     private $agcfg = null;
 | |
| 
 | |
|     /**
 | |
|      * Retrieve or initialize current config object
 | |
|      * @return stdClass
 | |
|      */
 | |
|     private function cfg() {
 | |
|         if (empty($this->agcfg)) {
 | |
|             $this->agcfg = (object)[
 | |
|                 'thresh_excellent' => 1.0,  // Minimum fraction that must be completed to aggregate as excellent (usually 1.0).
 | |
|                 'thresh_good' => 0.8,                 // Minimum fraction that must be completed to aggregate as good.
 | |
|                 'thresh_completed' => 0.66,           // Minimum fraction that must be completed to aggregate as completed.
 | |
|                 'use_failed' => true,                 // Support failed completion yes/no.
 | |
|                 'thresh_progress' => 0.33,            // Deprecated!
 | |
|                 'accept_pending_as_submitted' => false,  // Also count ungraded but submitted .
 | |
|             ];
 | |
|         }
 | |
|         return $this->agcfg;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Create new instance of aggregation method
 | |
|      * @param string $configstr Aggregation configuration string
 | |
|      */
 | |
|     public function __construct($configstr) {
 | |
|         // Allow public constructor for testing purposes.
 | |
|         $this->initialize($configstr);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize the aggregation method
 | |
|      * @param string $configstr Aggregation configuration string
 | |
|      */
 | |
|     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->cfg()->$key = floatval($val) / 100;
 | |
|             }
 | |
|         }
 | |
|         foreach (["use_failed", "accept_pending_as_submitted"] as $key) {
 | |
|             $this->cfg()->$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->cfg()->$key = floatval($val) / 100;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             foreach (["use_failed", "accept_pending_as_submitted"] as $key) {
 | |
|                 if (array_key_exists($key, $config)) {
 | |
|                     $this->cfg()->$key = boolval($config[$key]);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return the current configuration string.
 | |
|      * @return string Configuration string
 | |
|      */
 | |
|     public function config_string() {
 | |
|         return json_encode([
 | |
|             "thresh_excellent" => 100 * $this->cfg()->thresh_excellent,
 | |
|             "thresh_good" => 100 * $this->cfg()->thresh_good,
 | |
|             "thresh_completed" => 100 * $this->cfg()->thresh_completed,
 | |
|             "thresh_progress" => 100 * $this->cfg()->thresh_progress,
 | |
|             "use_failed" => $this->cfg()->use_failed,
 | |
|             "accept_pending_as_submitted" => $this->cfg()->accept_pending_as_submitted,
 | |
|         ]);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Determine if aggregation method wants to select gradables
 | |
|      * @return bool True if aggregation method needs gradables to be selected
 | |
|      */
 | |
|     public function select_gradables() {
 | |
|         return true;
 | |
|     }
 | |
|     /**
 | |
|      * Determine if aggregation method is deprecated
 | |
|      * @return bool True if aggregation method is deprecated
 | |
|      */
 | |
|     public function deprecated() {
 | |
|         return self::DEPRECATED;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Determine if the aggregation method uses manual activity selection,
 | |
|      * @return bool True if the aggregation method uses manual activity selection
 | |
|      */
 | |
|     public function use_manualactivityselection() {
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Determine if Aggregation method makes use of "required grades" in a course/module.
 | |
|      * @return bool True if Aggregation method makes use of "required grades" in a course/module.
 | |
|      */
 | |
|     public function use_required_grades() {
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Aggregate completed/failed goals into one outcome
 | |
|      * @param int[] $completions List of completions (completion class constants)
 | |
|      * @param array $required List of completions indexes that are marked as required
 | |
|      * @return int Aggregated completion as completion class constant
 | |
|      */
 | |
|     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->cfg()->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;
 | |
| 
 | |
|             if (in_array($index, $required)) {
 | |
|                  // Not using count($required) to prevent nonexistant indices in the required list from messing things up.
 | |
|                 $totalrequired += 1;
 | |
|                 if ($c >= completion::COMPLETED) {
 | |
|                     $requiredmet += 1;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|         }
 | |
|         $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->cfg()->thresh_excellent && $allrequiredmet) {
 | |
|             return completion::EXCELLENT;
 | |
|         } else if ($fractioncompleted >= $this->cfg()->thresh_good && $allrequiredmet) {
 | |
|             return completion::GOOD;
 | |
|         } else if ($fractioncompleted >= $this->cfg()->thresh_completed && $allrequiredmet) {
 | |
|             return completion::COMPLETED;
 | |
|         } else if ($started == 0) {
 | |
|             return completion::INCOMPLETE;
 | |
|         } else {
 | |
|             return completion::PROGRESS;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Aggregate all completions in a course into one final course completion
 | |
|      * Possible states:
 | |
|      *   completion::EXCELLENT  - Completed with excellent results
 | |
|      *   completion::GOOD       - Completed with good results
 | |
|      *   completion::COMPLETED  - Completed
 | |
|      *   completion::PROGRESS   - Started, but not completed yey
 | |
|      *   completion::FAILED     - Failed
 | |
|      *   completion::INCOMPLETE - Not yet started
 | |
|      * @param courseinfo $courseinfo Courseinfo object for the course to check
 | |
|      * @param studyitem $studyitem Studyitem object for the course to check
 | |
|      * @param int $userid Id of user to check this course for
 | |
|      * @return int Aggregated completion as completion class constant
 | |
|      */
 | |
|     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->cfg()->use_failed && $result == completion::PROGRESS && $coursefinished) {
 | |
|             return completion::FAILED;
 | |
|         } else {
 | |
|             return $result;
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Aggregate juncton/filter inputs into one final junction outcome
 | |
|      * @param int[] $completion List of completion inputs
 | |
|      * @param studyitem $studyitem Studyitem object for the junction
 | |
|      * @param int $userid Id of user to check completion for
 | |
|      * @return int Aggregated completion as completion class constant
 | |
|      */
 | |
|     public function aggregate_junction(array $completion, studyitem $studyitem, $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.
 | |
| 
 | |
|         $method = strtoupper($studyitem->conditions()); // One of ANY or ALL.
 | |
| 
 | |
|         // First count all states.
 | |
|         $statecount = completion::count_states($completion);
 | |
|         $total = count($completion);
 | |
| 
 | |
|         if ($method == "ANY") {
 | |
|             if ($statecount[completion::EXCELLENT] >= 1) {
 | |
|                 return completion::EXCELLENT;
 | |
|             } else if ($statecount[completion::GOOD] >= 1) {
 | |
|                 return completion::GOOD;
 | |
|             } else if ($statecount[completion::COMPLETED] >= 1) {
 | |
|                 return completion::COMPLETED;
 | |
|             } else if ($statecount[completion::PROGRESS] >= 1) {
 | |
|                 return completion::PROGRESS;
 | |
|             } else if ($statecount[completion::FAILED] >= 1) {
 | |
|                 return completion::FAILED;
 | |
|             } else {
 | |
|                 return completion::INCOMPLETE;
 | |
|             }
 | |
|         } else { /* default value of ALL */
 | |
|             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;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Determine completion for a single grade and user
 | |
|      * @param gradeinfo $gradeinfo Gradeinfo object for grade to check
 | |
|      * @param mixed $userid Id of user to check completion for
 | |
|      * @return int Aggregated completion as completion class constant
 | |
|      */
 | |
|     public function grade_completion(gradeinfo $gradeinfo, $userid) {
 | |
|         global $DB;
 | |
|         $table = "local_treestudyplan_gradecfg";
 | |
|         $gradeitem = $gradeinfo->get_gradeitem();
 | |
|         $grade = $gradeitem->get_final($userid);
 | |
|         $course = \get_course($gradeitem->courseid); // Fetch course from cache.
 | |
|         $coursefinished = ($course->enddate) ? ($course->enddate < time()) : false;
 | |
| 
 | |
|         if (!is_object($grade) || 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->get_gradingscanner()->pending($userid)) {
 | |
|                 return completion::PENDING;
 | |
|             } else {
 | |
|                 return completion::INCOMPLETE;
 | |
|             }
 | |
|         } else {
 | |
|             // First determine if we have a grade_config for this scale or this maximum grade.
 | |
|             $finalgrade = $grade->finalgrade;
 | |
|             $scale = $gradeinfo->get_scale();
 | |
|             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->cfg()->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->cfg()->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;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | 
