Implemented student view. Added feedback view

This commit is contained in:
PMKuipers 2023-11-26 22:58:26 +01:00
parent 82e92eed45
commit 456e9b503e
17 changed files with 892 additions and 377 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

@ -1,3 +1,3 @@
define("local_treestudyplan/util/string-helper",["exports","core/str"],(function(_exports,_str){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.load_stringkeys=function(string_keys){var _loop2=function(idx){var stringkeys=[];for(var i in string_keys[idx]){var parts=string_keys[idx][i].textkey.split("$"),identifier=parts[0],component=parts.length>1?parts[1]:"local_treestudyplan";stringkeys.push({key:identifier,component:component})}getstr_func(stringkeys).then((function(strings){for(var _i in strings){var s=strings[_i];string_keys[idx][_i].text=s}}))};for(var idx in string_keys)_loop2(idx);return string_keys},_exports.load_strings=function(strings){var _loop=function(idx){var stringkeys=[];for(var handle in strings[idx]){var parts=strings[idx][handle].split(/[$@]/),identifier=parts[0],component=parts.length>1?parts[1]:"local_treestudyplan";stringkeys.push({key:identifier,component:component})}getstr_func(stringkeys).then((function(str){var i=0;for(var _key in strings[idx])strings[idx][_key]=str[i],i++}))};for(var idx in strings)_loop(idx);return strings},_exports.strformat=function(str,values){return str.replace(/\{(\w+)\}/g,(function(m,m1){return m1&&values.hasOwnProperty(m1)?values[m1]:m}))};var getstr_func=void 0!==_str.getStrings?_str.getStrings:_str.get_strings}));
define("local_treestudyplan/util/string-helper",["exports","core/str"],(function(_exports,_str){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.load_stringkeys=function(string_keys){for(let idx in string_keys){let stringkeys=[];for(const i in string_keys[idx]){let parts=string_keys[idx][i].textkey.split("$"),identifier=parts[0],component=parts.length>1?parts[1]:"local_treestudyplan";stringkeys.push({key:identifier,component:component})}getstr_func(stringkeys).then((function(strings){for(const i in strings){const s=strings[i];string_keys[idx][i].text=s}}))}return string_keys},_exports.load_strings=function(strings){for(let idx in strings){let stringkeys=[];for(const handle in strings[idx]){let parts=strings[idx][handle].split(/[$@]/),identifier=parts[0],component=parts.length>1?parts[1]:"local_treestudyplan";stringkeys.push({key:identifier,component:component})}getstr_func(stringkeys).then((function(str){let i=0;for(const handle in strings[idx])strings[idx][handle]=str[i],i++}))}return strings},_exports.strformat=function(str,values){return str.replace(/\{(\w+)\}/g,((m,m1)=>m1&&values.hasOwnProperty(m1)?values[m1]:m))};const getstr_func=void 0!==_str.getStrings?_str.getStrings:_str.get_strings}));
//# sourceMappingURL=string-helper.min.js.map

View File

