Better configurable backend for couse competency aggregation

This commit is contained in:
PMKuipers 2023-11-24 23:00:53 +01:00
parent 54a8823bbd
commit 932587d2af
13 changed files with 302 additions and 85 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3467,7 +3467,7 @@ export default {
return cgroup.completion?'completed':'incomplete';
},
pathtags(competency,use_idnumber=false) {
pathtags(competency) {
const path = competency.path;
let s = "";
for (const ix in path) {
@ -3475,8 +3475,16 @@ export default {
if ( ix > 0) {
s += " / ";
}
s += `<a href="/admin/tool/lp/competencies.php?competencyid=${p.id}">${(use_idnumber)?p.idnumber:p.shortname}</a>`;
let url;
if (p.type =='competency') {
url = `/admin/tool/lp/competencies.php?competencyid=${p.id}`;
} else {
url = `/admin/tool/lp/competencies.php?competencyframeworkid=${p.id}&pagecontextid=${p.contextid}`;
}
s += `<a href="${url}">${p.title}</a>`;
}
return s;
},
},
@ -3489,12 +3497,11 @@ export default {
</tr>
<template v-else>
<tr v-for='c in value.competencies'>
<td><a href='#' v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.shortname'></span></a></td>
<td v-if="c.description">
<span v-html='c.description'></span>
<td><a href='#' v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.title'></span></a></td>
<td v-if="c.details">
<span v-html='c.details'></span>
</td>
<b-modal :id="'modal-competency-id-'+c.id"
:title="c.path.join(' / ')"
size="lg"
ok-only
centered
@ -3502,14 +3509,21 @@ export default {
>
<template #modal-header>
<div>
<h1><i class="fa fa-certificate"></i>
<a :href="'/admin/tool/lp/competencies.php?competencyid='+c.id'" target="_blank"
>{{c.shortname}}</a
<h1><i class="fa fa-puzzle-piece"></i>
<a :href="'/admin/tool/lp/competencies.php?competencyid='+c.id" target="_blank"
>{{c.title}} {{c.details}} </a
></h1>
<div>{{ pathtags(c) }}</div>
<div><span v-html="pathtags(c)"></span></div>
</div>
</template>
<span v-html='c.description'></span>
<table v-if="c.children">
<tr v-for="cc in c.children">
<td><span v-html='cc.displayfield'></span>
</td><td><span v-html='cc.description'></span>
</td>
</tr>
</table>
</b-modal>
</tr>
</template>

View File

@ -226,10 +226,6 @@ class badgeinfo {
$badge['issuedlink'] = (new \moodle_url('/badges/badge.php', ['hash' => $issueinfo->uniquehash]))->out(false);
}
$f = fopen("/tmp/badgedebug","a");
fputs($f,date("Y-m-d H:M:S")." Badge info\n");
fputs($f,print_r($badge,true)."\n\n");
fclose($f);
return $badge;
}
@ -532,10 +528,6 @@ class badgeinfo {
if(isset($userid)) {
$coursecompletion = new \completion_completion(["userid" => $userid, "course" => $course->id]);
$coursecompleted = $coursecompletion->is_complete();
$f = fopen("/tmp/debug","a+");
fputs($f,$course->fullname." ".($coursecompleted?"(COMPLETED)":"(NOT completed)")."\n");
fputs($f,print_r($coursecompletion,true));
fclose($f);
$subcrit["requirements"]["completion"]["completed"] = (bool) $coursecompleted;
$check_grade = true;

View File

@ -30,6 +30,7 @@ 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 stdClass;
/**
@ -69,13 +70,15 @@ class coursecompetencyinfo {
public static function competencyinfo_structure($recurse=true) : \external_description {
$struct = [
"id" => new \external_value(PARAM_INT, 'competency id'),
"shortname" => new \external_value(PARAM_RAW, 'competency short name'),
"idnumber" => new \external_value(PARAM_TEXT, 'competency ID number'),
"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),
"shortname" => new \external_value(PARAM_RAW),
"idnumber" => new \external_value(PARAM_TEXT),
"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),
@ -84,6 +87,8 @@ class coursecompetencyinfo {
"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),
];
if($recurse) {
$struct["children"] = new \external_multiple_structure(self::competencyinfo_structure(false),'child competencies',VALUE_OPTIONAL);
@ -120,27 +125,54 @@ class coursecompetencyinfo {
* @param Object $competency
*/
private function competencyinfo_model($competency) : array {
$path = [];
$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'),
'shortname' => $c->get('shortname'),
'idnumber' => $c->get('idnumber'),
'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'),
'shortname' => $competency->get('shortname'),
'idnumber' => $competency->get('idnumber'),
'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'),
'shortname' => $competency->get('shortname'),
'idnumber' => $competency->get('idnumber'),
'title' => $title,
'details' => $competency->get($detailfield),
'description' => $competency->get('description'),
'path' => $path,
];
@ -162,37 +194,80 @@ class coursecompetencyinfo {
$ncourseproficient = 0;
foreach($coursecompetencies as $c) {
$stats = $this->proficiency_stats($c,$studentlist);
$count += $stats->count;
$nproficient += $stats->nproficient;
$ncourseproficient += $stats->ncourseproficient;
if(!empty($studentslist)){
$stats = $this->proficiency_stats($c,$studentlist);
$count += $stats->count;
$nproficient += $stats->nproficient;
$ncourseproficient += $stats->ncourseproficient;
}
$ci = $this->competencyinfo_model($c);
// Copy proficiency stats to model.
foreach ((array)$stats as $key => $value) {
$ci[$key] = $value;
}
$ci['required'] = $this->is_required($c);
// 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);
$children[] = $cci;
}
$rule = $c->get_rule_object();
$ruleoutcome = $c->get('ruleoutcome');
if($rule && $ruleoutcome != competency::OUTCOME_NONE) {
$ruletext = $rule->get_name();
$ruleconfig = $c->get('ruleconfig');
$ci["children"] = $children;
}
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";
}
$model["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;
}
$model["rule"] = $ruletext . " ({$points} ".get_string("points","core_grades").")";
} else {
$model["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,
"fproficient" => (float)($nproficient)/(float)($count),
"fcourseproficient" => (float)($ncourseproficient)/(float)($count),
];
if(!empty($studentslist)){
$info["fproficient"] = (float)($nproficient)/(float)($count);
$info["fcourseproficient"] = (float)($ncourseproficient)/(float)($count);
}
return $info;
}
@ -218,24 +293,71 @@ class coursecompetencyinfo {
$progress += 1;
}
// 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);
$cp = $this->proficiency($cc,$userid);
// Copy proficiency info to model.
foreach ((array)$cp as $key => $value) {
$cci[$key] = $value;
}
$children[] = $cci;
}
$rule = $c->get_rule_object();
$ruleoutcome = $c->get('ruleoutcome');
if($rule && $ruleoutcome != competency::OUTCOME_NONE) {
$ruletext = $rule->get_name();
$ruleconfig = $c->get('ruleconfig');
$ci["children"] = $children;
}
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";
}
$model["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) {
$children = [];
foreach($dids as $did) {
$cc = new competency($did);
$cci = $this->competencyinfo_model($cc);
if($rule instanceof competency_rule_points) {
$cp = $p = $this->proficiency($cc,$userid);
// Copy proficiency info to model.
foreach ((array)$cp as $key => $value) {
$cci[$key] = $value;
}
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;
}
}
}
$children[] = $cci;
}
$ci["children"] = $children;
}
if ($rule instanceof competency_rule_points) {
$model["rule"] = $ruletext . " ({$points} / {$pointsreq} ".get_string("points","core_grades").")";
} else {
$model["rule"] = $ruletext;
}
}
$cis[] = $ci;
}
@ -299,4 +421,63 @@ class coursecompetencyinfo {
$r->coursegrade = $scale->get_nearest_item($ucc->get('grade'));
return $r;
}
/**
* 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);
}
}

View File

@ -173,6 +173,8 @@ class competency_aggregator extends \local_treestudyplan\aggregator {
$count = 0;
$courseproficient = 0;
$proficient = 0;
$requiredmet = 0;
$requiredcount = 0;
foreach ($competencies as $c) {
$count += 1;
$p = $cci->proficiency($c,$userid);
@ -182,19 +184,26 @@ class competency_aggregator extends \local_treestudyplan\aggregator {
if ($p->courseproficient) {
$courseproficient += 1;
}
if ($cci->is_required(($c))){
$requiredcount += 1;
if ($p->proficient) {
$requiredmet += 1;
}
}
}
// Determine minimum for
$limit = $this->cfg()->thresh_completed * $count;
$coursefinished = ($course->enddate) ? ($course->enddate < time()) : false;
if ($courseproficient >= $count) {
if ($courseproficient >= $count && $requiredmet >= $requiredcount) {
if ($limit < $count) {
return completion::EXCELLENT;
} else {
return completion::COMPLETED;
}
} else if ($courseproficient > $limit) {
} else if ($courseproficient > $limit && $requiredmet >= $requiredcount) {
return completion::COMPLETED;
} else if ($courseproficient > 0) {
if ( $this->cfg()->use_failed && $coursefinished) {

View File

@ -132,10 +132,8 @@ class tristate_aggregator extends \local_treestudyplan\aggregator {
* @return int Aggregated completion as completion class constant
*/
public function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid) {
$condition = $studyitem->conditions();
if (empty($condition)) {
$condition = self::DEFAULT_CONDITION;
}
$condition = self::DEFAULT_CONDITION;
$list = [];
foreach (gradeinfo::list_studyitem_gradables($studyitem) as $gi) {
$list[] = $this->grade_completion($gi, $userid);

View File

@ -84,7 +84,11 @@ class studyitem {
* Return the condition string for this item
*/
public function conditions() : string {
return (!empty($this->r->conditions)) ? $this->r->conditions : "ALL";
if($this->r->type == self::COURSE) {
return (!empty($this->r->conditions)) ? $this->r->conditions : "";
} else {
return (!empty($this->r->conditions)) ? $this->r->conditions : "ALL";
}
}
/**

View File

@ -288,6 +288,10 @@ $string["settingdesc_bistate_thresh_progress"] = 'Minimum percentage of outcomes
$string["setting_bistate_accept_pending_submitted"] = 'Accept submitted but ungraded result as "progress"';
$string["settingdesc_bistate_accept_pending_submitted"] = 'If enabled, submitted but ungraded outcomes will still count toward progress. If disabled, only graded outcomes will count';
$string["setting_competency_displayname"] = 'Competency display title';
$string["settingdesc_competency_displayname"] = 'The competency field used as it\'s title in the studyplan';
$string["setting_competency_detailfield"] = 'Competency detail field';
$string["settingdesc_competency_detailfield"] = 'The competency field used for competency details';
$string["setting_competency_heading"] = 'Defults for course competencies ';
$string["settingdesc_competency_heading"] = 'Set the defaults for this aggregation method';
$string["setting_competency_thresh_completed"] = 'Threshold for good (%)';

View File

@ -287,6 +287,10 @@ $string["settingdesc_bistate_thresh_progress"] = 'Minimumpercentage van doelen d
$string["setting_bistate_accept_pending_submitted"] = 'Accepteer nog niet beoordeelde doelen als "in ontwikkeling"';
$string["settingdesc_bistate_accept_pending_submitted"] = 'Neem leerdoelen waarbij bewijs is ingeleverd, maar wat nog niet is beoordeeld mee als "in ontwikkeling", zolang er nog geen beoordeling is';
$string["setting_competency_displayname"] = 'Weergavetitel competentie';
$string["settingdesc_competency_displayname"] = 'Veld dat wordt gebruikt als titel voor een competentie';
$string["setting_competency_detailfield"] = 'Detailinfo competentie';
$string["settingdesc_competency_detailfield"] = 'Veld dat wordt gebruikt voor korte competentie details. ';
$string["setting_competency_heading"] = 'Standaardwaarden voor cursuscompetenties ';
$string["settingdesc_competency_heading"] = 'Stel de standaardwaarden in voor deze verzamelmethode';
$string["setting_competency_thresh_completed"] = 'Drempelwaarde voor voltooid (%)';

View File

@ -405,15 +405,6 @@ function local_treestudyplan_pluginfile(
): bool {
global $DB,$USER;
$fp = fopen("/tmp/debug","a+");
fputs($fp, print_r([
'context' => $context,
'filearea' => $filearea,
'args' => $args,
'forcedownload' => $forcedownload,
'options' => $options,
],true)."\n\n");
fclose($fp);
$studyplan_filecaps = ["local/treestudyplan:editstudyplan","local/treestudyplan:viewuserreports"];

View File

@ -107,7 +107,27 @@ if ($hassiteconfig) {
$displayfields
));
// BISTATE AGGREGATON DEFAULTS.
// COMPETENCY AGGREGATON DEFAULTS.
$page->add(new admin_setting_configselect('local_treestudyplan/competency_displayname',
get_string('setting_competency_displayname', 'local_treestudyplan'),
get_string('settingdesc_competency_displayname', 'local_treestudyplan'),
"idnumber",
["shortname" => get_string("name","core",),
"idnumber" => get_string("idnumber","core",),
"description" => get_string("description","core",)]
));
$page->add(new admin_setting_configselect('local_treestudyplan/competency_detailfield',
get_string('setting_competency_detailfield', 'local_treestudyplan'),
get_string('settingdesc_competency_detailfield', 'local_treestudyplan'),
"shortname",
["" => get_string("none","core",),
"shortname" => get_string("name","core",),
"idnumber" => get_string("idnumber","core",),
"description" => get_string("description","core",)]
));
$page->add(new admin_setting_heading('local_treestudyplan/competency_aggregation_heading',
get_string('setting_competency_heading', 'local_treestudyplan'),
get_string('settingdesc_competency_heading', 'local_treestudyplan')

View File

@ -22,7 +22,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'local_treestudyplan'; // Recommended since 2.0.2 (MDL-26035). Required since 3.0 (MDL-48494).
$plugin->version = 2023111700; // YYYYMMDDHH (year, month, day, iteration).
$plugin->version = 2023112301; // YYYYMMDDHH (year, month, day, iteration).
$plugin->requires = 2021051700; // YYYYMMDDHH (This is the release version for Moodle 3.11).
$plugin->release = "1.1.0-b";