Competency backend/frontend

This commit is contained in:
PMKuipers 2023-11-23 07:44:04 +01:00
parent 4a97078405
commit 54a8823bbd
19 changed files with 828 additions and 149 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -26,6 +26,7 @@ import Debugger from './util/debugger';
import {load_strings} from './util/string-helper';
import {ProcessStudyplan} from './studyplan-processor';
import {download,upload} from './downloader';
import {studyplanTiming} from './util/date-helper';
import PortalVue from './portal-vue/portal-vue.esm';
Vue.use(PortalVue);
@ -128,8 +129,8 @@ export function init(contextid,categoryid,options) {
}])[0].done(function(response){
const timingval = { future: 0, present: 1, past: 2, };
response.sort((a,b) => {
const timinga = TSComponents.studyplanTiming(a);
const timingb = TSComponents.studyplanTiming(b);
const timinga = studyplanTiming(a);
const timingb = studyplanTiming(b);
let t = timingval[timinga] - timingval[timingb];
if(t == 0){

View File

@ -234,6 +234,10 @@ export default {
completion_not_configured: "completion_not_configured",
configure_completion: "configure_completion",
},
competency: {
competency_not_configured: "competency_not_configured",
configure_competency: "configure_competency",
},
badge: {
share_badge: "share_badge",
dateissued: "dateissued",
@ -2992,6 +2996,7 @@ export default {
`,
});
//TAG: Course item
Vue.component('t-item-course', {
props: {
value:{
@ -3032,7 +3037,7 @@ export default {
},
configurationState(){
if(this.hasGrades() || this.hasCompletions()) {
if(this.hasGrades() || this.hasCompletions() || this.hasCompetencies()) {
return "t-configured-ok";
} else {
return "t-configured-alert";
@ -3040,7 +3045,7 @@ export default {
},
configurationIcon(){
if(this.hasGrades() || this.hasCompletions()) {
if(this.hasGrades() || this.hasCompletions() || this.hasCompetencies()) {
return "check";
} else {
return "exclamation-circle";
@ -3080,6 +3085,12 @@ export default {
}
return false;
},
hasCompetencies() {
if(this.value.course.competency && this.value.course.competency.competencies) {
return (this.value.course.competency.competencies.length > 0);
}
return false;
},
includeChanged(newValue,g){
call([{
methodname: 'local_treestudyplan_include_grade',
@ -3177,6 +3188,11 @@ export default {
v-model='value.course.completion'
:course='value.course'
></t-item-course-completion>
<t-item-course-competency
v-if='!!value.course.competency'
v-model='value.course.competency'
:course='value.course'
></t-item-course-competency>
<template #modal-footer="{ ok, cancel, hide }" >
<a href='#' @click='$emit("deleterq")' class="text-danger"
@ -3383,7 +3399,123 @@ export default {
`,
});
//TAG: Course competency
Vue.component('t-item-course-competency',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
course: {
type: Object,
default: function(){ return {};},
},
},
data() {
return {
text: strings.competency,
};
},
created(){
const self = this;
// Get text strings for condition settings
let stringkeys = [];
for(const key in this.text){
stringkeys.push({ key: key, component: 'local_treestudyplan'});
}
get_strings(stringkeys).then(function(strings){
let i = 0;
for(const key in self.text){
self.text[key] = strings[i];
i++;
}
});
},
computed: {
hasCompletions() {
if(this.value.conditions) {
for(const cgroup of this.value.conditions){
if(cgroup.items && cgroup.items.length > 0){
return true;
}
}
}
return false;
},
},
methods: {
completion_icon(completion) {
switch(completion){
case "progress":
return "exclamation-circle";
case "complete":
return "check-circle";
case "complete-pass":
return "check-circle";
case "complete-fail":
return "times-circle";
default: // case "incomplete"
return "circle-o";
}
},
completion_tag(cgroup){
return cgroup.completion?'completed':'incomplete';
},
pathtags(competency,use_idnumber=false) {
const path = competency.path;
let s = "";
for (const ix in path) {
const p = path[ix];
if ( ix > 0) {
s += " / ";
}
s += `<a href="/admin/tool/lp/competencies.php?competencyid=${p.id}">${(use_idnumber)?p.idnumber:p.shortname}</a>`;
}
},
},
template: `
<table class="r-item-course-grade-details">
<tr v-if="value.competencies.length == 0">
<td colspan='2'>{{text.competencies_not_configured}}!
<br><a :href="'/admin/tool/lp/coursecompetencies.php?courseid='+course.id" target='_blank'>{{text.configure_competencies}}</a>
</td>
</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>
<b-modal :id="'modal-competency-id-'+c.id"
:title="c.path.join(' / ')"
size="lg"
ok-only
centered
scrollable
>
<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>
<div>{{ pathtags(c) }}</div>
</div>
</template>
</b-modal>
</tr>
</template>
</table>
`,
});
/************************************
* *
* Toolbox list components *

View File

@ -68,6 +68,7 @@ abstract class aggregator {
// And this is faster than any dynamic method.
return [
"core", // Use moodle core completion.
"competency",
"bistate",
"tristate", // Deprecated.
];
@ -161,23 +162,27 @@ abstract class aggregator {
*/
abstract public function grade_completion(gradeinfo $gradeinfo, $userid);
/**
* 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.
*/
abstract public function use_required_grades();
/**
* Determine if aggregation method makes use of required grades
* @return bool True if aggregation method makes use of
*/
abstract public function use_item_conditions();
/**
* Determine if the aggregation method uses core_completion, or treestudyplan custom completion.
* @return bool True if the aggregation method uses core_completion
*/
public function usecorecompletioninfo() {
public function use_corecompletioninfo() {
return false;
}
/**
* Determine if the aggregation method uses course competencies,
* @return bool True if the aggregation method uses course competencies
*/
public function use_coursecompetencies() {
return false;
}
/**
* 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 false;
}
@ -196,8 +201,6 @@ abstract class aggregator {
*/
public static function basic_structure($value = VALUE_REQUIRED) : \external_description {
return new \external_single_structure([
"useRequiredGrades" => new \external_value(PARAM_BOOL, 'id of studyplan'),
"useItemConditions" => new \external_value(PARAM_BOOL, 'name of studyplan'),
], "Aggregator requirements", $value);
}
@ -207,8 +210,6 @@ abstract class aggregator {
*/
public function basic_model() {
return [
"useRequiredGrades" => $this->use_required_grades(),
"useItemConditions" => $this->use_item_conditions(),
];
}

View File

@ -0,0 +1,302 @@
<?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\competency;
use core_competency\api as c_api;
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'),
"shortname" => new \external_value(PARAM_RAW, 'competency short name'),
"idnumber" => new \external_value(PARAM_TEXT, 'competency ID number'),
"description" => new \external_value(PARAM_RAW, 'competency description'),
"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),
]), '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),
];
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([
"progress" => new \external_value(PARAM_INT, 'number completed competencies'),
"competencies" => new \external_multiple_structure(self::competencyinfo_structure(), '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 {
$path = [];
foreach ($competency->get_ancestors() as $c) {
$competencypath[] = $c->get('shortname');
$path[] = [
'id' => $c->get('id'),
'shortname' => $c->get('shortname'),
'idnumber' => $c->get('idnumber'),
];
}
$path[] = [
'id' => $competency->get('id'),
'shortname' => $competency->get('shortname'),
'idnumber' => $competency->get('idnumber'),
];
$model = [
'id' => $competency->get('id'),
'shortname' => $competency->get('shortname'),
'idnumber' => $competency->get('idnumber'),
'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) {
$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;
}
// 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;
}
$ci["children"] = $children;
}
$cis[] = $ci;
}
$info = [
"competencies" => $cis,
"fproficient" => (float)($nproficient)/(float)($count),
"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;
}
if ($p->proficient || $p->courseproficient) {
$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;
}
$ci["children"] = $children;
}
$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');
$uc = c_api::get_user_competency($userid, $competencyid);
$ucc = c_api::get_user_competency_in_course($this->course->id,$userid,$competencyid);
$r = new \stdClass();
$r->proficient = $uc->get('proficiency');
$r->courseproficient = $ucc->get('proficiency');
$r->grade = $scale->get_nearest_item($uc->get('grade'));
$r->coursegrade = $scale->get_nearest_item($ucc->get('grade'));
return $r;
}
}

