. /** * Class to collect course completion info for a given course * @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'); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/course/lib.php'); use core_competency\course_competency; use core_competency\competency; use core_competency\api as c_api; use core_competency\competency_rule_points; use core_competency\evidence; use stdClass; /** * Class to collect course competencty info for a given course */ class coursecompetencyinfo { /** @var \stdClass */ private $course; /** @var \course_modinfo */ private $modinfo; /** @var studyitem */ private $studyitem; /** * Course id of relevant course */ public function id() { return $this->course->id; } /** * Construct new object for a given course * @param \stdClass $course Course database record */ public function __construct($course, $studyitem) { global $DB; $this->course = $course; $this->studyitem = $studyitem; $this->completion = new \completion_info($this->course); $this->modinfo = get_fast_modinfo($this->course); } /** * Generic competency info structure for individual competency stats * @param $recurse True if child competencies may be included */ public static function competencyinfo_structure($recurse=true) : \external_description { $struct = [ "id" => new \external_value(PARAM_INT, 'competency id'), "title" => new \external_value(PARAM_RAW, 'competency display title'), "details" => new \external_value(PARAM_RAW, 'competency details'), "description" => new \external_value(PARAM_RAW, 'competency description'), "ruleoutcome" => new \external_value(PARAM_TEXT, 'competency rule outcome text', VALUE_OPTIONAL), "rule" => new \external_value(PARAM_RAW, 'competency rule description', VALUE_OPTIONAL), "path" => new \external_multiple_structure(new \external_single_structure([ "id" => new \external_value(PARAM_INT), "title" => new \external_value(PARAM_RAW), "type" => new \external_value(PARAM_TEXT), ]), 'competency path'), "grade" => new \external_value(PARAM_TEXT, 'competency grade', VALUE_OPTIONAL), "coursegrade" => new \external_value(PARAM_TEXT, 'course competency grade', VALUE_OPTIONAL), "proficient" => new \external_value(PARAM_BOOL, 'competency proficiency',VALUE_OPTIONAL), "courseproficient" => new \external_value(PARAM_BOOL, 'course competency proficiency',VALUE_OPTIONAL), "nproficient" => new \external_value(PARAM_INT, 'number of students with proficiency',VALUE_OPTIONAL), "ncourseproficient" => new \external_value(PARAM_INT, 'number of students with course proficiency',VALUE_OPTIONAL), "count" => new \external_value(PARAM_INT, 'number of students in stats',VALUE_OPTIONAL), "required" => new \external_value(PARAM_BOOL, 'if required in parent competency rule',VALUE_OPTIONAL), "points" => new \external_value(PARAM_INT, 'number of points in parent competency rule',VALUE_OPTIONAL), "progress" => new \external_value(PARAM_INT, 'number completed child competencies/points',VALUE_OPTIONAL), "count" => new \external_value(PARAM_INT, 'number of child competencies/points required',VALUE_OPTIONAL), "feedback" => new \external_value(PARAM_RAW, 'feedback provided with this competency',VALUE_OPTIONAL), ]; if($recurse) { $struct["children"] = new \external_multiple_structure(self::competencyinfo_structure(false),'child competencies',VALUE_OPTIONAL); } return new \external_single_structure($struct, 'course completion info'); } /** * Webservice structure for editor info * @param int $value Webservice requirement constant */ public static function editor_structure($value = VALUE_REQUIRED) : \external_description { return new \external_single_structure([ "competencies" => new \external_multiple_structure(self::competencyinfo_structure(), 'competencies'), "fproficient" => new \external_value(PARAM_FLOAT, 'fraction of completion for total course proficiency ',VALUE_OPTIONAL), "fcourseproficient" => new \external_value(PARAM_FLOAT, 'fraction of completion for total course proficienct',VALUE_OPTIONAL), ], 'course completion info', $value); } /** * Webservice structure for userinfo * @param int $value Webservice requirement constant */ public static function user_structure($value = VALUE_REQUIRED) : \external_description { return new \external_single_structure([ "competencies" => new \external_multiple_structure(self::competencyinfo_structure(), 'competencies'), "progress" => new \external_value(PARAM_INT, 'number completed competencies'), "count" => new \external_value(PARAM_INT, 'number of competencies',VALUE_OPTIONAL), ], 'course completion info', $value); } /** * Create basic competency information model from competency * @param Object $competency */ private function competencyinfo_model($competency) : array { $displayfield = get_config("local_treestudyplan","competency_displayname"); $detailfield = get_config("local_treestudyplan","competency_detailfield"); $headingfield = ($displayfield != 'description')?$displayfield:"shortname"; $framework = $competency->get_framework(); $heading = $framework->get($headingfield); if(empty(trim($heading))) { $heading = $framework->get('shortname'); // Fall back to shortname if heading field is empty } $path = [[ 'id' => $framework->get('id'), 'title' => $heading, 'contextid' => $framework->get('contextid'), 'type' => 'framework', ]]; foreach ($competency->get_ancestors() as $c) { $competencypath[] = $c->get('shortname'); $heading = $c->get($headingfield); if(empty(trim($heading))) { $heading = $c->get('shortname'); // Fall back to shortname if heading field is empty } $path[] = [ 'id' => $c->get('id'), 'title' => $heading, 'contextid' => $framework->get('contextid'), 'type' => 'competency', ]; } $heading = $competency->get($headingfield); if(empty(trim($heading))) { $heading = $competency->get('shortname'); // Fall back to shortname if heading field is empty } $path[] = [ 'id' => $competency->get('id'), 'title' => $heading, 'contextid' => $framework->get('contextid'), 'type' => 'competency', ]; $title = $competency->get($displayfield); if(empty(trim($title))) { $title = $competency->get('shortname'); // Fall back to shortname if heading field is empty } $model = [ 'id' => $competency->get('id'), 'title' => $title, 'details' => $competency->get($detailfield), 'description' => $competency->get('description'), 'path' => $path, ]; return $model; } /** * Webservice model for editor info * @param int[] $studentlist List of user id's to use for checking issueing progress within a study plan * @return array Webservice data model */ public function editor_model(array $studentlist = null) { $coursecompetencies = $this->course_competencies(); // Next create the data model, and check user proficiency for each competency. $count = 0; $nproficient = 0; $ncourseproficient = 0; foreach($coursecompetencies as $c) { $ci = $this->competencyinfo_model($c); if(!empty($studentslist)){ $stats = $this->proficiency_stats($c,$studentlist); $count += $stats->count; $nproficient += $stats->nproficient; $ncourseproficient += $stats->ncourseproficient; // Copy proficiency stats to model. foreach ((array)$stats as $key => $value) { $ci[$key] = $value; } } $ci['required'] = $this->is_required($c); $rule = $c->get_rule_object(); $ruleoutcome = $c->get('ruleoutcome'); if($rule && $ruleoutcome != competency::OUTCOME_NONE) { $ruletext = $rule->get_name(); $ruleconfig = $c->get('ruleconfig'); if ($ruleoutcome == competency::OUTCOME_EVIDENCE) { $outcometag = "evidence"; } else if ($ruleoutcome == competency::OUTCOME_COMPLETE) { $outcometag = "complete"; } else if ($ruleoutcome == competency::OUTCOME_RECOMMEND) { $outcometag = "recommend"; } else { $outcometag = "none"; } $ci["ruleoutcome"] = get_string("coursemodulecompetencyoutcome_{$outcometag}","core_competency"); if ($rule instanceof competency_rule_points) { $ruleconfig = json_decode($ruleconfig); $points = $ruleconfig->base->points; // Make a nice map of the competency rule config $crlist = []; foreach($ruleconfig->competencies as $cr){ $crlist[$cr->id] = $cr; } $ci["rule"] = $ruletext . " ({$points} ".get_string("points","core_grades").")"; } else { $ci["rule"] = $ruletext; } // get one level of children $dids = competency::get_descendants_ids($c); if(count($dids) > 0) { $children = []; foreach($dids as $did) { $cc = new competency($did); $cci = $this->competencyinfo_model($cc); if($rule instanceof competency_rule_points) { if(array_key_exists($did,$crlist)) { $cr = $crlist[$did]; $cci["points"] = (int) $cr->points; $cci["required"] = (int) $cr->required; } } $children[] = $cci; } $ci["children"] = $children; } } $cis[] = $ci; } $info = [ "competencies" => $cis, ]; if(!empty($studentslist)){ $info["fproficient"] = (float)($nproficient)/(float)($count); $info["fcourseproficient"] = (float)($ncourseproficient)/(float)($count); } return $info; } /** * Webservice model for user course completion info * @param int $userid ID of user to check specific info for * @return array Webservice data model */ public function user_model($userid) { $competencies = $this->course_competencies(); $progress = 0; $cis = []; foreach ($competencies as $c) { $ci = $this->competencyinfo_model($c,$userid); // Add user info if $userid is set. $p = $this->proficiency($c,$userid); // Copy proficiency info to model. foreach ((array)$p as $key => $value) { $ci[$key] = $value; } $ci['required'] = $this->is_required($c); if ($p->proficient || $p->courseproficient) { $progress += 1; } // Retrieve feedback. $ci["feedback"] = $this->retrievefeedback($c,$userid); $rule = $c->get_rule_object(); $ruleoutcome = $c->get('ruleoutcome'); if($rule && $ruleoutcome != competency::OUTCOME_NONE) { $ruletext = $rule->get_name(); $ruleconfig = $c->get('ruleconfig'); if ($ruleoutcome == competency::OUTCOME_EVIDENCE) { $outcometag = "evidence"; } else if ($ruleoutcome == competency::OUTCOME_COMPLETE) { $outcometag = "complete"; } else if ($ruleoutcome == competency::OUTCOME_RECOMMEND) { $outcometag = "recommend"; } else { $outcometag = "none"; } $ci["ruleoutcome"] = get_string("coursemodulecompetencyoutcome_{$outcometag}","core_competency"); if ($rule instanceof competency_rule_points) { $ruleconfig = json_decode($ruleconfig); $pointsreq = $ruleconfig->base->points; $points = 0; // Make a nice map of the competency rule config $crlist = []; foreach($ruleconfig->competencies as $cr){ $crlist[$cr->id] = $cr; } } // get one level of children $dids = competency::get_descendants_ids($c); if(count($dids) > 0) { $dcount = 0; $dprogress = 0; $children = []; foreach($dids as $did) { $cc = new competency($did); $cci = $this->competencyinfo_model($cc); $cp = $p = $this->proficiency($cc,$userid); // Copy proficiency info to model. foreach ((array)$cp as $key => $value) { $cci[$key] = $value; } // Retrieve feedback. $cci["feedback"] = $this->retrievefeedback($cc,$userid); if($rule instanceof competency_rule_points) { if(array_key_exists($did,$crlist)) { $cr = $crlist[$did]; $cci["points"] = (int) $cr->points; $cci["required"] = (int) $cr->required; if($cp->proficient) { $points += (int) $cr->points; } } } else { $dcount += 1; if ($cp->proficient) { $dprogress += 1; } } $children[] = $cci; } $ci["children"] = $children; } if ($rule instanceof competency_rule_points) { $ci["rule"] = $ruletext . " ({$points} / {$pointsreq} ".get_string("points","core_grades").")"; $ci["count"] = $pointsreq; $ci["progress"] = $points; } else { $ci["rule"] = $ruletext; $ci["count"] = $dcount; $ci["progress"] = $dprogress; } } $cis[] = $ci; } $info = [ 'progress' => $progress, "count" => count($competencies), "competencies" => $cis, ]; return $info; } /** * Get the course's competencies with user status * @return array of Competencies Webservice data model */ public function course_competencies() { $list = []; // First retrieve all the competencies associates with this course. $coursecompetencies = c_api::list_course_competencies($this->course->id); // Next create the data model, and check user proficiency for each competency. foreach($coursecompetencies as $ccinfo) { $list[] = $ccinfo['competency']; } return $list; } protected function proficiency_stats($competency,$studentlist) { $r = new \stdClass(); $r->count = 0; $r->nproficient = 0; $r->ncourseproficient = 0; foreach ($studentlist as $sid) { $p = $this->proficiency($competency,$sid); $r->count += 1; $r->nproficient += ($p->proficient)?1:0; $r->ncourseproficient += ($p->courseproficient)?1:0; } return $r; } /** * Retrieve course proficiency and overall proficiency for a competency and user * * @param \core_competency\competency $competency * @param int $userid * * @return stdClass * */ public function proficiency($competency, $userid) { $scale = $competency->get_scale(); $competencyid = $competency->get('id'); $r = new \stdClass(); $uc = c_api::get_user_competency($userid, $competencyid); $r->proficient = $uc->get('proficiency'); $r->grade = $scale->get_nearest_item($uc->get('grade')); try { // Only add course grade and proficiency if the competency is included in the course. $ucc = c_api::get_user_competency_in_course($this->course->id,$userid,$competencyid); $r->courseproficient = $ucc->get('proficiency'); $r->coursegrade = $scale->get_nearest_item($ucc->get('grade')); } catch (\Exception $x) {} return $r; } /** * Retrieve course proficiency and overall proficiency for a competency and user * * @param \core_competency\competency $competency * @param int $userid * * @return stdClass * */ public function retrievefeedback($competency, $userid) { $competencyid = $competency->get('id'); $uc = c_api::get_user_competency($userid, $competencyid); // Get evidences and sort by creation date (newest first) $evidence = evidence::get_records_for_usercompetency($uc->get('id'),\context_system::instance(),'timecreated', "DESC"); // Get the first valid note and return it; foreach($evidence as $e) { if ( in_array($e->get('action'),[evidence::ACTION_OVERRIDE,evidence::ACTION_COMPLETE] )) { return $e->get('note'); break; } } return null; } /** * Webservice executor to mark competency as required * @param int $competencyid ID of the competency * @param int $itemid ID of the study item * @param bool $required Mark competency as required or not * @return success Always returns successful success object */ public static function require_competency(int $competencyid, int $itemid, bool $required) { global $DB; $item = studyitem::find_by_id($itemid); // Make sure conditions are properly configured; $conditions = []; try { $conditions = json_decode($item->conditions(),true); } catch (\Exception $x) {} // Make sure the competencied field exists if (!isset($conditions["competencies"]) || !is_array($conditions["competencies"])) { $conditions["competencies"] = []; } // Make sure a record exits. if (!array_key_exists($competencyid,$conditions["competencies"])){ $conditions["competencies"][$competencyid] = [ "required" => boolval($required), ]; } else { $conditions["competencies"][$competencyid]["required"] = boolval($required); } // Store conditions; $item->edit(["conditions" => json_encode($conditions)]); return success::success(); } /** * Check if this gradable item is marked required in the studyitem * @return bool */ public function is_required($competency) { if ($this->studyitem) { $conditions = []; try { $conditions = json_decode($this->studyitem->conditions(),true); } catch (\Exception $x) {} // Make sure the competencied field exists if ( isset($conditions["competencies"]) && is_array($conditions["competencies"]) && isset($conditions["competencies"][$competency->get("id")]) && isset($conditions["competencies"][$competency->get("id")]["required"]) ) { return boolval($conditions["competencies"][$competency->get("id")]["required"]); } } return(false); } }