moodle_local_treestudyplan/classes/coursecompetencyinfo.php
2023-12-13 00:13:07 +01:00

628 lines
25 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/>.
/**
* 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\user_competency_course;
use core_competency\competency;
use core_competency\api as c_api;
use core_competency\competency_rule_points;
use core_competency\evidence;
use core_competency\user_competency;
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);
}
/**
* Webservice structure for completion stats
* @param int $value Webservice requirement constant
*/
public static function completionstats_structure($value = VALUE_OPTIONAL) : \external_description {
return new \external_single_structure([
"ungraded" => new \external_value(PARAM_INT, 'number of ungraded submissions'),
"completed" => new \external_value(PARAM_INT, 'number of completed students'),
"students" => new \external_value(PARAM_INT, 'number of students that should submit'),
"completed_pass" => new \external_value(PARAM_INT, 'number of completed-pass students'),
"completed_fail" => new \external_value(PARAM_INT, 'number of completed-fail students'),
], "details about gradable submissions", $value);
}
protected static function completionstats($stats){
return [
"students" => $stats->count,
"completed" => 0,
"ungraded" => $stats->nneedreview,
"completed_pass" => $stats->nproficient,
"completed_fail" => $stats->nfailed,
];
}
/**
* 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'),
'ucid' => new \external_value(PARAM_INT, 'user competencyid',VALUE_OPTIONAL),
"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),
"needreview" => new \external_value(PARAM_BOOL, 'waiting for review or review in progress',VALUE_OPTIONAL),
"completionstats" => static::completionstats_structure(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'),
"proficient" => new \external_value(PARAM_INT, 'number of proficient user/competencys ',VALUE_OPTIONAL),
"total" => new \external_value(PARAM_INT, 'total number of gradable user/competencies',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,$userid=null) : 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,
];
if ($userid) {
$model['ucid'] = self::get_user_competency($userid, $competency->get('id'))->get('id');
}
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;
foreach($coursecompetencies as $c) {
$ci = $this->competencyinfo_model($c);
if(!empty($studentlist)){
$stats = $this->proficiency_stats($c,$studentlist);
$count += $stats->count;
$nproficient += $stats->nproficient;
// Copy proficiency stats to model.
foreach ((array)$stats as $key => $value) {
$ci[$key] = $value;
}
$ci['completionstats'] = self::completionstats($stats);
}
$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(!empty($studentlist)){
$stats = $this->proficiency_stats($cc,$studentlist);
$cci['completionstats'] = self::completionstats($stats);
}
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($studentlist)){
$info["proficient"] = $nproficient;
$info["total"] = $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,$userid);
$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 = [];
$coursecompetencies = course_competency::list_course_competencies($this->course->id);
$competencies = course_competency::list_competencies($this->course->id);
foreach ($coursecompetencies as $key => $coursecompetency) {
$list[] = $competencies[$coursecompetency->get('competencyid')];
}
return $list;
}
protected function proficiency_stats($competency,$studentlist) {
$r = new \stdClass();
$r->count = 0;
$r->nproficient = 0;
$r->ncourseproficient = 0;
$r->nneedreview = 0;
$r->nfailed = 0;
foreach ($studentlist as $sid) {
$p = $this->proficiency($competency,$sid);
$r->count += 1;
$r->nproficient += ($p->proficient === true)?1:0;
$r->nfailed += ($p->proficient === false)?1:0;
$r->ncourseproficient += ($p->courseproficient)?1:0;
$r->nneedreview += ($p->needreview)?1:0;
}
return $r;
}
/**
* Get a user competency. (Copied from competency api and stripped out permission checks)
*
* @param int $userid The user ID.
* @param int $competencyid The competency ID.
* @return user_competency
*/
public static function get_user_competency($userid, $competencyid) {
c_api::require_enabled();
$existing = user_competency::get_multiple($userid, array($competencyid));
$uc = array_pop($existing);
if (!$uc) {
$uc = user_competency::create_relation($userid, $competencyid);
$uc->create();
}
return $uc;
}
/**
* Get a user competency in a course. (Copied from competency api and stripped out permission checks)
*
* @param int $courseid The id of the course to check.
* @param int $userid The id of the course to check.
* @param int $competencyid The id of the competency.
* @return user_competency_course
*/
public static function get_user_competency_in_course($courseid, $userid, $competencyid) {
c_api::require_enabled();
// First we do a permissions check.
$context = \context_course::instance($courseid);
// This will throw an exception if the competency does not belong to the course.
$competency = course_competency::get_competency($courseid, $competencyid);
$params = array('courseid' => $courseid, 'userid' => $userid, 'competencyid' => $competencyid);
$exists = user_competency_course::get_record($params);
// Create missing.
if ($exists) {
$ucc = $exists;
} else {
$ucc = user_competency_course::create_relation($userid, $competency->get('id'), $courseid);
$ucc->create();
}
return $ucc;
}
/**
* 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 = self::get_user_competency($userid, $competencyid);
$proficiency = $uc->get('proficiency');
$r->proficient = $proficiency;
$r->grade = $scale->get_nearest_item($uc->get('grade'));
$r->needreview = (!($r->proficient) && ($uc->get('status') > user_competency::STATUS_IDLE));
$r->failed = $proficiency === false;
try {
// Only add course grade and proficiency if the competency is included in the course.
$ucc = self::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 = self::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);
}
}