View File

@ -283,6 +283,7 @@ class courseinfo {
"grades" => new \external_multiple_structure(gradeinfo::editor_structure(),
'grade list (legacy list)', VALUE_OPTIONAL),
"completion" => corecompletioninfo::editor_structure(VALUE_OPTIONAL),
"competency" => coursecompetencyinfo::editor_structure(VALUE_OPTIONAL),
"timing" => new \external_value(PARAM_TEXT, '(past|present|future)'),
"startdate" => new \external_value(PARAM_TEXT, 'Course start date'),
"enddate" => new \external_value(PARAM_TEXT, 'Course end date'),
@ -297,10 +298,9 @@ class courseinfo {
/**
* Webservice model for editor info
* @param studyitem $studyitem Specify a specific study item to check gradable selections for. Leave empty to use default
* @param bool $usecorecompletioninfo Whether to use corecompletion info instead of custom selected gradables
* @return array Webservice data model
*/
public function editor_model($usecorecompletioninfo = false) {
public function editor_model() {
$contextinfo = new contextinfo($this->context);
$timing = $this->timing();
@ -329,16 +329,24 @@ class courseinfo {
];
if (isset($this->studyitem)) {
if (!$usecorecompletioninfo) {
$aggregator = $this->studyitem->studyline()->studyplan()->aggregator();
if ($aggregator->use_manualactivityselection()) {
$gradables = gradeinfo::list_course_gradables($this->course, $this->studyitem );
foreach ($gradables as $gradable) {
$info['grades'][] = $gradable->editor_model($this->studyitem);
}
} else {
}
if ($aggregator->use_corecompletioninfo()) {
$cc = new corecompletioninfo($this->course, $this->studyitem);
$studentlist = $this->studyitem->studyline()->studyplan()->find_linked_userids();
$info['completion'] = $cc->editor_model($studentlist);
}
if ($aggregator->use_coursecompetencies()) {
$ci = new coursecompetencyinfo($this->course, $this->studyitem);
$studentlist = $this->studyitem->studyline()->studyplan()->find_linked_userids();
$info['competency'] = $ci->editor_model($studentlist);
}
}
return $info;
}
@ -357,6 +365,7 @@ class courseinfo {
"ctxid" => new \external_value(PARAM_INT, 'course context id name'),
"grades" => new \external_multiple_structure(gradeinfo::user_structure(), 'grade list (legacy list)', VALUE_OPTIONAL),
"completion" => corecompletioninfo::user_structure(VALUE_OPTIONAL),
"competency" => coursecompetencyinfo::user_structure(VALUE_OPTIONAL),
"timing" => new \external_value(PARAM_TEXT, '(past|present|future)'),
"startdate" => new \external_value(PARAM_TEXT, 'Course start date'),
"enddate" => new \external_value(PARAM_TEXT, 'Course end date'),
@ -416,10 +425,9 @@ class courseinfo {
/**
* Webservice model for user info
* @param int $userid ID of user to check specific info for
* @param bool $usecorecompletioninfo Whether to use corecompletion info instead of custom selected gradables
* @return array Webservice data model
*/
public function user_model($userid, $usecorecompletioninfo = false) {
public function user_model($userid) {
global $DB;
$contextinfo = new contextinfo($this->context);
@ -439,16 +447,24 @@ class courseinfo {
'enrolled' => $this->is_enrolled_student($userid),
];
if (isset($this->studyitem)) {
if (!$usecorecompletioninfo) {
$aggregator = $this->studyitem->studyline()->studyplan()->aggregator();
if ($aggregator->use_manualactivityselection()) {
$gradables = gradeinfo::list_studyitem_gradables($this->studyitem);
foreach ($gradables as $gi) {
$info['grades'][] = $gi->user_model($userid);
}
} else if (isset($this->studyitem)) {
}
if ($aggregator->use_corecompletioninfo()) {
$cc = new corecompletioninfo($this->course, $this->studyitem);
$info['completion'] = $cc->user_model($userid);
}
if ($aggregator->use_coursecompetencies()) {
$ci = new coursecompetencyinfo($this->course, $this->studyitem);
$info['competency'] = $ci->user_model($userid);
}
}
return $info;
}

View File

@ -131,21 +131,13 @@ class bistate_aggregator extends \local_treestudyplan\aggregator {
}
/**
* 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.
* Determine if the aggregation method uses manual activity selection,
* @return bool True if the aggregation method uses manual activity selection
*/
public function use_required_grades() {
public function use_manualactivityselection() {
return true;
}
/**
* Determine if aggregation method makes use of required grades
* @return bool True if aggregation method makes use of
*/
public function use_item_conditions() {
return false;
}
/**
* Aggregate completed/failed goals into one outcome
* @param int[] $completions List of completions (completion class constants)

View File

@ -0,0 +1,283 @@
<?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 with moodle course completion
* @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 core_competency\api as c_api;
use \local_treestudyplan\courseinfo;
use \local_treestudyplan\gradeinfo;
use \local_treestudyplan\studyitem;
use \local_treestudyplan\completion;
use local_treestudyplan\coursecompetencyinfo;
/**
* Aggregate course results with moodle course completion
*/
class competency_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_completed' => 0.66, // Minimum fraction that must be completed to aggregate as completed.
'use_failed' => true, // Support failed completion yes/no.
];
}
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_completed", ] as $key) {
$val = intval(get_config('local_treestudyplan', "competency_{$key}"));
if ($val >= 0 && $val <= 100) {
$this->cfg()->$key = floatval($val) / 100;
}
}
foreach (["use_failed", ] as $key) {
$this->cfg()->$key = boolval(get_config('local_treestudyplan', "competency_{$key}"));
}
// Next, decode json.
$config = \json_decode($configstr, true);
if (is_array($config)) {
// Copy all valid config settings to this item.
foreach (["thresh_completed", ] 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",] 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_completed" => 100 * $this->cfg()->thresh_completed,
"use_failed" => $this->cfg()->use_failed,
]);
}
/**
* Determine if aggregation method wants to select gradables
* @return bool True if aggregation method needs gradables to be selected
*/
public function select_gradables() {
return false;
}
/**
* Determine if aggregation method is deprecated
* @return bool True if aggregation method is deprecated
*/
public function deprecated() {
return self::DEPRECATED;
}
/**
* 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;
}
/**
* Determine if aggregation method makes use of required grades
* @return bool True if aggregation method makes use of
*/
public function use_item_conditions() {
return false;
}
/**
* Determine if the aggregation method uses course competencies,
* @return bool True if the aggregation method uses course competencies,
*/
public function use_coursecompetencies() {
return true;
}
/**
* Return course completion information based on the core completion infromation
* 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) {
// Retrieve the course competencies
$course = $courseinfo->course();
$cci = new coursecompetencyinfo($course,$studyitem);
$competencies = $cci->course_competencies();
$count = 0;
$courseproficient = 0;
$proficient = 0;
foreach ($competencies as $c) {
$count += 1;
$p = $cci->proficiency($c,$userid);
if ($p->proficient) {
$proficient += 1;
}
if ($p->courseproficient) {
$courseproficient += 1;
}
}
// Determine minimum for
$limit = $this->cfg()->thresh_completed * $count;
$coursefinished = ($course->enddate) ? ($course->enddate < time()) : false;
if ($courseproficient >= $count) {
if ($limit < $count) {
return completion::EXCELLENT;
} else {
return completion::COMPLETED;
}
} else if ($courseproficient > $limit) {
return completion::COMPLETED;
} else if ($courseproficient > 0) {
if ( $this->cfg()->use_failed && $coursefinished) {
return completion::FAILED;
} else {
return completion::PROGRESS;
}
} else {
if ( $this->cfg()->use_failed && $coursefinished) {
return completion::FAILED;
} else {
return completion::INCOMPLETE;
}
}
}
/**
* 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) {
// COURSE COMPETENCIES DOESN'T REALLY USE THIS FUNCTION.
}
}

View File

@ -97,7 +97,7 @@ class core_aggregator extends \local_treestudyplan\aggregator {
* Determine if the aggregation method uses core_completion, or treestudyplan custom completion.
* @return bool True if the aggregation method uses core_completion
*/
public function usecorecompletioninfo() {
public function use_corecompletioninfo() {
return true;
}
@ -217,82 +217,6 @@ class core_aggregator extends \local_treestudyplan\aggregator {
*/
public function grade_completion(gradeinfo $gradeinfo, $userid) {
// CORE COMPLETION DOESN'T REALLY USE THIS FUNCTION.
global $DB;
$table = "local_treestudyplan_gradecfg";
$gradeitem = $gradeinfo->get_gradeitem();
$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->get_gradingscanner()->pending($userid)) {
return completion::PENDING;
} else {
return completion::INCOMPLETE;
}
} else {
$grade = $gradeitem->get_final($userid);
// 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->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;
}
}
}
}
}

View File

@ -53,18 +53,10 @@ class tristate_aggregator extends \local_treestudyplan\aggregator {
}
/**
* 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.
* Determine if the aggregation method uses manual activity selection,
* @return bool True if the aggregation method uses manual activity selection
*/
public function use_required_grades() {
return false;
}
/**
* Determine if aggregation method makes use of required grades
* @return bool True if aggregation method makes use of
*/
public function use_item_conditions() {
public function use_manualactivityselection() {
return true;
}

View File

@ -207,8 +207,7 @@ class studentstudyplanservice extends \external_api {
return [];
}
// Validate context now.
\external_api::validate_context(\context_system::instance());
// Do not validate the context in this instance to avoid requiring logins when this is not wanted
$userid = $invite->user_id;
@ -262,8 +261,7 @@ class studentstudyplanservice extends \external_api {
return [];
}
// Validate context now.
\external_api::validate_context(\context_system::instance());
// Do not validate the context in this instance to avoid requiring logins when this is not wanted
$studyplan = studyplan::find_by_id($studyplanid);
$userid = $invite->user_id;
@ -317,8 +315,7 @@ class studentstudyplanservice extends \external_api {
return [];
}
// Validate context now.
\external_api::validate_context(\context_system::instance());
// Do not validate the context in this instance to avoid requiring logins when this is not wanted
$page = studyplanpage::find_by_id($pageid);
$studyplan = $page->studyplan();

View File

@ -229,7 +229,7 @@ class studyitem {
if ($mode == "export") {
$model['course'] = $ci->shortname();
} else {
$model['course'] = $ci->editor_model($this->aggregator->usecorecompletioninfo());
$model['course'] = $ci->editor_model();
}
}
@ -501,7 +501,7 @@ class studyitem {
// Add course if available.
if (courseinfo::exists($this->r->course_id)) {
$cinfo = $this->getcourseinfo();
$model['course'] = $cinfo->user_model($userid, $this->aggregator->usecorecompletioninfo());
$model['course'] = $cinfo->user_model($userid);
}
// Add incoming and outgoing connection info.

View File

@ -266,25 +266,35 @@ $string["bistate_aggregator_desc"] = 'Outcomes are completed or not (e.g. not st
$string["core_aggregator_title"] = 'Moodle course completion';
$string["core_aggregator_desc"] = 'Use Moodle core completion';
$string["competency_aggregator_title"] = 'Course competencies';
$string["competency_aggregator_desc"] = 'Use the competencies linekd to the course';
$string["setting_bistate_heading"] = 'Defaults for Completed + Required outcomes';
$string["settingdesc_bistate_heading"] = 'Set the defaults for this aggregation method';
$string["choose_aggregation_style"] = 'Choose aggregation style';
$string["select_scaleitem"] = 'Choose...';
$string["setting_bistate_thresh_excellent"] = 'Threshold for excellent';
$string["setting_bistate_thresh_excellent"] = 'Threshold for excellent (%)';
$string["settingdesc_bistate_thresh_excellent"] = 'Minimum percentage of outcomes completed for result "Excellent"';
$string["setting_bistate_thresh_good"] = 'Threshold for good';
$string["setting_bistate_thresh_good"] = 'Threshold for good (%)';
$string["settingdesc_bistate_thresh_good"] = 'Minimum percentage of outcomes completed for result "Good"';
$string["setting_bistate_thresh_completed"] = 'Threshold for completed';
$string["setting_bistate_thresh_completed"] = 'Threshold for completed (%)';
$string["settingdesc_bistate_thresh_completed"] = 'Minimum percentage of outcomes completed for result "Completed"';
$string["setting_bistate_support_failed"] = 'Support "Failed" result';
$string["settingdesc_bistate_support_failed"] = 'Whether the result "Failed" is supported or not';
$string["setting_bistate_thresh_progress"] = 'Threshold for progress';
$string["settingdesc_bistate_support_failed"] = 'When the course end date has passed, mark course as "Failed" instead of "Progress"';
$string["setting_bistate_thresh_progress"] = 'Threshold for progress (%)';
$string["settingdesc_bistate_thresh_progress"] = 'Minimum percentage of outcomes that should not be failed in order to qualify for progress result. <br><strong>Only relevant if "Failed" results are supported</strong>';
$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_heading"] = 'Defults for course competencies ';
$string["settingdesc_competency_heading"] = 'Set the defaults for this aggregation method';
$string["setting_competency_thresh_completed"] = 'Threshold for good (%)';
$string["settingdesc_competency_thresh_completed"] = 'Minimum percentage of proficient competencies for result "Completed';
$string["setting_competency_support_failed"] = 'Support "Failed" result';
$string["settingdesc_competency_support_failed"] = 'When the course end date has passed, mark course as "Failed" instead of "Progress"';
$string["grade_include"] = "Include";
$string["grade_require"] = "Require";
$string["required_goal"] = "Required outcome";

View File

@ -263,6 +263,10 @@ $string["bistate_aggregator_desc"] = 'Doelen zijn behaald of niet (o.a. niet ges
$string["core_aggregator_title"] = 'Moodle cursusvoltooiing';
$string["core_aggregator_desc"] = 'Gebruik de ingesteld cursusvoltooiing';
$string["competency_aggregator_title"] = 'Curuscompetenties';
$string["competency_aggregator_desc"] = 'Gebruik de bij de cursus ingestelde competenties';
$string["setting_bistate_heading"] = 'Standaardwaarden voor Behaald + Vereidte leerdoelen ';
$string["settingdesc_bistate_heading"] = 'Stel de standaardwaarden in voor deze verzamelmethode';
@ -271,11 +275,11 @@ $string["choose_aggregation_style"] = 'Kies berekening van eindresultaten';
$string["select_scaleitem"] = 'Kies...';
$string["setting_bistate_thresh_excellent"] = 'Drempelwaarde voor uitstekend (%)';
$string["settingdesc_bistate_thresh_excellent"] = 'Minimumpercentage of goals completed for result "Excellent"';
$string["settingdesc_bistate_thresh_excellent"] = 'Minimumpercentage behaalde doelen voor "Uitstekend"';
$string["setting_bistate_thresh_good"] = 'Drempelwaarde voor goed (%)';
$string["settingdesc_bistate_thresh_good"] = 'Minimum percentage of goals completed for result "Good"';
$string["settingdesc_bistate_thresh_good"] = 'Minimum ercentage behaalde doelen voor "Goed"';
$string["setting_bistate_thresh_completed"] = 'Drempelwaarde voor voltooid (%)';
$string["settingdesc_bistate_thresh_completed"] = 'Minimumpercentage of goals completed for result "Completed"';
$string["settingdesc_bistate_thresh_completed"] = 'Minimumpercentage behaalde doelen voor "Voltooid"';
$string["setting_bistate_support_failed"] = 'Onvoldoende ingeschakeld';
$string["settingdesc_bistate_support_failed"] = 'Vink aan om "Onvoldoende" weer te kunnen geven als resultaat voor een module';
$string["setting_bistate_thresh_progress"] = 'Drempelwaarde voor "in ontwikkeling" (%)';
@ -283,6 +287,13 @@ $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_heading"] = 'Standaardwaarden voor cursuscompetenties ';
$string["settingdesc_competency_heading"] = 'Stel de standaardwaarden in voor deze verzamelmethode';
$string["setting_competency_thresh_completed"] = 'Drempelwaarde voor voltooid (%)';
$string["settingdesc_competency_thresh_completed"] = 'Minimumpercentage behaalde competenties voor "Behaald"';
$string["setting_competency_support_failed"] = 'Onvoldoende ingeschakeld';
$string["settingdesc_competency_support_failed"] = 'Vink aan om "Onvoldoende" weer te kunnen geven als eind resultaat voor een cursus';
$string["grade_include"] = "Doel";
$string["grade_require"] = "Verplicht";
$string["required_goal"] = "Verplicht leerdoel";

View File

@ -107,6 +107,24 @@ if ($hassiteconfig) {
$displayfields
));
// BISTATE AGGREGATON DEFAULTS.
$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')
));
$page->add(new admin_setting_configtext('local_treestudyplan/competency_thresh_completed',
get_string('setting_competency_thresh_completed', 'local_treestudyplan'),
get_string('settingdesc_competency_thresh_completed', 'local_treestudyplan'),
"66",
PARAM_INT
));
$page->add(new admin_setting_configcheckbox('local_treestudyplan/competency_support_failed',
get_string('setting_competency_support_failed', 'local_treestudyplan'),
get_string('settingdesc_competency_support_failed', 'local_treestudyplan'),
true,
));
// BISTATE AGGREGATON DEFAULTS.
$page->add(new admin_setting_heading('local_treestudyplan/bistate_aggregation_heading',
get_string('setting_bistate_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 = 2023111202; // YYYYMMDDHH (year, month, day, iteration).
$plugin->version = 2023111700; // YYYYMMDDHH (year, month, day, iteration).
$plugin->requires = 2021051700; // YYYYMMDDHH (This is the release version for Moodle 3.11).
$plugin->release = "1.1.0-b";