@ -1 +1 @@
{"version":3,"file":"string-helper.min.js","sources":["../../src/util/string-helper.js"],"sourcesContent":["import {get_strings} from 'core/str';\nimport {getStrings} from 'core/str';\n\n/* Determine the proper getstrings function to use (MDL4.3+ recommends use of getStrings, which is jquery independent) */\nconst getstr_func = (getStrings !== undefined)?getStrings:get_strings;\n\n/**\n * Load the translation of strings from a strings object\n * @param {Object} strings The map of strings\n * @returns {Object} The map with strings loaded in\n */\nexport function load_strings(strings){\n for(let idx in strings){\n let stringkeys = [];\n for(const handle in strings[idx]){\n const key = strings[idx][handle];\n let parts = key.split(/[$@]/);\n let identifier = parts[0];\n let component = (parts.length > 1)?parts[1]:'local_treestudyplan';\n stringkeys.push({ key: identifier, component: component});\n }\n getstr_func(stringkeys).then(function(str){\n let i = 0;\n for(const key in strings[idx]){\n strings[idx][key] = str[i];\n i++;\n }\n });\n }\n\n return strings;\n}\n\n/**\n * Load the translation of strings from a strings object based on keys\n * Used for loading values for a drop down menu or the like\n * @param {Object} string_keys The map of stringkeys\n * @returns {Object} The map with strings loaded in\n */\nexport function load_stringkeys(string_keys){\n for(let idx in string_keys){\n // Get text strings for condition settings\n let stringkeys = [];\n for(const i in string_keys[idx]){\n const key = string_keys[idx][i].textkey;\n let parts = key.split(\"$\");\n let identifier = parts[0];\n let component = (parts.length > 1)?parts[1]:'local_treestudyplan';\n stringkeys.push({ key: identifier, component: component});\n }\n getstr_func(stringkeys).then(function(strings){\n for(const i in strings) {\n const s = strings[i];\n const l = string_keys[idx][i];\n l.text = s;\n }\n });\n }\n return string_keys;\n}\n\n/**\n * String formatting function - replaces {name} in string by value of same key in values parameter\n * @param {string} str String t\n * @param {object} values Object containing keys to replace {key} strings with\n * @returns Formatted string\n */\nexport function strformat(str,values) {\n return str.replace(/\\{(\\w+)\\}/g, (m, m1) => {\n if (m1 && values.hasOwnProperty(m1)) {\n return values[m1];\n } else {\n return m;\n }\n });\n}"],"names":["string_keys","idx","stringkeys","i","parts","textkey","split","identifier","component","length","push","key","getstr_func","then","strings","s","text","handle","str","values","replace","m","m1","hasOwnProperty","undefined","getStrings","get_strings"],"mappings":"0LAuCgCA,iCACpBC,SAEAC,WAAa,OACb,IAAMC,KAAMH,YAAYC,KAAK,KAEzBG,MADQJ,YAAYC,KAAKE,GAAGE,QAChBC,MAAM,KAClBC,WAAaH,MAAM,GACnBI,UAAaJ,MAAMK,OAAS,EAAGL,MAAM,GAAG,sBAC5CF,WAAWQ,KAAK,CAAEC,IAAKJ,WAAYC,UAAWA,YAElDI,YAAYV,YAAYW,MAAK,SAASC,aAC9B,IAAMX,MAAKW,QAAS,KACdC,EAAID,QAAQX,IACRH,YAAYC,KAAKE,IACzBa,KAAOD,WAdjB,IAAId,OAAOD,mBAAPC,YAkBDD,4CA/CkBc,4BACjBb,SACAC,WAAa,OACb,IAAMe,UAAWH,QAAQb,KAAK,KAE1BG,MADQU,QAAQb,KAAKgB,QACTX,MAAM,QAClBC,WAAaH,MAAM,GACnBI,UAAaJ,MAAMK,OAAS,EAAGL,MAAM,GAAG,sBAC5CF,WAAWQ,KAAK,CAAEC,IAAKJ,WAAYC,UAAWA,YAElDI,YAAYV,YAAYW,MAAK,SAASK,SAC9Bf,EAAI,MACJ,IAAMQ,QAAOG,QAAQb,KACrBa,QAAQb,KAAKU,MAAOO,IAAIf,GACxBA,YAbR,IAAIF,OAAOa,cAAPb,YAkBDa,qCAqCeI,IAAIC,eACnBD,IAAIE,QAAQ,cAAc,SAACC,EAAGC,WAC7BA,IAAMH,OAAOI,eAAeD,IACrBH,OAAOG,IAEPD,UApEbT,iBAA8BY,IAAfC,gBAA0BA,gBAAWC"}
{"version":3,"file":"string-helper.min.js","sources":["../../src/util/string-helper.js"],"sourcesContent":["import {get_strings} from 'core/str';\nimport {getStrings} from 'core/str';\n\n/* Determine the proper getstrings function to use (MDL4.3+ recommends use of getStrings, which is jquery independent) */\nconst getstr_func = (getStrings !== undefined)?getStrings:get_strings;\n\n/**\n * Load the translation of strings from a strings object\n * @param {Object} strings The map of strings\n * @returns {Object} The map with strings loaded in\n */\nexport function load_strings(strings){\n for(let idx in strings){\n let stringkeys = [];\n for(const handle in strings[idx]){\n const key = strings[idx][handle];\n let parts = key.split(/[$@]/);\n let identifier = parts[0];\n let component = (parts.length > 1)?parts[1]:'local_treestudyplan';\n stringkeys.push({ key: identifier, component: component});\n }\n getstr_func(stringkeys).then(function(str){\n let i = 0;\n for(const handle in strings[idx]){\n strings[idx][handle] = str[i];\n i++;\n }\n });\n }\n\n return strings;\n}\n\n/**\n * Load the translation of strings from a strings object based on keys\n * Used for loading values for a drop down menu or the like\n * @param {Object} string_keys The map of stringkeys\n * @returns {Object} The map with strings loaded in\n */\nexport function load_stringkeys(string_keys){\n for(let idx in string_keys){\n // Get text strings for condition settings\n let stringkeys = [];\n for(const i in string_keys[idx]){\n const key = string_keys[idx][i].textkey;\n let parts = key.split(\"$\");\n let identifier = parts[0];\n let component = (parts.length > 1)?parts[1]:'local_treestudyplan';\n stringkeys.push({ key: identifier, component: component});\n }\n getstr_func(stringkeys).then(function(strings){\n for(const i in strings) {\n const s = strings[i];\n const l = string_keys[idx][i];\n l.text = s;\n }\n });\n }\n return string_keys;\n}\n\n/**\n * String formatting function - replaces {name} in string by value of same key in values parameter\n * @param {string} str String t\n * @param {object} values Object containing keys to replace {key} strings with\n * @returns Formatted string\n */\nexport function strformat(str,values) {\n return str.replace(/\\{(\\w+)\\}/g, (m, m1) => {\n if (m1 && values.hasOwnProperty(m1)) {\n return values[m1];\n } else {\n return m;\n }\n });\n}"],"names":["string_keys","idx","stringkeys","i","parts","textkey","split","identifier","component","length","push","key","getstr_func","then","strings","s","text","handle","str","values","replace","m","m1","hasOwnProperty","undefined","getStrings","get_strings"],"mappings":"0LAuCgCA,iBACxB,IAAIC,OAAOD,YAAY,KAEnBE,WAAa,OACb,MAAMC,KAAMH,YAAYC,KAAK,KAEzBG,MADQJ,YAAYC,KAAKE,GAAGE,QAChBC,MAAM,KAClBC,WAAaH,MAAM,GACnBI,UAAaJ,MAAMK,OAAS,EAAGL,MAAM,GAAG,sBAC5CF,WAAWQ,KAAK,CAAEC,IAAKJ,WAAYC,UAAWA,YAElDI,YAAYV,YAAYW,MAAK,SAASC,aAC9B,MAAMX,KAAKW,QAAS,OACdC,EAAID,QAAQX,GACRH,YAAYC,KAAKE,GACzBa,KAAOD,aAIdf,4CA/CkBc,aACrB,IAAIb,OAAOa,QAAQ,KACfZ,WAAa,OACb,MAAMe,UAAWH,QAAQb,KAAK,KAE1BG,MADQU,QAAQb,KAAKgB,QACTX,MAAM,QAClBC,WAAaH,MAAM,GACnBI,UAAaJ,MAAMK,OAAS,EAAGL,MAAM,GAAG,sBAC5CF,WAAWQ,KAAK,CAAEC,IAAKJ,WAAYC,UAAWA,YAElDI,YAAYV,YAAYW,MAAK,SAASK,SAC9Bf,EAAI,MACJ,MAAMc,UAAUH,QAAQb,KACxBa,QAAQb,KAAKgB,QAAUC,IAAIf,GAC3BA,cAKLW,qCAqCeI,IAAIC,eACnBD,IAAIE,QAAQ,cAAc,CAACC,EAAGC,KAC7BA,IAAMH,OAAOI,eAAeD,IACrBH,OAAOG,IAEPD,WApEbT,iBAA8BY,IAAfC,gBAA0BA,gBAAWC"}

