Implemented teacher view for competencies

This commit is contained in:
PMKuipers 2023-11-27 23:11:17 +01:00
parent 456e9b503e
commit 96caeb895a
16 changed files with 233 additions and 181 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -64,12 +64,13 @@ export function init(contextid,categoryid,options) {
if ( !options.defaultAggregation ) { if ( !options.defaultAggregation ) {
options.defaultAggregation = "core"; options.defaultAggregation = "core";
} }
if ( !options.editMode ) {
options.editMode = false;
}
} else { } else {
options = { defaultAggregation: "core"}; options = { defaultAggregation: "core", editMode: false};
} }
const in_systemcontext = (contextid <= 1);
// Setup the initial Vue app for this page // Setup the initial Vue app for this page
let app = new Vue({ let app = new Vue({
el: '#root', el: '#root',
@ -100,6 +101,7 @@ export function init(contextid,categoryid,options) {
courses: [], courses: [],
text: strings.studyplan, text: strings.studyplan,
usedcontexts: [], usedcontexts: [],
initialEditMode: !!options.editMode,
}, },
created() { created() {
this.$root.$on('studyplanRemoved',(studyplan)=>{ this.$root.$on('studyplanRemoved',(studyplan)=>{

View file

@ -16,8 +16,8 @@ import {svgarcpath} from './util/svgarc';
import Debugger from './util/debugger'; import Debugger from './util/debugger';
import Config from 'core/config'; import Config from 'core/config';
import {ProcessStudyplan, ProcessStudyplanPage, objCopy} from './studyplan-processor'; import {ProcessStudyplan, ProcessStudyplanPage, objCopy} from './studyplan-processor';
import TSComponents from './treestudyplan-components'; import TSComponents from './treestudyplan-components';
import {eventTypes as editSwEventTypes} from 'core/edit_switch';
// Make π available as a constant // Make π available as a constant
const π = Math.PI; const π = Math.PI;
@ -180,6 +180,13 @@ export default {
// Create new eventbus for interaction between item components // Create new eventbus for interaction between item components
const ItemEventBus = new Vue(); const ItemEventBus = new Vue();
// Add event listener for the edit mode event so we can react to it, or at the very least ignore it
document.addEventListener(editSwEventTypes.editModeSet,(e) => {
e.preventDefault();
ItemEventBus.$emit('editModeSet',e.detail.editMode);
});
Vue.component('r-progress-circle',{ Vue.component('r-progress-circle',{
props: { props: {
value: { value: {
@ -1606,11 +1613,15 @@ export default {
</tr> </tr>
<template v-else> <template v-else>
<tr v-for='c in value.competencies'> <tr v-for='c in value.competencies'>
<td :colspan="(c.details)?1:2"> <td>
<a href="#" v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.title'></span></a> <a href="#" v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.title'></span></a>
</td> </td>
<td class='details' v-if="c.details"> <td class='details' >
<a href="#" v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.details'></span></a> <a v-if="c.details" href="#" v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.details'></span></a>
<abbr v-if="c.required" :title="text.required"
:class="'s-required ' + + completion_tag(c)"
><i class='fa fa-asterisk' ></i
></abbr>
</td> </td>
<td :colspan="(c.required)?1:2"> <td :colspan="(c.required)?1:2">
<span :class="'r-completion-'+completion_tag(c)"> <span :class="'r-completion-'+completion_tag(c)">
@ -1630,9 +1641,6 @@ export default {
</template> </template>
</span> </span>
</td> </td>
<td v-if="c.required">
<span class="text-danger" v-if="c.required">{{ text.required }}</span>
</td>
<td v-if="c.feedback"> <td v-if="c.feedback">
<a v-b-modal="'r-competency-feedback-'+c.id" <a v-b-modal="'r-competency-feedback-'+c.id"
href="#" href="#"
@ -1675,17 +1683,22 @@ export default {
<th colspan="3">{{text.results}}</th> <th colspan="3">{{text.results}}</th>
</tr> </tr>
<tr v-for="cc in c.children"> <tr v-for="cc in c.children">
<td :colspan="(c.details)?1:2" > <td >
<a :href='competencyurl(c)' target="_blank"><span v-html='cc.title'></span></a> <a :href='competencyurl(c)' target="_blank"><span v-html='cc.title'></span></a>
</td> </td>
<td class='details' v-if="cc.details"> <td class='details'>
<a :href='competencyurl(c)' target="_blank"><span v-html='cc.details'></span></a> <a v-if="cc.details" :href='competencyurl(c)' target="_blank"><span v-html='cc.details'></span></a>
<abbr v-if="c.required" :title="text.required"
:class="'s-required ' + + completion_tag(cc)"
><i class='fa fa-asterisk' ></i
></abbr>
</td> </td>
<td><span :class="'r-completion-'+completion_tag(cc)" <td><span :class="'r-completion-'+completion_tag(cc)"
><i :class="'fa fa-'+completion_icon(cc)" :title="text['completion_'+completion_tag(cc)]"></i> ><i :class="'fa fa-'+completion_icon(cc)" :title="text['completion_'+completion_tag(cc)]"></i>
{{ (cc.proficient === null)?text.unrated:cc.grade }}</span></td> {{ (cc.proficient === null)?text.unrated:cc.grade }}</span></td>
<td><span class="text-info">{{ cc.points }} {{ text.points }}</span></td> <td><span class="text-info">{{ cc.points }} {{ text.points }}</span></td>
<td><span class="text-danger" v-if='cc.required'>{{ text.required }}</span></td> <td>
</td>
<td v-if="cc.feedback"> <td v-if="cc.feedback">
<a v-b-modal="'r-competency-feedback-'+cc.id" <a v-b-modal="'r-competency-feedback-'+cc.id"
href="#" href="#"
@ -1782,10 +1795,10 @@ export default {
ungraded: 0, ungraded: 0,
}; };
if(this.value.course.completion){ if(this.value.course.completion) {
for(const cond of this.value.course.completion.conditions){ for(const cond of this.value.course.completion.conditions) {
for(const itm of cond.items){ for(const itm of cond.items) {
if(itm.progress){ if(itm.progress) {
status.students += itm.progress.students; status.students += itm.progress.students;
status.completed += itm.progress.completed; status.completed += itm.progress.completed;
status.completed_pass += itm.progress.completed_pass; status.completed_pass += itm.progress.completed_pass;
@ -1794,10 +1807,12 @@ export default {
} }
} }
} }
} else if (this.value.course.competency) {
} else if (this.value.course.grades){ status.students = this.value.course.competency.total;
for( const g of this.value.course.grades){ status.completed = this.value.course.competency.proficient;
if(g.grading){ } else if (this.value.course.grades) {
for( const g of this.value.course.grades) {
if(g.grading) {
status.students += g.grading.students; status.students += g.grading.students;
status.completed += g.grading.completed; status.completed += g.grading.completed;
status.completed_pass += g.grading.completed_pass; status.completed_pass += g.grading.completed_pass;
@ -2276,7 +2291,7 @@ export default {
}); });
//TAG: Course competency //TAG: Teacher Course competency
Vue.component('r-item-teacher-course-competency',{ Vue.component('r-item-teacher-course-competency',{
props: { props: {
value : { value : {
@ -2313,23 +2328,30 @@ export default {
}, },
}, },
methods: { methods: {
completion_icon(completion) { completion_icon(competency) {
switch(completion){ if (competency.proficient && competency.courseproficient) {
case "progress": return "check-circle";
return "exclamation-circle"; } else if (competency.proficient) {
case "complete": return "check";
return "check-circle"; } else if (competency.proficient === false) {
case "complete-pass": return "times-circle";
return "check-circle"; } else {
case "complete-fail": return "circle-o";
return "times-circle";
default: // case "incomplete"
return "circle-o";
} }
}, },
completion_tag(cgroup){ completion_tag(competency){
return cgroup.completion?'completed':'incomplete'; if (competency.proficient && competency.courseproficient) {
return "completed";
} else if (competency.proficient) {
return "completed";
} else if (competency.proficient === false) {
return "failed";
} else if (competency.progress) {
return "progress";
} else {
return "incomplete";
}
}, },
pathtags(competency) { pathtags(competency) {
@ -2342,48 +2364,55 @@ export default {
} }
let url; let url;
if (p.type =='competency') { if (p.type =='competency') {
url = `/admin/tool/lp/competencies.php?competencyid=${p.id}`; url = `/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${p.id}`;
} else { } else {
url = `/admin/tool/lp/competencies.php?competencyframeworkid=${p.id}&pagecontextid=${p.contextid}`; url = this.competencyurl(p);
} }
s += `<a href="${url}">${p.title}</a>`; s += `<a href="${url}" target="_blank">${p.title}</a>`;
} }
return s; return s;
}, },
requiredChanged(newValue,c){ competencyurl(c) {
call([{ return `/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${c.id}`;
methodname: 'local_treestudyplan_require_competency', }
args: { 'competency_id': c.id,
'item_id': this.item.id,
'required': newValue,
}
}])[0].fail(notification.exception);
},
}, },
template: ` template: `
<table class="t-item-course-competency-list"> <table class="r-item-course-competency-list">
<tr v-if="value.competencies.length == 0"> <tr v-if="value.competencies.length == 0">
<td colspan='2'>{{text.competencies_not_configured}}! <td colspan='2'>{{text.competencies_not_configured}}!
<br><a :href="'/admin/tool/lp/coursecompetencies.php?courseid='+item.course.id" target='_blank'>{{text.configure_competencies}}</a> <br><a :href="'/admin/tool/lp/coursecompetencies.php?courseid='+item.course.id" target='_blank'>{{text.configure_competencies}}</a>
</td> </td>
</tr> </tr>
<template v-else> <template v-else>
<tr class='t-item-course-competency-headers'>
<th>{{text.heading}}</th>
<th></th>
<th>{{text.required}}</th>
</tr>
<tr v-for='c in value.competencies'> <tr v-for='c in value.competencies'>
<td :colspan="(c.details)?1:2"><a href='#' v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.title'></span></a></td>
<td class='details' v-if="c.details">
<a href='#' v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.details'></span></a>
</td>
<td> <td>
<b-form-checkbox inline <a href="#" v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.title'></span></a>
@change="requiredChanged($event,c)" </td>
v-model="c.required" <td class='details'>
>{{ text.required }}</b-form-checkbox> <a v-if="c.details" href="#" v-b-modal="'modal-competency-id-'+c.id"><span v-html='c.details'></span></a>
<abbr v-if="c.required" :title="text.required"
:class="'s-required ' + + completion_tag(c)"
><i class='fa fa-asterisk' ></i
></abbr>
</td>
<td><r-completion-bar v-model="c.completionstats" :width="150" :height="15"></td>
<td v-if="c.feedback">
<a v-b-modal="'r-competency-feedback-'+c.id"
href="#"
>{{ text["view_feedback"]}}</a>
<b-modal
:id="'r-competency-feedback-'+c.id"
size="sm"
ok-only
centered
scrollable
>
<template #modal-header>
<h2><i class="fa fa-puzzle-piece"></i>{{ c.title }}</h2><br>
</template>
<span v-html="c.feedback"></span>
</b-modal>
</td> </td>
<b-modal :id="'modal-competency-id-'+c.id" <b-modal :id="'modal-competency-id-'+c.id"
size="lg" size="lg"
@ -2404,17 +2433,41 @@ export default {
<template v-if="c.rule && c.children"> <template v-if="c.rule && c.children">
<div>{{ c.ruleoutcome }} {{ text.when}} <span v-html="c.rule.toLocaleLowerCase()"></span></div> <div>{{ c.ruleoutcome }} {{ text.when}} <span v-html="c.rule.toLocaleLowerCase()"></span></div>
<table v-if="c.children" class='t-item-course-competency-list'> <table v-if="c.children" class='r-item-course-competency-list'>
<tr class='t-item-course-competency-headers'> <tr class='t-item-course-competency-headers'>
<th>{{text.heading}}</th> <th colspan="2">{{text.heading}}</th>
<th></th> <th colspan="3">{{text.results}}</th>
<th>{{text.required}}</th>
</tr> </tr>
<tr v-for="cc in c.children"> <tr v-for="cc in c.children">
<td :colspan="(c.details)?1:2" ><span v-html='cc.title'></span></td> <td>
<td class='details' v-if="cc.details"><span v-html='cc.details'></span></td> <a :href='competencyurl(c)' target="_blank"><span v-html='cc.title'></span></a>
</td>
<td class='details'>
<a v-if="cc.details" :href='competencyurl(c)' target="_blank"><span v-html='cc.details'></span></a>
<abbr v-if="c.required" :title="text.required"
:class="'s-required ' + completion_tag(cc)"
><i class='fa fa-asterisk' ></i
></abbr>
</td>
<td><r-completion-bar v-model="cc.completionstats" :width="150" :height="15"></r-completion-bar></td>
<td><span class="text-info">{{ cc.points }} {{ text.points }}</span></td> <td><span class="text-info">{{ cc.points }} {{ text.points }}</span></td>
<td><span class="text-danger" v-if='cc.required'>{{ text.required }}</span></td> <td v-if="cc.feedback">
<a v-b-modal="'r-competency-feedback-'+cc.id"
href="#"
>{{ text["view_feedback"]}}</a>
<b-modal
:id="'r-competency-feedback-'+cc.id"
size="sm"
ok-only
centered
scrollable
>
<template #modal-header>
<h2><i class="fa fa-puzzle-piece"></i>{{ cc.title }}</h2><br>
</template>
<span v-html="cc.feedback"></span>
</b-modal>
</td>
</tr> </tr>
</table> </table>
</template> </template>
@ -2426,7 +2479,6 @@ export default {
}); });
Vue.component('r-grading-bar',{ Vue.component('r-grading-bar',{
props: { props: {
value : { value : {

View file

@ -16,6 +16,7 @@ import {objCopy,transportItem} from './studyplan-processor';
import Debugger from './util/debugger'; import Debugger from './util/debugger';
import {download,upload} from './downloader'; import {download,upload} from './downloader';
import {ProcessStudyplan, ProcessStudyplanPage} from './studyplan-processor'; import {ProcessStudyplan, ProcessStudyplanPage} from './studyplan-processor';
import {eventTypes as editSwEventTypes} from 'core/edit_switch';
import TSComponents from './treestudyplan-components'; import TSComponents from './treestudyplan-components';
import mFormComponents from "./util/mform-helper"; import mFormComponents from "./util/mform-helper";
@ -42,7 +43,6 @@ export default {
Vue.use(TSComponents); Vue.use(TSComponents);
Vue.use(mFormComponents); Vue.use(mFormComponents);
let debug = new Debugger("treestudyplan-editor"); let debug = new Debugger("treestudyplan-editor");
/************************************ /************************************
* * * *
* Treestudyplan Editor components * * Treestudyplan Editor components *
@ -58,10 +58,15 @@ export default {
return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
} }
// Create new eventbus for interaction between item components // Create new eventbus for interaction between item components
const ItemEventBus = new Vue(); const ItemEventBus = new Vue();
// Add event listener for the edit mode event so we can react to it, or at the very least ignore it
document.addEventListener(editSwEventTypes.editModeSet,(e) => {
e.preventDefault();
ItemEventBus.$emit('editModeSet', e.detail.editMode);
});
let string_keys = load_stringkeys({ let string_keys = load_stringkeys({
conditions: [ conditions: [
{ value: 'ALL', textkey: 'condition_all'}, { value: 'ALL', textkey: 'condition_all'},
@ -249,10 +254,8 @@ export default {
dateexpire: "dateexpire", dateexpire: "dateexpire",
badgeinfo: "badgeinfo", badgeinfo: "badgeinfo",
}, },
}); });
/* /*
* T-STUDYPLAN-ADVANCED * T-STUDYPLAN-ADVANCED
*/ */
@ -282,7 +285,6 @@ export default {
mounted() { mounted() {
}, },
updated() { updated() {
}, },
computed: { computed: {
scales(){ scales(){
@ -1184,7 +1186,7 @@ export default {
* T-STUDYPLAN * T-STUDYPLAN
*/ */
Vue.component('t-studyplan', { Vue.component('t-studyplan', {
props: [ 'value', 'index', ], props: [ 'value', 'index', 'initeditmode'],
data() { data() {
return { return {
config: { config: {
@ -1257,13 +1259,16 @@ export default {
}; };
}, },
created() { created() {
// Listener for the signal that a new connection was made and needs to be drawn
// Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines
ItemEventBus.$on('editModeSet', this.onMoodleEditModeChanged);
}, },
mounted() { mounted() {
if(this.value.pages[0].studylines.length == 0){ /*if(this.value.pages[0].studylines.length == 0){
// start in editmode if studylines on first page are empty // start in editmode if studylines on first page are empty
this.edit.studyline.editmode = true; this.edit.studyline.editmode = true;
} }*/
this.edit.studyline.editmode = this.initeditmode;
this.$root.$emit('redrawLines'); this.$root.$emit('redrawLines');
}, },
updated() { updated() {
@ -1276,6 +1281,9 @@ export default {
} }
}, },
methods: { methods: {
onMoodleEditModeChanged(mode){
this.edit.studyline.editmode = mode;
},
columns(page) { columns(page) {
return 1+ (page.periods * 2); return 1+ (page.periods * 2);
}, },
@ -1522,11 +1530,11 @@ export default {
selectedpageChanged(newTabIndex,prevTabIndex) { selectedpageChanged(newTabIndex,prevTabIndex) {
const page = this.value.pages[newTabIndex]; const page = this.value.pages[newTabIndex];
/*
if (page.studylines.length == 0) { if (page.studylines.length == 0) {
this.edit.studyline.editmode = true; this.edit.studyline.editmode = true;
} else {
this.edit.studyline.editmode = false;
} }
*/
} }
} }
, ,

View file

@ -186,6 +186,23 @@ abstract class aggregator {
return false; return false;
} }
/**
* 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 false;
}
/**
* Determine if aggregation method makes use of item conditions
* @return bool True if aggregation method makes use of
*/
public function use_item_conditions(){
return false;
}
/** /**
* Return the current configuration string. * Return the current configuration string.
* @return string Configuration string * @return string Configuration string
@ -201,6 +218,8 @@ abstract class aggregator {
*/ */
public static function basic_structure($value = VALUE_REQUIRED) : \external_description { public static function basic_structure($value = VALUE_REQUIRED) : \external_description {
return new \external_single_structure([ 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); ], "Aggregator requirements", $value);
} }
@ -210,6 +229,8 @@ abstract class aggregator {
*/ */
public function basic_model() { public function basic_model() {
return [ return [
"useRequiredGrades" => $this->use_required_grades(),
"useItemConditions" => $this->use_item_conditions(),
]; ];
} }

View file

@ -32,6 +32,7 @@ use core_competency\competency;
use core_competency\api as c_api; use core_competency\api as c_api;
use core_competency\competency_rule_points; use core_competency\competency_rule_points;
use core_competency\evidence; use core_competency\evidence;
use core_competency\user_competency;
use stdClass; use stdClass;
/** /**
@ -63,6 +64,29 @@ class coursecompetencyinfo {
$this->modinfo = get_fast_modinfo($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 * Generic competency info structure for individual competency stats
@ -85,8 +109,8 @@ class coursecompetencyinfo {
"coursegrade" => new \external_value(PARAM_TEXT, 'course 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), "proficient" => new \external_value(PARAM_BOOL, 'competency proficiency',VALUE_OPTIONAL),
"courseproficient" => new \external_value(PARAM_BOOL, 'course 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), "needreview" => new \external_value(PARAM_BOOL, 'waiting for review or review in progress',VALUE_OPTIONAL),
"ncourseproficient" => new \external_value(PARAM_INT, 'number of students with course proficiency',VALUE_OPTIONAL), "completionstats" => static::completionstats_structure(VALUE_OPTIONAL),
"count" => new \external_value(PARAM_INT, 'number of students in stats',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), "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), "points" => new \external_value(PARAM_INT, 'number of points in parent competency rule',VALUE_OPTIONAL),
@ -107,8 +131,8 @@ class coursecompetencyinfo {
public static function editor_structure($value = VALUE_REQUIRED) : \external_description { public static function editor_structure($value = VALUE_REQUIRED) : \external_description {
return new \external_single_structure([ return new \external_single_structure([
"competencies" => new \external_multiple_structure(self::competencyinfo_structure(), 'competencies'), "competencies" => new \external_multiple_structure(self::competencyinfo_structure(), 'competencies'),
"fproficient" => new \external_value(PARAM_FLOAT, 'fraction of completion for total course proficiency ',VALUE_OPTIONAL), "proficient" => new \external_value(PARAM_INT, 'number of proficient user/competencys ',VALUE_OPTIONAL),
"fcourseproficient" => new \external_value(PARAM_FLOAT, 'fraction of completion for total course proficienct',VALUE_OPTIONAL), "total" => new \external_value(PARAM_INT, 'total number of gradable user/competencies',VALUE_OPTIONAL),
], 'course completion info', $value); ], 'course completion info', $value);
} }
@ -195,19 +219,19 @@ class coursecompetencyinfo {
$count = 0; $count = 0;
$nproficient = 0; $nproficient = 0;
$ncourseproficient = 0;
foreach($coursecompetencies as $c) { foreach($coursecompetencies as $c) {
$ci = $this->competencyinfo_model($c); $ci = $this->competencyinfo_model($c);
if(!empty($studentslist)){ if(!empty($studentlist)){
$stats = $this->proficiency_stats($c,$studentlist); $stats = $this->proficiency_stats($c,$studentlist);
$count += $stats->count; $count += $stats->count;
$nproficient += $stats->nproficient; $nproficient += $stats->nproficient;
$ncourseproficient += $stats->ncourseproficient;
// Copy proficiency stats to model. // Copy proficiency stats to model.
foreach ((array)$stats as $key => $value) { foreach ((array)$stats as $key => $value) {
$ci[$key] = $value; $ci[$key] = $value;
} }
$ci['completionstats'] = self::completionstats($stats);
} }
$ci['required'] = $this->is_required($c); $ci['required'] = $this->is_required($c);
@ -249,6 +273,10 @@ class coursecompetencyinfo {
foreach($dids as $did) { foreach($dids as $did) {
$cc = new competency($did); $cc = new competency($did);
$cci = $this->competencyinfo_model($cc); $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($rule instanceof competency_rule_points) {
if(array_key_exists($did,$crlist)) { if(array_key_exists($did,$crlist)) {
$cr = $crlist[$did]; $cr = $crlist[$did];
@ -267,9 +295,9 @@ class coursecompetencyinfo {
$info = [ $info = [
"competencies" => $cis, "competencies" => $cis,
]; ];
if(!empty($studentslist)){ if(!empty($studentlist)){
$info["fproficient"] = (float)($nproficient)/(float)($count); $info["proficient"] = $nproficient;
$info["fcourseproficient"] = (float)($ncourseproficient)/(float)($count); $info["total"] = $count;
} }
return $info; return $info;
} }
@ -413,12 +441,16 @@ class coursecompetencyinfo {
$r->count = 0; $r->count = 0;
$r->nproficient = 0; $r->nproficient = 0;
$r->ncourseproficient = 0; $r->ncourseproficient = 0;
$r->nneedreview = 0;
$r->nfailed = 0;
foreach ($studentlist as $sid) { foreach ($studentlist as $sid) {
$p = $this->proficiency($competency,$sid); $p = $this->proficiency($competency,$sid);
$r->count += 1; $r->count += 1;
$r->nproficient += ($p->proficient)?1:0; $r->nproficient += ($p->proficient === true)?1:0;
$r->nfailed += ($p->proficient === false)?1:0;
$r->ncourseproficient += ($p->courseproficient)?1:0; $r->ncourseproficient += ($p->courseproficient)?1:0;
$r->nneedreview += ($p->needreview)?1:0;
} }
return $r; return $r;
} }
@ -438,8 +470,11 @@ class coursecompetencyinfo {
$r = new \stdClass(); $r = new \stdClass();
$uc = c_api::get_user_competency($userid, $competencyid); $uc = c_api::get_user_competency($userid, $competencyid);
$r->proficient = $uc->get('proficiency'); $proficiency = $uc->get('proficiency');
$r->proficient = $proficiency;
$r->grade = $scale->get_nearest_item($uc->get('grade')); $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 { try {
// Only add course grade and proficiency if the competency is included in the course. // 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); $ucc = c_api::get_user_competency_in_course($this->course->id,$userid,$competencyid);
@ -449,7 +484,6 @@ class coursecompetencyinfo {
return $r; return $r;
} }
/** /**
* Retrieve course proficiency and overall proficiency for a competency and user * Retrieve course proficiency and overall proficiency for a competency and user
* *

View file

@ -138,6 +138,14 @@ class bistate_aggregator extends \local_treestudyplan\aggregator {
return true; return true;
} }
/**
* Determine if Aggregation method makes use of "required grades" in a course/module.
* @return bool True if Aggregation method makes use of "required grades" in a course/module.
*/
public function use_required_grades(){
return true;
}
/** /**
* Aggregate completed/failed goals into one outcome * Aggregate completed/failed goals into one outcome
* @param int[] $completions List of completions (completion class constants) * @param int[] $completions List of completions (completion class constants)

View file

@ -82,6 +82,7 @@ if ($CFG->debugdeveloper) {
} }
$PAGE->requires->js_call_amd('local_treestudyplan/page-edit-plan', 'init', [$studyplancontext->id, $categoryid, [ $PAGE->requires->js_call_amd('local_treestudyplan/page-edit-plan', 'init', [$studyplancontext->id, $categoryid, [
"defaultAggregation" => get_config("local_treestudyplan","aggregation_mode"), "defaultAggregation" => get_config("local_treestudyplan","aggregation_mode"),
"editMode" => $PAGE->user_is_editing()
]]); ]]);
$catlist = courseservice::list_accessible_categories_with_usage("edit"); $catlist = courseservice::list_accessible_categories_with_usage("edit");
@ -150,6 +151,7 @@ print $OUTPUT->header();
v-model='activestudyplan' v-model='activestudyplan'
@moved="movedStudyplan" @moved="movedStudyplan"
@toggletoolbox="toggletoolbox" @toggletoolbox="toggletoolbox"
:initeditmode="initialEditMode"
></t-studyplan> ></t-studyplan>
<div v-else-if='loadingstudyplan' class="spinner-border text-primary" role="status"> <div v-else-if='loadingstudyplan' class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span> <span class="sr-only">Loading...</span>

View file

@ -1,77 +0,0 @@
<?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/>.
/**
* Entry point for other moodle modules to embed the user's report in a view
* @package local_treestudyplan
* @copyright 2023 P.M. Kuipers
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once("../../config.php");
require_once($CFG->libdir.'/weblib.php');
use local_treestudyplan;
$systemcontext = context_system::instance();
$PAGE->set_url("/local/treestudyplan/myreport.php", array());
require_login();
$PAGE->set_pagelayout('embedded');
$PAGE->set_context($systemcontext);
$PAGE->set_title(get_string('report_invited', 'local_treestudyplan', "{$USER->firstname} {$USER->lastname}"));
$PAGE->set_heading(get_string('report_invited', 'local_treestudyplan', "{$USER->firstname} {$USER->lastname}"));
// Load javascripts and specific css.
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/bootstrap-vue/bootstrap-vue.css'));
if ($CFG->debugdeveloper) {
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/devstyles.css'));
}
$PAGE->requires->js_call_amd('local_treestudyplan/page-myreport', 'init');
/**
* Shortcut function to provide translations
*
* @param mixed $str Translation key
* @param null|string[] $param Parameters to pass to translation
* @param string $plugin Location to search for translation strings
* @return string Translation of key
*/
function t($str, $param = null, $plugin = 'local_treestudyplan') {
print get_string($str, $plugin, $param);
}
print $OUTPUT->header();
?>
<div class="m-buttonbar" style="margin-bottom: 1em; text-align: right;">
<a class="btn btn-primary" href="invitations.php" id="manage_invites"><i class="fa fa-share"></i> <?php t('manage_invites'); ?></a>
</div>
<div id='root'>
<div class='vue-loader' v-show='false'>
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-cloak>
<r-report v-model="studyplans"></r-report>
</div>
</div>
<?php
print $OUTPUT->footer();

View file

@ -87,7 +87,7 @@ print " <div v-cloak>";
if ($am_teaching) { if ($am_teaching) {
print " <r-report type='teaching' teachermode ></r-report>"; print " <r-report type='teaching' teachermode ></r-report>";
} else { } else {
print " <r-report type='own' ></r-report>"; print " <r-report type='own' :userid='userid'></r-report>";
} }
print " </div>"; print " </div>";

View file

@ -80,6 +80,8 @@ if ($CFG->debugdeveloper) {
} }
$PAGE->requires->js_call_amd('local_treestudyplan/page-view-plan', 'init', [$studyplancontext->id, $categoryid]); $PAGE->requires->js_call_amd('local_treestudyplan/page-view-plan', 'init', [$studyplancontext->id, $categoryid]);
// Disable edit switch.
$PAGE->theme->haseditswitch = false;
/** /**
* Shortcut function to provide translations * Shortcut function to provide translations
* *