View File

@ -136,6 +136,19 @@ export default {
student_from_plan_enrolled: "student_from_plan_enrolled",
students_from_plan_enrolled: "students_from_plan_enrolled",
},
competency: {
competency_not_configured: "competency_not_configured",
configure_competency: "configure_competency",
when: "when",
required: "required",
points: "points@core_grades",
heading: "competency_heading",
details: "competency_details",
results: "results",
unrated: "unrated",
progress: "completion_progress",
view_feedback: "view_feedback",
},
pageinfo: {
edit: 'period_edit',
fullname: 'studyplan_name',
@ -1150,6 +1163,20 @@ export default {
' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
</template>
<template v-else-if='value.course.competency'>
<r-progress-circle v-if='["failed", "progress","incomplete"].includes(value.completion)'
:value='value.course.competency.progress'
:max='value.course.competency.count'
:min='0'
:class="'r-course-result r-completion-'+value.completion"
:icon='circle_icon(value.completion)'
:title="text['completion_'+value.completion]"
></r-progress-circle>
<i v-else v-b-popover.top
:class="'r-course-result fa fa-'+completion_icon(value.completion)+
' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
</template>
<template v-else>
<i v-b-popover.top
:class="'r-course-result fa fa-'+completion_icon(value.completion)+
@ -1222,12 +1249,17 @@ export default {
v-model='value.course.completion'
:course='value.course'
:guestmode='guestmode'></r-item-studentcompletion>
<r-item-student-course-competency
v-if='!!value.course.competency'
v-model='value.course.competency'
:item='value'
></r-item-student-course-competency>
</b-modal>
</b-card></div>
`,
});
//TAG: Selected activities dispaly
//Selected activities dispaly
Vue.component('r-item-studentgrades',{
props: {
value : {
@ -1324,7 +1356,7 @@ export default {
`,
});
//TAG: Core completion version of student course info
// Core completion version of student course info
Vue.component('r-item-studentcompletion',{
props: {
value : {
@ -1479,6 +1511,208 @@ export default {
`,
});
//TAG: STUDENT Course competency
Vue.component('r-item-student-course-competency',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
item: {
type: Object,
default: function(){ return { id: null};},
}
},
data() {
return {
text: strings.competency,
};
},
created(){
},
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(competency) {
if (competency.proficient && competency.courseproficient) {
return "check-circle";
} else if (competency.proficient) {
return "check";
} else if (competency.proficient === false) {
return "times-circle";
} else {
return "circle-o";
}
},
completion_tag(competency){
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) {
const path = competency.path;
let s = "";
for (const ix in path) {
const p = path[ix];
if ( ix > 0) {
s += " / ";
}
let url;
if (p.type =='competency') {
url = `/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${p.id}`;
} else {
url = this.competencyurl(p);
}
s += `<a href="${url}" target="_blank">${p.title}</a>`;
}
return s;
},
competencyurl(c) {
return `/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${c.id}`;
}
},
template: `
<table class="r-item-course-competency-list">
<tr v-if="value.competencies.length == 0">
<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>
</td>
</tr>
<template v-else>
<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 :colspan="(c.required)?1:2">
<span :class="'r-completion-'+completion_tag(c)">
<template v-if="!c.progress && !c.count">
<i :class="'fa fa-'+completion_icon(c)" :title="text['completion_'+completion_tag(c)]"></i>
{{ (c.proficient === null)?text.unrated:c.grade }}
</template>
<template v-else>
<r-progress-circle
:value='c.progress'
:max='c.count'
:min='0'
:class="'r-completion-'+completion_tag(c)"
:title="text['completion_'+completion_tag(c)]"
></r-progress-circle>
{{ (c.proficient === null)?((c.progress)?text.progress:text.unrated):c.grade }}
</template>
</span>
</td>
<td v-if="c.required">
<span class="text-danger" v-if="c.required">{{ text.required }}</span>
</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>
<b-modal :id="'modal-competency-id-'+c.id"
size="lg"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<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><span v-html="pathtags(c)"></span></div>
</div>
</template>
<div class="mb-2" v-if="c.description"><span v-html='c.description'></span></div>
<template v-if="c.rule && c.children">
<div>{{ c.ruleoutcome }} {{ text.when}} <span v-html="c.rule.toLocaleLowerCase()"></span></div>
<table v-if="c.children" class='r-item-course-competency-list'>
<tr class='t-item-course-competency-headers'>
<th colspan="2">{{text.heading}}</th>
<th colspan="3">{{text.results}}</th>
</tr>
<tr v-for="cc in c.children">
<td :colspan="(c.details)?1:2" >
<a :href='competencyurl(c)' target="_blank"><span v-html='cc.title'></span></a>
</td>
<td class='details' v-if="cc.details">
<a :href='competencyurl(c)' target="_blank"><span v-html='cc.details'></span></a>
</td>
<td><span :class="'r-completion-'+completion_tag(cc)"
><i :class="'fa fa-'+completion_icon(cc)" :title="text['completion_'+completion_tag(cc)]"></i>
{{ (cc.proficient === null)?text.unrated:cc.grade }}</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>
</table>
</template>
</b-modal>
</tr>
</template>
</table>
`,
});
//TAG: Teacher course
Vue.component('r-item-teachercourse', {
props: {
@ -1539,7 +1773,7 @@ export default {
return completable;
},
progress_circle() { //INFO:
progress_circle() {
const status = {
students: 0,
completed: 0,
@ -1719,6 +1953,11 @@ export default {
v-model='value.course.completion'
:course='value.course'
></r-item-teachercompletion>
<r-item-teacher-course-competency
v-if='!!value.course.competency'
v-model='value.course.competency'
:item='value'
></r-item-teacher-course-competency>
</b-modal>
</b-card>
@ -1727,7 +1966,7 @@ export default {
});
//TAG: Select activities to use in grade overview
//Select activities to use in grade overview
Vue.component('r-item-teacher-gradepicker', {
props: {
value : {
@ -1825,7 +2064,7 @@ export default {
//TAG: Selected activities dispaly
//Selected activities dispaly
Vue.component('r-item-teachergrades',{
props: {
value : {
@ -1938,7 +2177,7 @@ export default {
`,
});
//TAG: Core completion version of student course info
// Core completion version of student course info
Vue.component('r-item-teachercompletion',{
props: {
value : {
@ -2037,6 +2276,157 @@ export default {
});
//TAG: Course competency
Vue.component('r-item-teacher-course-competency',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
item: {
type: Object,
default: function(){ return { id: null};},
}
},
data() {
return {
text: strings.competency,
};
},
created(){
},
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) {
const path = competency.path;
let s = "";
for (const ix in path) {
const p = path[ix];
if ( ix > 0) {
s += " / ";
}
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;
},
requiredChanged(newValue,c){
call([{
methodname: 'local_treestudyplan_require_competency',
args: { 'competency_id': c.id,
'item_id': this.item.id,
'required': newValue,
}
}])[0].fail(notification.exception);
},
},
template: `
<table class="t-item-course-competency-list">
<tr v-if="value.competencies.length == 0">
<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>
</td>
</tr>
<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'>
<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>
<b-form-checkbox inline
@change="requiredChanged($event,c)"
v-model="c.required"
>{{ text.required }}</b-form-checkbox>
</td>
<b-modal :id="'modal-competency-id-'+c.id"
size="lg"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<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><span v-html="pathtags(c)"></span></div>
</div>
</template>
<div class="mb-2" v-if="c.description"><span v-html='c.description'></span></div>
<template v-if="c.rule && c.children">
<div>{{ c.ruleoutcome }} {{ text.when}} <span v-html="c.rule.toLocaleLowerCase()"></span></div>
<table v-if="c.children" class='t-item-course-competency-list'>
<tr class='t-item-course-competency-headers'>
<th>{{text.heading}}</th>
<th></th>
<th>{{text.required}}</th>
</tr>
<tr v-for="cc in c.children">
<td :colspan="(c.details)?1:2" ><span v-html='cc.title'></span></td>
<td class='details' v-if="cc.details"><span v-html='cc.details'></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>
</tr>
</table>
</template>
</b-modal>
</tr>
</template>
</table>
`,
});
Vue.component('r-grading-bar',{
props: {
value : {

View File

@ -237,6 +237,11 @@ export default {
competency: {
competency_not_configured: "competency_not_configured",
configure_competency: "configure_competency",
when: "when",
required: "required",
points: "points@core_grades",
heading: "competency_heading",
details: "competency_details",
},
badge: {
share_badge: "share_badge",
@ -3191,7 +3196,7 @@ export default {
<t-item-course-competency
v-if='!!value.course.competency'
v-model='value.course.competency'
:course='value.course'
:item='value'
></t-item-course-competency>
<template #modal-footer="{ ok, cancel, hide }" >
@ -3410,10 +3415,10 @@ export default {
type: Boolean,
default: false,
},
course: {
item: {
type: Object,
default: function(){ return {};},
},
default: function(){ return { id: null};},
}
},
data() {
return {
@ -3421,19 +3426,7 @@ export default {
};
},
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() {
@ -3448,25 +3441,6 @@ export default {
},
},
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) {
const path = competency.path;
let s = "";
@ -3486,20 +3460,39 @@ export default {
}
return s;
},
requiredChanged(newValue,c){
call([{
methodname: 'local_treestudyplan_require_competency',
args: { 'competency_id': c.id,
'item_id': this.item.id,
'required': newValue,
}
}])[0].fail(notification.exception);
},
},
template: `
<table class="r-item-course-grade-details">
<table class="t-item-course-competency-list">
<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>
<br><a :href="'/admin/tool/lp/coursecompetencies.php?courseid='+item.course.id" target='_blank'>{{text.configure_competencies}}</a>
</td>
</tr>
<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'>
<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 :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>
<b-form-checkbox inline
@change="requiredChanged($event,c)"
v-model="c.required"
>{{ text.required }}</b-form-checkbox>
</td>
<b-modal :id="'modal-competency-id-'+c.id"
size="lg"
@ -3516,14 +3509,24 @@ export default {
<div><span v-html="pathtags(c)"></span></div>
</div>
</template>
<span v-html='c.description'></span>
<table v-if="c.children">
<div class="mb-2" v-if="c.description"><span v-html='c.description'></span></div>
<template v-if="c.rule && c.children">
<div>{{ c.ruleoutcome }} {{ text.when}} <span v-html="c.rule.toLocaleLowerCase()"></span></div>
<table v-if="c.children" class='t-item-course-competency-list'>
<tr class='t-item-course-competency-headers'>
<th>{{text.heading}}</th>
<th></th>
<th>{{text.required}}</th>
</tr>
<tr v-for="cc in c.children">
<td><span v-html='cc.displayfield'></span>
</td><td><span v-html='cc.description'></span>
</td>
<td :colspan="(c.details)?1:2" ><span v-html='cc.title'></span></td>
<td class='details' v-if="cc.details"><span v-html='cc.details'></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>
</tr>
</table>
</template>
</b-modal>
</tr>
</template>

View File

@ -21,8 +21,8 @@ export function load_strings(strings){
}
getstr_func(stringkeys).then(function(str){
let i = 0;
for(const key in strings[idx]){
strings[idx][key] = str[i];
for(const handle in strings[idx]){
strings[idx][handle] = str[i];
i++;
}
});

View File

@ -31,6 +31,7 @@ use core_competency\course_competency;
use core_competency\competency;
use core_competency\api as c_api;
use core_competency\competency_rule_points;
use core_competency\evidence;
use stdClass;
/**
@ -89,6 +90,9 @@ class coursecompetencyinfo {
"count" => new \external_value(PARAM_INT, 'number of students in stats',VALUE_OPTIONAL),
"required" => new \external_value(PARAM_BOOL, 'if required in parent competency rule',VALUE_OPTIONAL),
"points" => new \external_value(PARAM_INT, 'number of points in parent competency rule',VALUE_OPTIONAL),
"progress" => new \external_value(PARAM_INT, 'number completed child competencies/points',VALUE_OPTIONAL),
"count" => new \external_value(PARAM_INT, 'number of child competencies/points required',VALUE_OPTIONAL),
"feedback" => new \external_value(PARAM_RAW, 'feedback provided with this competency',VALUE_OPTIONAL),
];
if($recurse) {
$struct["children"] = new \external_multiple_structure(self::competencyinfo_structure(false),'child competencies',VALUE_OPTIONAL);
@ -114,8 +118,8 @@ class coursecompetencyinfo {
*/
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'),
"progress" => new \external_value(PARAM_INT, 'number completed competencies'),
"count" => new \external_value(PARAM_INT, 'number of competencies',VALUE_OPTIONAL),
], 'course completion info', $value);
}
@ -194,20 +198,19 @@ class coursecompetencyinfo {
$ncourseproficient = 0;
foreach($coursecompetencies as $c) {
$ci = $this->competencyinfo_model($c);
if(!empty($studentslist)){
$stats = $this->proficiency_stats($c,$studentlist);
$count += $stats->count;
$nproficient += $stats->nproficient;
$ncourseproficient += $stats->ncourseproficient;
}
$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);
$rule = $c->get_rule_object();
$ruleoutcome = $c->get('ruleoutcome');
if($rule && $ruleoutcome != competency::OUTCOME_NONE) {
@ -223,7 +226,7 @@ class coursecompetencyinfo {
} else {
$outcometag = "none";
}
$model["ruleoutcome"] = get_string("coursemodulecompetencyoutcome_{$outcometag}","core_competency");
$ci["ruleoutcome"] = get_string("coursemodulecompetencyoutcome_{$outcometag}","core_competency");
if ($rule instanceof competency_rule_points) {
$ruleconfig = json_decode($ruleconfig);
@ -234,9 +237,9 @@ class coursecompetencyinfo {
foreach($ruleconfig->competencies as $cr){
$crlist[$cr->id] = $cr;
}
$model["rule"] = $ruletext . " ({$points} ".get_string("points","core_grades").")";
$ci["rule"] = $ruletext . " ({$points} ".get_string("points","core_grades").")";
} else {
$model["rule"] = $ruletext;
$ci["rule"] = $ruletext;
}
// get one level of children
@ -289,10 +292,15 @@ class coursecompetencyinfo {
foreach ((array)$p as $key => $value) {
$ci[$key] = $value;
}
$ci['required'] = $this->is_required($c);
if ($p->proficient || $p->courseproficient) {
$progress += 1;
}
// Retrieve feedback.
$ci["feedback"] = $this->retrievefeedback($c,$userid);
$rule = $c->get_rule_object();
$ruleoutcome = $c->get('ruleoutcome');
if($rule && $ruleoutcome != competency::OUTCOME_NONE) {
@ -308,7 +316,7 @@ class coursecompetencyinfo {
} else {
$outcometag = "none";
}
$model["ruleoutcome"] = get_string("coursemodulecompetencyoutcome_{$outcometag}","core_competency");
$ci["ruleoutcome"] = get_string("coursemodulecompetencyoutcome_{$outcometag}","core_competency");
if ($rule instanceof competency_rule_points) {
$ruleconfig = json_decode($ruleconfig);
@ -325,16 +333,21 @@ class coursecompetencyinfo {
// get one level of children
$dids = competency::get_descendants_ids($c);
if(count($dids) > 0) {
$dcount = 0;
$dprogress = 0;
$children = [];
foreach($dids as $did) {
$cc = new competency($did);
$cci = $this->competencyinfo_model($cc);
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;
}
// Retrieve feedback.
$cci["feedback"] = $this->retrievefeedback($cc,$userid);
if($rule instanceof competency_rule_points) {
if(array_key_exists($did,$crlist)) {
$cr = $crlist[$did];
$cci["points"] = (int) $cr->points;
@ -343,17 +356,27 @@ class coursecompetencyinfo {
$points += (int) $cr->points;
}
}
} else {
$dcount += 1;
if ($cp->proficient) {
$dprogress += 1;
}
}
$children[] = $cci;
}
$ci["children"] = $children;
}
if ($rule instanceof competency_rule_points) {
$model["rule"] = $ruletext . " ({$points} / {$pointsreq} ".get_string("points","core_grades").")";
$ci["rule"] = $ruletext . " ({$points} / {$pointsreq} ".get_string("points","core_grades").")";
$ci["count"] = $pointsreq;
$ci["progress"] = $points;
} else {
$model["rule"] = $ruletext;
$ci["rule"] = $ruletext;
$ci["count"] = $dcount;
$ci["progress"] = $dprogress;
}
@ -412,16 +435,47 @@ class coursecompetencyinfo {
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();
$uc = c_api::get_user_competency($userid, $competencyid);
$r->proficient = $uc->get('proficiency');
$r->courseproficient = $ucc->get('proficiency');
$r->grade = $scale->get_nearest_item($uc->get('grade'));
try {
// Only add course grade and proficiency if the competency is included in the course.
$ucc = c_api::get_user_competency_in_course($this->course->id,$userid,$competencyid);
$r->courseproficient = $ucc->get('proficiency');
$r->coursegrade = $scale->get_nearest_item($ucc->get('grade'));
} catch (\Exception $x) {}
return $r;
}
/**
* Retrieve course proficiency and overall proficiency for a competency and user
*
* @param \core_competency\competency $competency
* @param int $userid
*
* @return stdClass
*
*/
public function retrievefeedback($competency, $userid) {
$competencyid = $competency->get('id');
$uc = c_api::get_user_competency($userid, $competencyid);
// Get evidences and sort by creation date (newest first)
$evidence = evidence::get_records_for_usercompetency($uc->get('id'),\context_system::instance(),'timecreated', "DESC");
// Get the first valid note and return it;
foreach($evidence as $e) {
if ( in_array($e->get('action'),[evidence::ACTION_OVERRIDE,evidence::ACTION_COMPLETE] )) {
return $e->get('note');
break;
}
}
return null;
}
/**
* Webservice executor to mark competency as required
* @param int $competencyid ID of the competency

View File

@ -197,15 +197,15 @@ class competency_aggregator extends \local_treestudyplan\aggregator {
$limit = $this->cfg()->thresh_completed * $count;
$coursefinished = ($course->enddate) ? ($course->enddate < time()) : false;
if ($courseproficient >= $count && $requiredmet >= $requiredcount) {
if ($proficient >= $count && $requiredmet >= $requiredcount) {
if ($limit < $count) {
return completion::EXCELLENT;
} else {
return completion::COMPLETED;
}
} else if ($courseproficient > $limit && $requiredmet >= $requiredcount) {
} else if ($proficient > $limit && $requiredmet >= $requiredcount) {
return completion::COMPLETED;
} else if ($courseproficient > 0) {
} else if ($proficient > 0) {
if ( $this->cfg()->use_failed && $coursefinished) {
return completion::FAILED;
} else {

View File

@ -901,7 +901,7 @@ class studyplanservice extends \external_api {
****************************************/
/**
* Parameter description for webservice function include_grade
* Parameter description for webservice function require_competency
*/
public static function require_competency_parameters() : \external_function_parameters {
return new \external_function_parameters( [
@ -912,7 +912,7 @@ class studyplanservice extends \external_api {
}
/**
* Return value description for webservice function include_grade
* Return value description for webservice function require_competency
*/
public static function require_competency_returns() : \external_description {
return success::structure();
@ -920,15 +920,14 @@ class studyplanservice extends \external_api {
/**
* Mark a gradable item for inclusion in the studyplan
* Mark a competency as required for course completion
* @param mixed $gradeid Id of gradable
* @param mixed $itemid Id of study item
* @param bool $include Include grade or not
* @param bool $required Mark grade as required or not
* @return array Success/Fail model
*
*/
public static function require_competency($competencyid, $itemid, $include, $required = false) {
public static function require_competency($competencyid, $itemid, $required) {
global $USER;
$item = studyitem::find_by_id($itemid);
// Find related course and course context.
@ -944,7 +943,7 @@ class studyplanservice extends \external_api {
// Check correct capabilities.
if (has_capability('local/treestudyplan:editstudyplan', $item->context()) ||
($coursecontext && is_enrolled($coursecontext, $USER, 'local/treestudyplan:selectowngradables'))) {
return coursecompetencyinfo::require_competency($competencyid, $itemid, $include, $required)->model();
return coursecompetencyinfo::require_competency($competencyid, $itemid, $required)->model();
} else {
return success::fail("Access denied")->model();
}

View File

@ -1306,6 +1306,22 @@
margin-bottom: auto;
margin-left: 0.5em;
}
.path-local-treestudyplan table.t-item-course-competency-list,
.features-treestudyplan table.t-item-course-competency-list {
width: 100%;
}
.path-local-treestudyplan table.t-item-course-competency-list td,
.features-treestudyplan table.t-item-course-competency-list td {
padding-right: 1em;
}
.path-local-treestudyplan table.t-item-course-competency-list td.details,
.features-treestudyplan table.t-item-course-competency-list td.details {
width: 100%;
}
.path-local-treestudyplan table.r-item-course-competency-list td,
.features-treestudyplan table.r-item-course-competency-list td {
padding-right: 1em;
}
.path-local-treestudyplan .card.s-studyplan-card,
.features-treestudyplan .card.s-studyplan-card {

View File

@ -177,6 +177,9 @@ $string["condition_any"] = 'One or more entries need to be completed';
$string["courses"] = 'Courses';
$string["select_grades"] = 'Grades included in report';
$string["configure_competency"] = "Configure course competencies";
$string["competency_not_configured"] = "Course competencies have not yet been configured.";
$string["configure_completion"] = "Configure course completion";
$string["completion_not_configured"] = "Course completion has not yet been configured.";
$string["completion_failed"] = "Failed";
@ -242,6 +245,7 @@ $string["allgraded"] = 'All graded';
$string["unsubmitted"] = 'No submission';
$string["nogrades"] = 'No grades';
$string["unknown"] = 'Grading status unknown';
$string["when"] = "when";
$string["selectstudent_btn"] = "View student plans";
$string["selectstudent"] = "Choose student";
@ -299,9 +303,14 @@ $string["settingdesc_competency_thresh_completed"] = 'Minimum percentage of prof
$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";
$string["grade_include"] = 'Include';
$string["grade_require"] = 'Require';
$string["required_goal"] = 'Required outcome';
$string["required"] = 'Required';
$string["competency_heading"] = 'Competency';
$string["competency_details"] = 'Details';
$string["results"] = 'Results';
$string["unrated"] = 'Unrated';
$string["advanced_tools"] = 'Advanced';
$string["confirm_cancel"] = 'Cancel';

View File

@ -174,6 +174,9 @@ $string["condition_any"] = 'Minimaal één onderdeel moet afgerond zijn';
$string["courses"] = 'Cursussen';
$string["select_grades"] = 'Resultaten die meetellen';
$string["configure_competency"] = "Cursuscompetenties instellen";
$string["competency_not_configured"] = "De cursuscompetenties zijn nog niet ingesteld.";
$string["configure_completion"] = "Voltooiing instellen";
$string["completion_not_configured"] = "De cursusvoltooiing is nog niet ingesteld.";
$string["completion_failed"] = "Onvoldoende";
@ -239,6 +242,7 @@ $string["allgraded"] = 'Alles beoordeeld';
$string["unsubmitted"] = 'Geen inzendingen';
$string["nogrades"] = 'Geen items';
$string["unknown"] = 'Beoordelingen onbekend';
$string["when"] = "zodra";
$string["selectstudent_btn"] = "Bekijk studentenvoortgang";
$string["selectstudent"] = "Kies een student";
@ -298,9 +302,14 @@ $string["settingdesc_competency_thresh_completed"] = 'Minimumpercentage behaalde
$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";
$string["grade_include"] = 'Doel';
$string["grade_require"] = 'Verplicht';
$string["required_goal"] = 'Verplicht leerdoel';
$string["required"] = 'Verplicht';
$string["competency_heading"] = 'Competentie';
$string["competency_details"] = 'Details';
$string["results"] = 'Resultaten';
$string["unrated"] = 'Niet beoordeeld';
$string["advanced_tools"] = 'Geavanceerd';
$string["confirm_cancel"] = 'Annuleren';

View File

@ -1118,4 +1118,23 @@
margin-bottom: auto;
margin-left: 0.5em;
}
table.t-item-course-competency-list {
width: 100%;
td {
padding-right: 1em;
}
td.details {
width: 100%;
}
}
table.r-item-course-competency-list {
td {
padding-right: 1em;
}
}
}

View File

@ -1306,6 +1306,22 @@
margin-bottom: auto;
margin-left: 0.5em;
}
.path-local-treestudyplan table.t-item-course-competency-list,
.features-treestudyplan table.t-item-course-competency-list {
width: 100%;
}
.path-local-treestudyplan table.t-item-course-competency-list td,
.features-treestudyplan table.t-item-course-competency-list td {
padding-right: 1em;
}
.path-local-treestudyplan table.t-item-course-competency-list td.details,
.features-treestudyplan table.t-item-course-competency-list td.details {
width: 100%;
}
.path-local-treestudyplan table.r-item-course-competency-list td,
.features-treestudyplan table.r-item-course-competency-list td {
padding-right: 1em;
}
.path-local-treestudyplan .card.s-studyplan-card,
.features-treestudyplan .card.s-studyplan-card {