Implemented group completion stats for all situations

This commit is contained in:
PMKuipers 2023-06-16 23:12:17 +02:00
parent 73f4646121
commit e698377dca
17 changed files with 1014 additions and 270 deletions

View File

@ -11,6 +11,7 @@ import {call} from 'core/ajax';
import notification from 'core/notification'; import notification from 'core/notification';
import {svgarcpath} from './svgarc'; import {svgarcpath} from './svgarc';
//import {fixLineWrappers} from './studyplan-processor'; //import {fixLineWrappers} from './studyplan-processor';
import Debugger from './debugger';
// Make π available as a constant // Make π available as a constant
const π = Math.PI; const π = Math.PI;
@ -18,7 +19,8 @@ const π = Math.PI;
export default { export default {
install(Vue/*,options*/){ install(Vue/*,options*/){
let debug = new Debugger("treestudyplan-viewer");
debug.enable();
let strings = load_strings({ let strings = load_strings({
invalid: { invalid: {
error: 'error', error: 'error',
@ -32,15 +34,39 @@ export default {
unknown: "unknown", unknown: "unknown",
}, },
completion: { completion: {
completion_completed: "completion_completed", completed: "completion_completed",
completion_incomplete: "completion_incomplete", incomplete: "completion_incomplete",
completed_pass: "completion_passed",
completed_fail: "completion_failed",
ungraded: "ungraded",
aggregation_all: "aggregation_all",
aggregation_any: "aggregation_any",
aggregation_overall_all: "aggregation_overall_all",
aggregation_overall_any: "aggregation_overall_any",
completion_not_configured: "completion_not_configured",
configure_completion: "configure_completion",
}, },
badge: { badge: {
share_badge: "share_badge", share_badge: "share_badge",
dateissued: "dateissued", dateissued: "dateissued",
dateexpire: "dateexpire", dateexpire: "dateexpire",
badgeinfo: "badgeinfo", badgeinfo: "badgeinfo",
} badgeissuedstats: "badgeissuedstats",
},
course: {
completion_incomplete: "completion_incomplete",
completion_failed: "completion_failed",
completion_pending: "completion_pending",
completion_progress: "completion_progress",
completion_completed: "completion_completed",
completion_good: "completion_good",
completion_excellent: "completion_excellent",
view_feedback: "view_feedback",
coursetiming_past: "coursetiming_past",
coursetiming_present: "coursetiming_present",
coursetiming_future: "coursetiming_future",
required_goal: "required_goal",
},
}); });
@ -573,20 +599,7 @@ export default {
}, },
data() { data() {
return { return {
text: { text: strings.course,
completion_incomplete: "completion_incomplete",
completion_failed: "completion_failed",
completion_pending: "completion_pending",
completion_progress: "completion_progress",
completion_completed: "completion_completed",
completion_good: "completion_good",
completion_excellent: "completion_excellent",
view_feedback: "view_feedback",
coursetiming_past: "coursetiming_past",
coursetiming_present: "coursetiming_present",
coursetiming_future: "coursetiming_future",
required_goal: "required_goal",
},
}; };
}, },
computed: { computed: {
@ -673,9 +686,21 @@ export default {
</div> </div>
<div class="r-course-detail-header-right"> <div class="r-course-detail-header-right">
<div class="r-completion-detail-header"> <div class="r-completion-detail-header">
{{text['completion_'+value.completion]}} <template v-if='value.course.completion'>
<i :class="'fa fa-'+completion_icon(value.completion)+' r-completion-'+value.completion" {{text['completion_'+value.completion]}}
:title="text['completion_'+value.completion]"></i> <r-progress-circle
:value='value.course.completion.progress'
:max='value.course.completion.count'
:min='0'
:title="text['completion_'+value.completion]"
:class="'r-progress-circle-popup r-completion-'+value.completion"
></r-progress-circle>
</template>
<template v-else>
{{text['completion_'+value.completion]}}
<i :class="'fa fa-'+completion_icon(value.completion)+' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
</template>
</div> </div>
<div :class="'r-timing-'+value.course.timing"> <div :class="'r-timing-'+value.course.timing">
{{text['coursetiming_'+value.course.timing]}}<br> {{text['coursetiming_'+value.course.timing]}}<br>
@ -711,6 +736,7 @@ export default {
}, },
data() { data() {
return { return {
text: strings.course,
}; };
}, },
computed: { computed: {
@ -879,10 +905,11 @@ export default {
<tr v-for='ci in cgroup.items'> <tr v-for='ci in cgroup.items'>
<td><span v-if='guestmode'>{{ci.title}}</span> <td><span v-if='guestmode'>{{ci.title}}</span>
<span v-else v-html='ci.details.criteria'></span> <span v-else v-html='ci.details.criteria'></span>
<abbr v-if="ci.details.requirement" :title="ci.details.requirement" <a href="#" v-b-tooltip.click="{ customClass: 'r-tooltip ' + ci.status}"
:class="'s-required ' + ci.status" :title="ci.details.requirement"
><i class='fa fa-questionmark' ></i :class="'s-required ' + ci.status"><i v-if="ci.details.requirement"
></abbr> class='fa fa-question-circle'
></i></a>
<td><span :class="' r-completion-'+ci.status">{{ci.grade}}</span></td> <td><span :class="' r-completion-'+ci.status">{{ci.grade}}</span></td>
<td><i :class="'fa fa-'+completion_icon(ci.status)+' r-completion-'+ci.status" <td><i :class="'fa fa-'+completion_icon(ci.status)+' r-completion-'+ci.status"
:title="text['completion_'+ci.status]"></i> :title="text['completion_'+ci.status]"></i>
@ -913,7 +940,7 @@ export default {
`, `,
}); });
//TODO: Implement corecompletion //TAG: Teacher course
Vue.component('r-item-teachercourse', { Vue.component('r-item-teachercourse', {
props: { props: {
value :{ value :{
@ -968,6 +995,56 @@ export default {
return false; return false;
} }
}, },
isCompletable() {
let completable = false;
if(this.value.course.completion){
if(this.value.course.completion.conditions.length > 0){
completable = true;
}
} else if (this.value.course.grades){
if(this.value.course.grades.length > 0){
completable = true;
}
}
return completable;
},
progress_circle() { //INFO:
const status = {
students: 0,
completed: 0,
completed_pass: 0,
completed_fail: 0,
ungraded: 0,
};
if(this.value.course.completion){
for(const cond of this.value.course.completion.conditions){
for(const itm of cond.items){
if(itm.progress){
status.students += itm.progress.students;
status.completed += itm.progress.completed;
status.completed_pass += itm.progress.completed_pass;
status.completed_fail += itm.progress.completed_fail;
status.ungraded += itm.progress.completed;
}
}
}
} else if (this.value.course.grades){
for( const g of this.value.course.grades){
if(g.grading){
status.students += g.grading.students;
status.completed += g.grading.completed;
status.completed_pass += g.grading.completed_pass;
status.completed_fail += g.grading.completed_fail;
status.ungraded += g.grading.ungraded;
}
}
}
return status;
}
}, },
created(){ created(){
const self = this; const self = this;
@ -1054,13 +1131,12 @@ export default {
</b-col> </b-col>
<b-col md="11"> <b-col md="11">
<b-card-body class="align-items-center"> <b-card-body class="align-items-center">
<i v-b-popover.hover
:class="'r-course-graded fa fa-'+course_grading_icon+' r-graded-'+course_grading_needed"
:title="txt.grading[course_grading_needed]"></i>
<a v-b-modal="'r-item-course-details-'+value.id" <a v-b-modal="'r-item-course-details-'+value.id"
:href="(!guestmode)?('/course/view.php?id='+value.course.id):'#'" :href="(!guestmode)?('/course/view.php?id='+value.course.id):'#'"
@click.prevent.stop='' @click.prevent.stop=''
>{{ value.course.displayname }}</i></a> >{{ value.course.displayname }}</i></a>
<r-completion-circle class="r-course-graded" :disabled="!isCompletable"
v-model="progress_circle"></r-completion-circle>
</b-card-body> </b-card-body>
</b-col> </b-col>
</b-row> </b-row>
@ -1082,14 +1158,16 @@ export default {
:useRequiredGrades="useRequiredGrades" :useRequiredGrades="useRequiredGrades"
:plan="plan" :plan="plan"
></r-item-teacher-gradepicker> ></r-item-teacher-gradepicker>
<a v-if='!!value.course.completion && value.course.amteacher'
:href="'/course/completion.php?id='+value.course.id" target="_blank"
:title="text.configure_completion"><i class="fa fa-gear"></i></a>
</h1> </h1>
{{ value.course.context.path.join(" / ")}} {{ value.course.context.path.join(" / ")}}
</div> </div>
<div class="r-course-detail-header-right"> <div class="r-course-detail-header-right">
<div class="r-completion-detail-header"> <div class="r-completion-detail-header">
{{ txt.grading[course_grading_needed] }} <r-completion-circle class="r-progress-circle-popup" :disabled="!isCompletable"
<i v-b-popover.hover :class="'fa fa-'+course_grading_icon+' r-graded-'+course_grading_needed" v-model="progress_circle"></r-completion-circle>
:title="txt.grading[course_grading_needed]"></i>
</div> </div>
<div :class="'r-timing-'+value.course.timing"> <div :class="'r-timing-'+value.course.timing">
{{ text['coursetiming_'+value.course.timing] }}<br> {{ text['coursetiming_'+value.course.timing] }}<br>
@ -1114,7 +1192,7 @@ export default {
}); });
//TODO: Selecte activities to use in grade overview //TAG: Select activities to use in grade overview
Vue.component('r-item-teacher-gradepicker', { Vue.component('r-item-teacher-gradepicker', {
props: { props: {
value : { value : {
@ -1191,7 +1269,7 @@ export default {
//TODO: Selected activities dispaly //TAG: Selected activities dispaly
Vue.component('r-item-teachergrades',{ Vue.component('r-item-teachergrades',{
props: { props: {
value : { value : {
@ -1247,12 +1325,17 @@ export default {
return this.determine_grading_icon(this.is_grading_needed(grade)); return this.determine_grading_icon(this.is_grading_needed(grade));
}, },
is_grading_needed(grade){ is_grading_needed(grade){
debug.info("Grade: ", grade.name);
debug.info(grade.grading);
if(grade.grading){ if(grade.grading){
debug.info("Ping");
if(grade.grading.ungraded){ if(grade.grading.ungraded){
return 'ungraded'; return 'ungraded';
} }
else if(grade.grading.graded){ else if(grade.grading.completed_pass || grade.grading.completed || grade.grading.completed_fail) {
if(Number(grade.grading.graded) == Number(grade.grading.students)){ if(Number(grade.grading.completed) + Number(grade.grading.completed_pass)
+ Number(grade.grading.completed_fail)
== Number(grade.grading.students)){
return 'allgraded'; return 'allgraded';
} }
else { else {
@ -1312,7 +1395,7 @@ export default {
:title="txt.grading[is_grading_needed(g)]"></i> :title="txt.grading[is_grading_needed(g)]"></i>
</td> </td>
<td v-if='g.grading'> <td v-if='g.grading'>
<r-grading-bar v-model="g.grading" :width="150" :height="15"></r-grading-bar> <r-completion-bar v-model="g.grading" :width="150" :height="15"></r-completion-bar>
</td> </td>
</tr> </tr>
</table> </table>
@ -1338,20 +1421,7 @@ export default {
}, },
data() { data() {
return { return {
text: { text: strings.completion,
completion_incomplete: "completion_incomplete",
completion_failed: "completion_failed",
completion_pending: "completion_pending",
completion_progress: "completion_progress",
completion_completed: "completion_completed",
completion_good: "completion_good",
completion_excellent: "completion_excellent",
view_feedback: "view_feedback",
coursetiming_past: "coursetiming_past",
coursetiming_present: "coursetiming_present",
coursetiming_future: "coursetiming_future",
required_goal: "required_goal",
},
}; };
}, },
created(){ created(){
@ -1372,67 +1442,47 @@ export default {
computed: { computed: {
}, },
methods: { methods: {
completion_icon(completion) { hasCompletions() {
switch(completion){ if(this.value.conditions) {
case "progress": for(const cgroup of this.value.conditions){
return "exclamation-circle"; if(cgroup.items && cgroup.items.length > 0){
case "complete": return true;
return "check-circle"; }
case "complete-pass": }
return "check-circle";
case "complete-fail":
return "times-circle";
default: // case "incomplete"
return "circle-o";
} }
return false;
}, },
completion_tag(cgroup){
return cgroup.completion?'completed':'incomplete';
}
}, },
template: ` template: `
<table class="r-item-course-grade-details"> <table class="r-item-course-grade-details">
<tr v-if="hasCompletions">
<td colspan='2'><span v-if="value.aggregation == 'all'">{{ text.aggregation_overall_all}}</span
><span v-else>{{ text.aggregation_overall_any}}</span></td>
</tr>
<tr v-else>
<td colspan='2'>{{text.completion_not_configured}}!
<span v-if="course.amteacher">
<br><a :href="'/course/completion.php?id='+course.id" target='_blank'>{{text.configure_completion}}</a>
</span>
</td>
</tr>
<template v-for='cgroup in value.conditions'> <template v-for='cgroup in value.conditions'>
<tr> <tr>
<th colspan='2'>{{cgroup.title}}</th> <th colspan='2'><span v-if="cgroup.items.length > 1"
<th><r-progress-circle ><span v-if="cgroup.aggregation == 'all'">{{ text.aggregation_all}}</span
:value='cgroup.progress' ><span v-else>{{ text.aggregation_any}}</span></span>
:max='cgroup.count' {{cgroup.title}}</th>
:class="'r-completion-'+cgroup.status"
:title="text['completion_'+cgroup.status]"
></r-progress-circle></th>
</tr> </tr>
<tr v-for='ci in cgroup.items'> <tr v-for='ci in cgroup.items'>
<td><span v-if='guestmode'>{{ci.title}}</span> <td><span v-html='ci.details.criteria'></span>
<span v-else v-html='ci.details.criteria'></span> <a href="#" v-b-tooltip.click
<abbr v-if="ci.details.requirement" :title="ci.details.requirement" :title="ci.details.requirement"
:class="'s-required ' + ci.status" class='text-info'><i v-if="ci.details.requirement"
><i class='fa fa-questionmark' ></i class='fa fa-question-circle'
></abbr> ></i></a>
<td><span :class="' r-completion-'+ci.status">{{ci.grade}}</span></td>
<td><i :class="'fa fa-'+completion_icon(ci.status)+' r-completion-'+ci.status"
:title="text['completion_'+ci.status]"></i>
<i v-if='ci.pending' :title="text['completion_pending']"
class="r-pendingsubmission fa fa-clock-o"></i>
</td> </td>
<td v-if="ci.feedback"> <td>
<a v-b-modal="'r-grade-feedback-'+ci.id" <r-completion-bar v-model="ci.progress" :width="150" :height="15"></r-completion-bar>
href="#"
>{{ text["view_feedback"]}}</a>
<b-modal
:id="'r-grade-feedback-'+ci.id"
size="sm"
ok-only
centered
scrollable
>
<template #modal-header>
<h2><i class="fa fa-graduation-cap"></i>{{ course.fullname }}</h2><br>
<span class="r-activity-icon" :title="ci.typename" v-html="ci.icon"></span>{{ci.name}}
</template>
<span v-html="ci.feedback"></span>
</b-modal>
</td> </td>
</tr> </tr>
</template> </template>
@ -1516,6 +1566,245 @@ export default {
`, `,
}); });
Vue.component('r-completion-bar',{
props: {
value : {
type: Object,
default: function(){ return {
students: 0,
completed: 0,
completed_pass: 0,
completed_fail: 0,
ungraded: 0,
};},
},
width: {
type: Number,
default: 150,
},
height: {
type: Number,
default: 15,
}
},
data() {
return {
text: strings.completion,
};
},
computed: {
width_incomplete() {
return this.width * this.fraction_incomplete();
},
width_completed() {
return this.width * this.fraction_completed();
},
width_completed_pass() {
return this.width * this.fraction_completed_pass();
},
width_completed_fail() {
return this.width * this.fraction_completed_fail();
},
width_ungraded() {
return this.width * this.fraction_ungraded();
},
count_incomplete(){
return (this.value.students - this.value.completed - this.value.completed_pass
- this.value.completed_fail - this.value.ungraded);
}
},
methods: {
fraction_incomplete() {
if(this.value.students > 0){
return 1 - (
(this.value.completed + this.value.completed_pass +
this.value.completed_fail + this.value.ungraded) / this.value.students);
} else {
return 1;
}
},
fraction_completed() {
if(this.value.students > 0){
return this.value.completed / this.value.students;
} else {
return 0;
}
},
fraction_completed_pass() {
if(this.value.students > 0){
return this.value.completed_pass / this.value.students;
} else {
return 0;
}
},
fraction_completed_fail() {
if(this.value.students > 0){
return this.value.completed_fail / this.value.students;
} else {
return 0;
}
},
fraction_ungraded() {
if(this.value.students > 0){
return this.value.ungraded / this.value.students;
} else {
return 0;
}
},
},
template: `
<span class="r-grading-bar" :style="{height: height+'px'}"
><span :style="{height: height+'px', width: width_ungraded+'px'}"
class='r-grading-bar-segment r-completion-bar-ungraded'
:title="text.ungraded + ' (' + this.value.ungraded + ')'" v-b-popover.hover.top
></span
><span :style="{height: height+'px', width: width_completed+'px'}"
class='r-grading-bar-segment r-completion-bar-completed'
:title="text.completed + ' (' + this.value.completed + ')'" v-b-popover.hover.top
></span
><span :style="{height: height+'px', width: width_completed_pass+'px'}"
class='r-grading-bar-segment r-completion-bar-completed-pass'
:title="text.completed_pass + ' (' + this.value.completed_pass + ')'" v-b-popover.hover.top
></span
><span :style="{height: height+'px', width: width_completed_fail+'px'}"
class='r-grading-bar-segment r-completion-bar-completed-fail'
:title="text.completed_fail + ' (' + this.value.completed_fail + ')'" v-b-popover.hover.top
></span
><span :style="{height: height+'px', width: width_incomplete+'px'}"
class='r-grading-bar-segment r-completion-bar-incomplete'
:title="text.incomplete + ' (' + count_incomplete + ')'" v-b-popover.hover.top
></span
></span>
`,
});
Vue.component('r-completion-circle',{
props: {
value : {
type: Object,
default: function(){ return {
students: 10,
completed: 2,
completed_pass: 2,
completed_fail: 2,
ungraded: 2,
};},
},
stroke: {
type: Number,
default: 0.2,
},
disabled: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "",
}
},
computed: {
radius() {
return 50 - (50*this.stroke);
},
arcpath_ungraded() {
const begin = 0;
return this.arcpath(begin,this.fraction_ungraded());
},
arcpath_completed() {
const begin = this.fraction_ungraded();
return this.arcpath(begin,this.fraction_completed());
},
arcpath_completed_pass() {
const begin = this.fraction_ungraded()
+ this.fraction_completed();
return this.arcpath(begin,this.fraction_completed_pass());
},
arcpath_completed_fail() {
const begin = this.fraction_ungraded()
+ this.fraction_completed()
+ this.fraction_completed_pass();
return this.arcpath(begin,this.fraction_completed_fail());
},
arcpath_incomplete() {
const begin = this.fraction_ungraded()
+ this.fraction_completed()
+ this.fraction_completed_pass()
+ this.fraction_completed_fail();
return this.arcpath(begin,this.fraction_incomplete());
},
},
methods: {
arcpath(start, end) {
const r = 50 - (50*this.stroke);
const t1 = start * 2*π;
const Δ = end * 2*π;
return svgarcpath([50,50],[r,r],[t1,Δ], 1.5*π);
},
fraction_incomplete() {
if(this.value.students > 0){
return 1 - (
(this.value.completed + this.value.completed_pass +
this.value.completed_fail + this.value.ungraded) / this.value.students);
} else {
return 1;
}
},
fraction_completed() {
if(this.value.students > 0){
return this.value.completed / this.value.students;
} else {
return 0;
}
},
fraction_completed_pass() {
if(this.value.students > 0){
return this.value.completed_pass / this.value.students;
} else {
return 0;
}
},
fraction_completed_fail() {
if(this.value.students > 0){
return this.value.completed_fail / this.value.students;
} else {
return 0;
}
},
fraction_ungraded() {
if(this.value.students > 0){
return this.value.ungraded / this.value.students;
} else {
return 0;
}
},
},
template: `
<svg width="1em" height="1em" viewBox="0 0 100 100">
<title>{{title}}</title>
<circle cx="50" cy="50" :r="radius"
:style="'stroke-width: ' + (stroke*100)+'; stroke: #ccc; fill: none;'"/>
<path :d="arcpath_ungraded"
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--warning); fill: none;'"/>
<path :d="arcpath_completed"
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--info); fill: none;'"/>
<path :d="arcpath_completed_pass"
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--success); fill: none;'"/>
<path :d="arcpath_completed_fail"
:style="'stroke-width: ' + (stroke*100) +'; stroke: var(--danger); fill: none;'"/>
<circle v-if="disabled" cx="50" cy="50" :r="radius/2"
:style="'fill: var(--dark);'"/>
<circle v-else-if="value.ungraded > 0" cx="50" cy="50" :r="radius/2"
:style="'fill: var(--warning);'"/>
</g>
</svg>
`,
});
Vue.component('r-item-junction',{ Vue.component('r-item-junction',{
props: { props: {
value : { value : {
@ -1666,16 +1955,46 @@ export default {
return "check"; return "check";
} }
}, },
issuestats(){
// so the r-completion-bar can be used to show issuing stats
return {
students: (this.value.badge.studentcount)?this.value.badge.studentcount:0,
completed: (this.value.badge.issuedcount)?this.value.badge.issuedcount:0,
completed_pass: 0,
completed_fail: 0,
ungraded: 0,
};
},
arcpath_issued(){
if(this.value.badge.studentcount){
const fraction = this.value.badge.issuedcount/this.value.badge.studentcount;
return this.arcpath(0,fraction);
} else {
return ""; // no path
}
}
}, },
methods: { methods: {
arcpath(start, end) {
const r = 44;
const t1 = start * 2*π;
const Δ = end * 2*π;
return svgarcpath([50,50],[r,r],[t1,Δ], 1.5*π);
},
}, },
template: ` template: `
<div :class="'r-item-badge r-item-filter r-completion-'+completion" v-b-tooltip.hover :title="value.badge.name"> <div :class="'r-item-badge r-item-filter r-completion-'+completion" v-b-tooltip.hover :title="value.badge.name">
<a v-b-modal="'r-item-badge-details-'+value.id" <a v-b-modal="'r-item-badge-details-'+value.id"
><svg class="r-badge-backdrop " width='50px' height='50px' viewBox="0 0 100 100"> ><svg class="r-badge-backdrop " width='50px' height='50px' viewBox="0 0 100 100">
<title>{{value.badge.name}}</title> <title>{{value.badge.name}}</title>
<circle v-if="teachermode" cx="50" cy="50" r="46" <template v-if="teachermode">
style="stroke: #999; stroke-width: 6; fill: #ddd; fill-opacity: 0.8;"/> <circle cx="50" cy="50" r="44"
style="stroke: #ccc; stroke-width: 8; fill: #ddd; fill-opacity: 0.8;"/>
<path :d="arcpath_issued"
:style="'stroke-width: 8; stroke: var(--info); fill: none;'"/>
</template>
<circle v-else-if="value.badge.issued" cx="50" cy="50" r="46" <circle v-else-if="value.badge.issued" cx="50" cy="50" r="46"
style="stroke: currentcolor; stroke-width: 4; fill: currentcolor; fill-opacity: 0.5;"/> style="stroke: currentcolor; stroke-width: 4; fill: currentcolor; fill-opacity: 0.5;"/>
<circle v-else cx="50" cy="50" r="46" <circle v-else cx="50" cy="50" r="46"
@ -1701,10 +2020,10 @@ export default {
>{{ value.badge.name }}</a >{{ value.badge.name }}</a
></h1> ></h1>
</div> </div>
<div class="r-course-detail-header-right"> <div class="r-course-detail-header-right" v-if="!teachermode">
<div class="r-completion-detail-header"> <div class="r-completion-detail-header">
{{ txt.completion['completion_'+completion] }} {{ txt.completion['completion_'+completion] }}
<i v-b-popover.hover :class="'fa fa-'+issued_icon+' r-completion-'+completion" <i v-b-popover.hover :class="'fa fa-'+issued_icon+' r-completion-'+completion"
:title="txt.completion['completion_'+completion]"></i> :title="txt.completion['completion_'+completion]"></i>
</div> </div>
</div> </div>
@ -1727,7 +2046,12 @@ export default {
v-if="value.badge.criteria"><li v-for="crit in value.badge.criteria" v-if="value.badge.criteria"><li v-for="crit in value.badge.criteria"
><span v-html='crit'></span></li></ul> ><span v-html='crit'></span></li></ul>
<p v-if="(!guestmode)"><strong><i class="fa fa-link"></i> <p v-if="(!guestmode)"><strong><i class="fa fa-link"></i>
<a :href="value.badge.infolink">{{ txt.badge.badgeinfo }}</a></strong></p> <a :href="value.badge.infolink" target="_blank"
>{{ txt.badge.badgeinfo }}</a></strong></p>
<p v-if="teachermode && !guestmode"
>{{txt.badge.badgeissuedstats}}:<br>
<r-completion-bar v-model="issuestats" :width="150" :height="15"></r-completion-bar>
</p>
</b-col></b-row> </b-col></b-row>
</b-container> </b-container>
</b-modal> </b-modal>

View File

@ -172,13 +172,20 @@ export default {
completion: { completion: {
completion_completed: "completion_completed", completion_completed: "completion_completed",
completion_incomplete: "completion_incomplete", completion_incomplete: "completion_incomplete",
aggregation_all: "aggregation_all",
aggregation_any: "aggregation_any",
aggregation_overall_all: "aggregation_overall_all",
aggregation_overall_any: "aggregation_overall_any",
completion_not_configured: "completion_not_configured",
configure_completion: "configure_completion",
}, },
badge: { badge: {
share_badge: "share_badge", share_badge: "share_badge",
dateissued: "dateissued", dateissued: "dateissued",
dateexpire: "dateexpire", dateexpire: "dateexpire",
badgeinfo: "badgeinfo", badgeinfo: "badgeinfo",
} },
}); });
@ -2152,14 +2159,6 @@ export default {
}; };
}, },
computed: { computed: {
useRequiredGrades() {
if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useRequiredGrades !== undefined){
return this.plan.aggregation_info.useRequiredGrades;
}
else {
return false;
}
},
useItemConditions() { useItemConditions() {
if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useItemConditions !== undefined){ if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useItemConditions !== undefined){
return this.plan.aggregation_info.useItemConditions; return this.plan.aggregation_info.useItemConditions;
@ -2168,18 +2167,45 @@ export default {
return false; return false;
} }
}, },
selectedgrades(){
let list = []; configurationState(){
for(let ix in this.value.course.grades){ if(this.hasGrades() || this.hasCompletions()) {
let g = this.value.course.grades[ix]; return "t-configured-ok";
if(g.selected){ } else {
list.push(g); return "t-configured-alert";
}
} }
return list; },
},
configurationIcon(){
if(this.hasGrades() || this.hasCompletions()) {
return "check";
} else {
return "exclamation-circle";
}
}
}, },
methods: { methods: {
hasGrades() {
if(this.value.course.grades && this.value.course.grades > 0){
for(const g of this.value.course.grades){
if(g.selected){
return true;
}
}
}
return false;
},
hasCompletions() {
if(this.value.course.completion && this.value.course.completion.conditions) {
for(const cgroup of this.value.course.completion.conditions){
if(cgroup.items && cgroup.items.length > 0){
return true;
}
}
}
return false;
},
includeChanged(newValue,g){ includeChanged(newValue,g){
call([{ call([{
methodname: 'local_treestudyplan_include_grade', methodname: 'local_treestudyplan_include_grade',
@ -2225,7 +2251,8 @@ export default {
<b-card-body class="align-items-center"> <b-card-body class="align-items-center">
<a class="t-item-course-config" <a class="t-item-course-config"
v-b-modal="'t-item-course-config-'+value.id" v-b-modal="'t-item-course-config-'+value.id"
href="#" @click.prevent=""><i class="fa fa-gear"></i></a> href="#" @click.prevent=""
><i :class="'fa fa-'+configurationIcon+' ' + configurationState"></i></a>
<a v-b-modal="'t-item-course-config-'+value.id" <a v-b-modal="'t-item-course-config-'+value.id"
:id="'t-item-course-details-'+value.id" :id="'t-item-course-details-'+value.id"
:href="'/course/view.php?id='+value.course.id" :href="'/course/view.php?id='+value.course.id"
@ -2237,12 +2264,17 @@ export default {
:id="'t-item-course-config-'+value.id" :id="'t-item-course-config-'+value.id"
:title="value.course.displayname + ' - ' + value.course.fullname" :title="value.course.displayname + ' - ' + value.course.fullname"
ok-only ok-only
size="lg"
scrollable scrollable
> >
<template #modal-header> <template #modal-header>
<div> <div>
<h1><a :href="'/course/view.php?id='+value.course.id" target="_blank" <h1><a :href="'/course/view.php?id='+value.course.id" target="_blank"
><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a></h1> ><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a>
<a v-if='!!value.course.completion'
:href="'/course/completion.php?id='+value.course.id" target="_blank"
:title="text.configure_completion"><i class="fa fa-gear"></i></a>
</h1>
{{ value.course.context.path.join(" / ")}} / {{value.course.shortname}} {{ value.course.context.path.join(" / ")}} / {{value.course.shortname}}
</div> </div>
<div class="r-course-detail-header-right"> <div class="r-course-detail-header-right">
@ -2252,45 +2284,221 @@ export default {
</div> </div>
</div> </div>
</template> </template>
<b-form-group v-if="useItemConditions" <t-item-course-grades
:label="text.select_conditions" v-if='!!value.course.grades && value.course.grades.length > 0'
><b-form-select size="sm" v-model='value' :plan="plan"
@input="updateConditions" ></t-item-course-grades>
v-model="value.conditions" <t-item-course-completion
:options="condition_options" v-if='!!value.course.completion'
></b-form-select> v-model='value.course.completion'
</b-form-group> :course='value.course'
<b-form-group ></t-item-course-completion>
:label="text.select_grades"
><ul class="t-item-module-children">
<li class="t-item-course-gradeinfo">
<span class='t-item-course-chk-lbl'>{{text.grade_include}}</span
><span v-if="useRequiredGrades" class='t-item-course-chk-lbl'>{{text.grade_require}}</span>
</li>
<li class="t-item-course-gradeinfo" v-for="g in value.course.grades">
<b-form-checkbox inline
@change="includeChanged($event,g)" v-model="g.selected"
></b-form-checkbox>
<b-form-checkbox v-if="useRequiredGrades" inline :disabled="!g.selected"
@change="requiredChanged($event,g)" v-model="g.required"
></b-form-checkbox>
<span :title="g.typename" v-html="g.icon"></span>
<s-edit-mod
:title="value.course.fullname"
@saved="(fd) => g.name = fd.get('name')"
v-if="g.cmid > 0"
:cmid="g.cmid"
:coursectxid="value.course.ctxid"
genericonly>{{g.name}}</s-edit-mod>
</li>
</ul>
</b-form-group>
</b-modal> </b-modal>
</b-card> </b-card>
`, `,
}); });
Vue.component('t-item-course-grades', {
props: {
'value' :{
type: Object,
default(){ return null;},
},
'plan' :{
type: Object,
default(){ return null;},
},
},
data() {
return {
condition_options: string_keys.conditions,
text: strings.item_course_text,
};
},
computed: {
useRequiredGrades() {
if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useRequiredGrades !== undefined){
return this.plan.aggregation_info.useRequiredGrades;
}
else {
return false;
}
},
selectedgrades(){
let list = [];
for(let ix in this.value.course.grades){
let g = this.value.course.grades[ix];
if(g.selected){
list.push(g);
}
}
return list;
},
},
methods: {
includeChanged(newValue,g){
call([{
methodname: 'local_treestudyplan_include_grade',
args: { 'grade_id': g.id,
'item_id': this.value.id,
'include': newValue,
'required': g.required,
}
}])[0].fail(notification.exception);
},
requiredChanged(newValue,g){
call([{
methodname: 'local_treestudyplan_include_grade',
args: { 'grade_id': g.id,
'item_id': this.value.id,
'include': g.selected,
'required': newValue,
}
}])[0].fail(notification.exception);
},
},
created() {
},
template: `
<div>
<b-form-group v-if="useItemConditions"
:label="text.select_conditions"
><b-form-select size="sm"
@input="updateConditions"
v-model="value.conditions"
:options="condition_options"
></b-form-select>
</b-form-group>
<b-form-group
:label="text.select_grades"
><ul class="t-item-module-children">
<li class="t-item-course-gradeinfo">
<span class='t-item-course-chk-lbl'>{{text.grade_include}}</span
><span v-if="useRequiredGrades" class='t-item-course-chk-lbl'>{{text.grade_require}}</span>
</li>
<li class="t-item-course-gradeinfo" v-for="g in value.course.grades">
<b-form-checkbox inline
@change="includeChanged($event,g)" v-model="g.selected"
></b-form-checkbox>
<b-form-checkbox v-if="useRequiredGrades" inline :disabled="!g.selected"
@change="requiredChanged($event,g)" v-model="g.required"
></b-form-checkbox>
<span :title="g.typename" v-html="g.icon"></span>
<s-edit-mod
:title="value.course.fullname"
@saved="(fd) => g.name = fd.get('name')"
v-if="g.cmid > 0"
:cmid="g.cmid"
:coursectxid="value.course.ctxid"
genericonly>{{g.name}}</s-edit-mod>
</li>
</ul>
</b-form-group>
</div>
`,
});
Vue.component('t-item-course-completion',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
course: {
type: Object,
default: function(){ return {};},
},
},
data() {
return {
text: strings.completion,
};
},
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';
}
},
template: `
<table class="r-item-course-grade-details">
<tr v-if="hasCompletions">
<td colspan='2'><span v-if="value.aggregation == 'all'">{{ text.aggregation_overall_all}}</span
><span v-else>{{ text.aggregation_overall_any}}</span></td>
</tr>
<tr v-else>
<td colspan='2'>{{text.completion_not_configured}}!
<br/><a :href="'/course/completion.php?id='+course.id" target='_blank'>{{text.configure_completion}}</a>
</td>
</tr>
<template v-for='cgroup in value.conditions'>
<tr>
<th colspan='2'><span v-if="cgroup.items.length > 1"
><span v-if="cgroup.aggregation == 'all'">{{ text.aggregation_all}}</span
><span v-else>{{ text.aggregation_any}}</span></span>
{{cgroup.title}}</th>
</tr>
<tr v-for='ci in cgroup.items'>
<td><span v-html='ci.details.criteria'></span>
</td>
<td v-if="ci.details.requirement" class="font-italic">
{{ci.details.requirement}}
</td>
</tr>
</template>
</table>
`,
});
/************************************ /************************************
* * * *
* Competency map Vue components * * Competency map Vue components *

View File

@ -50,10 +50,12 @@ class badgeinfo {
"criteria" => new \external_multiple_structure(new \external_value(PARAM_RAW, 'criteria text'),'badge criteria',VALUE_OPTIONAL), "criteria" => new \external_multiple_structure(new \external_value(PARAM_RAW, 'criteria text'),'badge criteria',VALUE_OPTIONAL),
"description"=> new \external_value(PARAM_TEXT, 'badge description'), "description"=> new \external_value(PARAM_TEXT, 'badge description'),
"imageurl" => new \external_value(PARAM_TEXT, 'url of badge image'), "imageurl" => new \external_value(PARAM_TEXT, 'url of badge image'),
"studentcount" => new \external_value(PARAM_INT, 'number of studyplan students that can get this badge',VALUE_OPTIONAL),
"issuedcount" => new \external_value(PARAM_INT, 'number of studyplan students that have got this badge',VALUE_OPTIONAL),
],"Badge info",$value); ],"Badge info",$value);
} }
public function editor_model() public function editor_model(array $studentlist=null)
{ {
$context = ($this->badge->type == BADGE_TYPE_SITE) ? \context_system::instance() : \context_course::instance($this->badge->courseid); $context = ($this->badge->type == BADGE_TYPE_SITE) ? \context_system::instance() : \context_course::instance($this->badge->courseid);
// If the user is viewing another user's badge and doesn't have the right capability return only part of the data. // If the user is viewing another user's badge and doesn't have the right capability return only part of the data.
@ -72,6 +74,13 @@ class badgeinfo {
'description' => $this->badge->description, 'description' => $this->badge->description,
'imageurl' => \moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $this->badge->id, '/','f1')->out(false), 'imageurl' => \moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $this->badge->id, '/','f1')->out(false),
]; ];
// Add badge issue stats if a studentlist is attached to the request
if(!empty($studentlist) && is_array($studentlist)){
$model['studentcount'] = count($studentlist);
$model['issuedcount'] = $this->count_issued($studentlist);
}
return $model; return $model;
} }
@ -130,7 +139,7 @@ class badgeinfo {
function count_issued(array $student_ids){ function count_issued(array $student_ids){
$issuecount = 0; $issuecount = 0;
foreach($student_ids as $uid){ foreach($student_ids as $userid){
if($this->badge->is_issued($userid)){ if($this->badge->is_issued($userid)){
$issuecount++; $issuecount++;
} }

View File

@ -5,9 +5,6 @@ require_once($CFG->libdir.'/externallib.php');
use \grade_item; use \grade_item;
// $gi->courseid,
// $gi->itemmodule,
// $gi->iteminstance
class completionscanner class completionscanner
{ {
private static $mod_supported = []; private static $mod_supported = [];
@ -39,17 +36,19 @@ class completionscanner
return self::$course_students[$courseid]; return self::$course_students[$courseid];
} }
public function __construct(\completion_criteria $crit){ public function __construct(\completion_criteria $crit,$course){
$this->course = $crit->course; $this->courseid = $course->id;
$this->course = $course;
$this->modinfo = get_fast_modinfo($course);
$this->crit = $crit; $this->crit = $crit;
$this->completioninfo = new \completion_info($course);
// Find a related scanner if the type is an activity type // Find a related scanner if the type is an activity type
if($crit->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY){ if($crit->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY){
// First find the course module // First find the course module
$modinfo = get_fast_modinfo($course); $this->cm = $this->modinfo->get_cm($crit->moduleinstance);
$this->cm = $modinfo->get_cm($crit->moduleinstance); $gi = grade_item::fetch(['itemtype' => 'mod', 'itemmodule' => $this->cm->modname, 'iteminstance' => $this->cm->instance, 'courseid' => $this->courseid]);
$gi = grade_item::fetch(['itemtype' => 'mod', 'itemmodule' => $cm->mod, 'iteminstance' => $cm->id, 'courseid' => $course->id]);
if($gi !== false) if($gi !== false)
{ {
// Grade none items should not be relevant // Grade none items should not be relevant
@ -57,37 +56,16 @@ class completionscanner
if(($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE)) if(($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE))
{ {
// If it's a relevant grade type, initialize a scanner if possible // If it's a relevant grade type, initialize a scanner if possible
$this->gi = gi; $this->gi = $gi;
if(self::supported($gi->itemmodule)) { if(self::supported($gi->itemmodule)) {
$scannerclass = "\local_treestudyplan\\local\ungradedscanners\\{$gi->itemmodule}_scanner"; $scannerclass = "\local_treestudyplan\\local\ungradedscanners\\{$gi->itemmodule}_scanner";
$this->scanner = new $scannerclass($gi); $this->scanner = new $scannerclass($gi);
} }
} }
} }
} }
} }
public function count_students(){
return count(self::get_course_students($this->gi->courseid));
}
// count students with ungraded (including completed-fail) worl
public function count_ungraded(){
if($this->scanner === null) {
return -1;
}
return $this->scanner->count_ungraded(self::get_course_students($this->gi->courseid));
}
public function count_completed(){
if($this->scanner === null) {
return -1;
}
return $this->scanner->count_graded(self::get_course_students($this->gi->courseid));
}
public function pending($userid){ public function pending($userid){
if(!array_key_exists($userid, $this->pending_cache)){ if(!array_key_exists($userid, $this->pending_cache)){
@ -112,12 +90,48 @@ class completionscanner
} }
public function model(){ public function model(){
// get completion info
$students = self::get_course_students($this->courseid);
$completed = 0;
$ungraded = 0;
$completed_pass = 0;
$completed_fail = 0;
foreach($students as $userid){
if($this->pending($userid)){
// First check if the completion needs grading
$ungraded++;
} else {
$completion = $this->completioninfo->get_user_completion($userid,$this->crit);
if($this->crit->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY){
// If it's an activity completion, add all the relevant activities as sub-items
$completion_status = $this->completioninfo->get_grade_completion($this->cm,$userid);
if($completion_status == COMPLETION_COMPLETE_PASS){
$completed_pass++;
} else if ($completion_status == COMPLETION_COMPLETE_FAIL){
$completed_fail++;
} else if ($completion_status == COMPLETION_COMPLETE){
$completed++;
}
}
else{
if($completion->is_complete()){
$completed++;
}
}
}
}
return [ return [
'ungraded' => $this->count_ungraded(), 'ungraded' => $ungraded,
'completed' => $this->count_completed(), 'completed' => $completed,
'completed_pass' => 0, 'completed_pass' => $completed_pass,
'completed_fail' => 0, 'completed_fail' => $completed_fail,
'students' => $this->count_students(), 'students' => count($students),
]; ];
} }

View File

@ -62,7 +62,7 @@ class corecompletioninfo {
"requirement" => new \external_value(PARAM_RAW, 'requirement',VALUE_OPTIONAL), "requirement" => new \external_value(PARAM_RAW, 'requirement',VALUE_OPTIONAL),
"status" => new \external_value(PARAM_RAW, 'status',VALUE_OPTIONAL), "status" => new \external_value(PARAM_RAW, 'status',VALUE_OPTIONAL),
]), ]),
//"studentcompletion" => completionscanner::structure(VALUE_OPTIONAL), "progress" => completionscanner::structure(),
], 'completion type',$value); ], 'completion type',$value);
} }
@ -240,7 +240,7 @@ class corecompletioninfo {
"type" => get_string('coursegrade', 'completion'), "type" => get_string('coursegrade', 'completion'),
"criteria" => get_string('graderequired', 'completion'), "criteria" => get_string('graderequired', 'completion'),
// TODO: convert to selected representation (letter, percentage, etc) // TODO: convert to selected representation (letter, percentage, etc)
"requirement" => format_float($criteria->gradepass, $decimalpoints), "requirement" => get_string('graderequired', 'completion').": ".format_float($criteria->gradepass, $decimalpoints),
"status" => "", "status" => "",
]; ];
} }
@ -274,13 +274,14 @@ class corecompletioninfo {
]; ];
} }
$scanner = new completionscanner($criteria,$this->course);
// only add the items list if we actually have items... // only add the items list if we actually have items...
$cinfo["items"][] = [ $cinfo["items"][] = [
"id" => $criteria->id, "id" => $criteria->id,
"title" => $criteria->get_title_detailed(), "title" => $criteria->get_title_detailed(),
"details" => $details, "details" => $details,
//"studentcompletion" => "progress" => $scanner->model(),
]; ];
} }

View File

@ -208,7 +208,7 @@ class courseinfo {
$gradables = gradeinfo::list_course_gradables($this->course,$studyitem); $gradables = gradeinfo::list_course_gradables($this->course,$studyitem);
foreach($gradables as $gradable) { foreach($gradables as $gradable) {
$info['grades'][] = $gradable->editor_model(); $info['grades'][] = $gradable->editor_model($studyitem);
} }
} }
else { else {

View File

@ -328,6 +328,7 @@ class courseservice extends \external_api
return new \external_function_parameters( [ return new \external_function_parameters( [
"criteriaid" => new \external_value(PARAM_INT, 'CriteriaID to scan progress for',VALUE_DEFAULT), "criteriaid" => new \external_value(PARAM_INT, 'CriteriaID to scan progress for',VALUE_DEFAULT),
"studyplanid" => new \external_value(PARAM_INT, 'Study plan id to check progress in',VALUE_DEFAULT), "studyplanid" => new \external_value(PARAM_INT, 'Study plan id to check progress in',VALUE_DEFAULT),
"courseid" => new \external_value(PARAM_INT, 'Course id of criteria',VALUE_DEFAULT),
]); ]);
} }
@ -336,7 +337,7 @@ class courseservice extends \external_api
return completionscanner::structure(VALUE_REQUIRED); return completionscanner::structure(VALUE_REQUIRED);
} }
public static function scan_completion_progress($criteriaid, $studyplanid) public static function scan_completion_progress($criteriaid, $studyplanid,$courseid)
{ {
global $DB; global $DB;
// Verify access to the study plan // Verify access to the study plan
@ -344,11 +345,10 @@ class courseservice extends \external_api
webservicehelper::require_capabilities(self::CAP_VIEW,$o->context()); webservicehelper::require_capabilities(self::CAP_VIEW,$o->context());
$crit = \completion_criteria::fetch(["id" => $criteriaid]); $crit = \completion_criteria::fetch(["id" => $criteriaid]);
$courseid = $crit->course;
if(!$o->course_linked($courseid)){ if(!$o->course_linked($courseid)){
throw new \webservice_access_exception("Course {$courseid} linked to criteria {$criteriaid} is not linked to studyplan {$o->id()}"); throw new \webservice_access_exception("Course {$courseid} linked to criteria {$criteriaid} is not linked to studyplan {$o->id()}");
} }
$scanner = new completionscanner($crit); $scanner = new completionscanner($crit,\get_course($courseid));
return $scanner->model(); return $scanner->model();
} }

View File

@ -181,7 +181,7 @@ class gradeinfo {
], 'referenced course information',$value); ], 'referenced course information',$value);
} }
public function editor_model(studyitem $studyitem=null) { public function editor_model(studyitem $studyitem = null) {
$model = [ $model = [
"id" => $this->id, "id" => $this->id,
"cmid" => $this->cmid, "cmid" => $this->cmid,
@ -194,7 +194,8 @@ class gradeinfo {
"gradinglink" => $this->gradinglink, "gradinglink" => $this->gradinglink,
"required" => $this->is_required(), "required" => $this->is_required(),
]; ];
if($this->is_selected() && has_capability('local/treestudyplan:viewuserreports',\context_system::instance()) // Unfortunately, lazy loading of the completion data is off, since we need the data to show study item completion...
if($studyitem !== null && $this->is_selected() && has_capability('local/treestudyplan:viewuserreports',$studyitem->getStudyline()->getStudyplan()->context())
&& $this->gradingscanner->is_available()){ && $this->gradingscanner->is_available()){
$model['grading'] = $this->gradingscanner->model(); $model['grading'] = $this->gradingscanner->model();
} }

View File

@ -38,6 +38,7 @@ class gradingscanner
} }
public function __construct(grade_item $gi){ public function __construct(grade_item $gi){
$this->courseid = $gi->courseid;
$this->gi = $gi; $this->gi = $gi;
if(self::supported($gi->itemmodule)) { if(self::supported($gi->itemmodule)) {
$scannerclass = "\local_treestudyplan\\local\ungradedscanners\\{$gi->itemmodule}_scanner"; $scannerclass = "\local_treestudyplan\\local\ungradedscanners\\{$gi->itemmodule}_scanner";
@ -49,24 +50,6 @@ class gradingscanner
return $this->scanner !== null; return $this->scanner !== null;
} }
public function count_students(){
return count(self::get_course_students($this->gi->courseid));
}
public function count_ungraded(){
if($this->scanner === null) {
return -1;
}
return $this->scanner->count_ungraded(self::get_course_students($this->gi->courseid));
}
public function count_graded(){
if($this->scanner === null) {
return -1;
}
return $this->scanner->count_graded(self::get_course_students($this->gi->courseid));
}
public function pending($userid){ public function pending($userid){
if(!array_key_exists($userid, $this->pending_cache)){ if(!array_key_exists($userid, $this->pending_cache)){
if($this->scanner === null) { if($this->scanner === null) {
@ -79,22 +62,113 @@ class gradingscanner
return $this->pending_cache[$userid]; return $this->pending_cache[$userid];
} }
public static function structure($value=VALUE_OPTIONAL){ public static function structure($value=VALUE_OPTIONAL){
return new \external_single_structure([ return new \external_single_structure([
"ungraded" => new \external_value(PARAM_INT, 'number of ungraded submissions'), "ungraded" => new \external_value(PARAM_INT, 'number of ungraded submissions'),
"graded" => new \external_value(PARAM_INT, 'number of graded students'), "completed" => new \external_value(PARAM_INT, 'number of completed students'),
"completed_pass" => new \external_value(PARAM_INT, 'number of completed-pass students'),
"completed_fail" => new \external_value(PARAM_INT, 'number of completed-fail students'),
"students" => new \external_value(PARAM_INT, 'number of students that should submit'), "students" => new \external_value(PARAM_INT, 'number of students that should submit'),
],"details about gradable submissions",$value); ],"details about gradable submissions",$value);
} }
public function model(){ public function model(){
// Upda
$students = self::get_course_students($this->courseid);
$completed = 0;
$ungraded = 0;
$completed_pass = 0;
$completed_fail = 0;
foreach($students as $userid){
if($this->pending($userid)){
// First check if the completion needs grading
$ungraded++;
} else {
$grade = $this->gi->get_final($userid);
if(!is_numeric($grade->finalgrade) && empty($grade->finalgrade)){
//skip
}
else
{
//compare grade to minimum grade
if($this->grade_passed($grade)){
$completed_pass++;
} else {
$completed_fail++;
}
}
}
}
return [ return [
'ungraded' => $this->count_ungraded(), 'ungraded' => $ungraded,
'graded' => $this->count_graded(), 'completed' => $completed,
'students' => $this->count_students(), 'completed_pass' => $completed_pass,
'completed_fail' => $completed_fail,
'students' => count($students),
]; ];
} }
// Function copied from bistate aggregator to avoid reference mazes
private function grade_passed($grade){
global $DB;
$table = "local_treestudyplan_gradecfg";
// first determine if we have a grade_config for this scale or this maximum grade
$finalgrade = $grade->finalgrade;
$scale = $this->gi->load_scale();
if( isset($scale)){
$gradecfg = $DB->get_record($table,["scale_id"=>$scale->id]);
}
else if($this->gi->grademin == 0)
{
$gradecfg = $DB->get_record($table,["grade_points"=>$this->gi->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) || $this->gi->gradepass == 0))
{
// if so, we need to know if the grade is
if($finalgrade >= $gradecfg->min_completed){
return true;
}
else {
return false;
}
}
else if($this->gi->gradepass > 0)
{
$range = floatval($this->gi->grademax - $this->gi->grademin);
// if no gradeconfig and gradepass is set, use that one to determine config.
if($finalgrade >= $this->gi->gradepass){
return true;
}
else {
return false;
}
}
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 - $this->gi->grademin);
$range = floatval($this->gi->grademax - $this->gi->grademin);
$score = $g / $range;
if($score > 0.55){
return true;
}
else {
return false;
}
}
}
} }

View File

@ -218,6 +218,7 @@ class bistate_aggregator extends \local_treestudyplan\aggregator {
} }
} }
else { else {
$grade = $gradeitem->get_final($userid);
// first determine if we have a grade_config for this scale or this maximum grade // first determine if we have a grade_config for this scale or this maximum grade
$finalgrade = $grade->finalgrade; $finalgrade = $grade->finalgrade;
$scale = $gradeinfo->getScale(); $scale = $gradeinfo->getScale();
@ -235,7 +236,7 @@ class bistate_aggregator extends \local_treestudyplan\aggregator {
// for point grades, a provided grade pass overrides the defaults in the gradeconfig // for point grades, a provided grade pass overrides the defaults in the gradeconfig
// for scales, the configuration in the gradeconfig is leading // for scales, the configuration in the gradeconfig is leading
if($gradecfg && (isset($scale) || $gradeitem->gradepass == 0)) if($gradecfg && (isset($scale) || $gradeitem->gradepass == 0))
{ {
// if so, we need to know if the grade is // if so, we need to know if the grade is
@ -291,4 +292,5 @@ class bistate_aggregator extends \local_treestudyplan\aggregator {
} }
} }
} }

View File

@ -145,6 +145,10 @@ class core_aggregator extends \local_treestudyplan\aggregator {
} }
} }
// CORE COMPLETION DOESN'T REALLY USE THE FUNCTIONS BELOW
// AGGREGATORS ARE GOING TO BE DEPRECATED ANYWAY... but used in legacy parts of this plugin.
public function grade_completion(gradeinfo $gradeinfo, $userid) { public function grade_completion(gradeinfo $gradeinfo, $userid) {
global $DB; global $DB;
$table = "local_treestudyplan_gradecfg"; $table = "local_treestudyplan_gradecfg";
@ -169,6 +173,7 @@ class core_aggregator extends \local_treestudyplan\aggregator {
} }
} }
else { else {
$grade = $gradeitem->get_final($userid);
// first determine if we have a grade_config for this scale or this maximum grade // first determine if we have a grade_config for this scale or this maximum grade
$finalgrade = $grade->finalgrade; $finalgrade = $grade->finalgrade;
$scale = $gradeinfo->getScale(); $scale = $gradeinfo->getScale();
@ -186,7 +191,7 @@ class core_aggregator extends \local_treestudyplan\aggregator {
// for point grades, a provided grade pass overrides the defaults in the gradeconfig // for point grades, a provided grade pass overrides the defaults in the gradeconfig
// for scales, the configuration in the gradeconfig is leading // for scales, the configuration in the gradeconfig is leading
if($gradecfg && (isset($scale) || $gradeitem->gradepass == 0)) if($gradecfg && (isset($scale) || $gradeitem->gradepass == 0))
{ {
// if so, we need to know if the grade is // if so, we need to know if the grade is

View File

@ -92,7 +92,6 @@ class tristate_aggregator extends \local_treestudyplan\aggregator {
public function grade_completion(gradeinfo $gradeinfo, $userid) { public function grade_completion(gradeinfo $gradeinfo, $userid) {
global $DB; global $DB;
$table = "local_treestudyplan_gradecfg";
$gradeitem = $gradeinfo->getGradeitem(); $gradeitem = $gradeinfo->getGradeitem();
$grade = $gradeitem->get_final($userid); $grade = $gradeitem->get_final($userid);
@ -153,4 +152,6 @@ class tristate_aggregator extends \local_treestudyplan\aggregator {
} }
} }
} }

View File

@ -144,8 +144,11 @@ class studyitem {
$badgeinfo = new badgeinfo($badge); $badgeinfo = new badgeinfo($badge);
if($mode == "export"){ if($mode == "export"){
$model['badge'] = $badgeinfo->name(); $model['badge'] = $badgeinfo->name();
} else { } else {
$model['badge'] = $badgeinfo->editor_model(); // Also supply a list of linked users, so the badgeinfo can give stats on
// the amount issued, related to this studyplan
$studentids = $this->getStudyline()->getStudyplan()->find_linked_userids();
$model['badge'] = $badgeinfo->editor_model($studentids);
} }
} }

View File

@ -14,6 +14,7 @@ class studyplan {
private $id; private $id;
private $aggregator; private $aggregator;
private $context = null; // Hold context object once retrieved private $context = null; // Hold context object once retrieved
private $linked_userids = null; // cache lookup of linked users (saves queries)
public function getAggregator(){ public function getAggregator(){
return $this->aggregator; return $this->aggregator;
@ -392,29 +393,32 @@ class studyplan {
* Retrieve the user id's of the users linked to this studyplan. * Retrieve the user id's of the users linked to this studyplan.
* @return array of int (User Id) * @return array of int (User Id)
*/ */
private function find_linked_userids(): array { public function find_linked_userids(): array {
global $DB; global $DB;
$uids = []; if($this->linked_userids === null){
// First get directly linked userids $uids = [];
$sql = "SELECT j.user_id FROM {local_treestudyplan_user} j // First get directly linked userids
WHERE j.studyplan_id = :planid"; $sql = "SELECT j.user_id FROM {local_treestudyplan_user} j
$ulist = $DB->get_fieldset_sql($sql, ['planid' => $this->id]); WHERE j.studyplan_id = :planid";
$ulist = $DB->get_fieldset_sql($sql, ['planid' => $this->id]);
$uids = array_merge($uids,$ulist); $uids = array_merge($uids,$ulist);
foreach($ulist as $uid){ foreach($ulist as $uid){
$users[] = $DB->get_record("user",["id"=>$uid]); $users[] = $DB->get_record("user",["id"=>$uid]);
}
// Next het users linked though cohort
$sql = "SELECT cm.userid FROM {local_treestudyplan_cohort} j
INNER JOIN {cohort_members} cm ON j.cohort_id = cm.cohortid
WHERE j.studyplan_id = :planid";
$ulist = $DB->get_fieldset_sql($sql, ['planid' => $this->id]);
$uids = array_merge($uids,$ulist);
$this->linked_userids = array_unique($uids);
} }
return $this->linked_userids;
// Next het users linked though cohort
$sql = "SELECT cm.userid FROM {local_treestudyplan_cohort} j
INNER JOIN {cohort_members} cm ON j.cohort_id = cm.cohortid
WHERE j.studyplan_id = :planid";
$ulist = $DB->get_fieldset_sql($sql, ['planid' => $this->id]);
$uids = array_merge($uids,$ulist);
return array_unique($uids);
} }
/** Check if this studyplan is linked to a particular user /** Check if this studyplan is linked to a particular user
@ -738,6 +742,23 @@ class studyplan {
return ($count > 0)?true:false; return ($count > 0)?true:false;
} }
/**
* List the course id is linked in this studyplan
* Used for cohort enrolment cascading
*/
public function get_linked_course_ids(){
global $DB;
$sql = "SELECT i.course_id
FROM {local_treestudyplan}
INNER JOIN {local_treestudyplan_line} l ON p.id = l.studyplan_id
INNER JOIN {local_treestudyplan_item} i ON l.id = i.line_id
WHERE p.id = :planid ";
$fields = $DB->get_fieldset_sql($sql,["planid" => $this->id]);
return $fields;
}
/** /**
* See if the specified course id is linked in this studyplan * See if the specified course id is linked in this studyplan
*/ */

View File

@ -724,6 +724,12 @@ tr.r-completion-category-header {
margin-top: 2px; margin-top: 2px;
} }
.r-progress-circle-popup{
position: relative;
top: -0.15em;
}
.r-completion-detail-header { .r-completion-detail-header {
font-size: 20pt; font-size: 20pt;
} }
@ -760,7 +766,7 @@ tr.r-completion-category-header {
} }
table.r-item-course-grade-details td { table.r-item-course-grade-details td {
padding-right: 10px; padding-right: 3px;
} }
.r-course-detail-header-right { .r-course-detail-header-right {
@ -833,12 +839,20 @@ table.r-item-course-grade-details td {
color: #35f; color: #35f;
} }
.r-graded-graded { .r-graded-graded {
color: #3a3; color: var(--success);
} }
.r-graded-nogrades { .r-graded-nogrades {
color: #ddd; color: #ddd;
} }
.t-configured-ok {
color: var(--success);
}
.t-configured-alert {
color: var(--warning);
}
.r-grading-bar { .r-grading-bar {
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
@ -865,17 +879,40 @@ table.r-item-course-grade-details td {
} }
.r-grading-bar-unsubmitted { .r-grading-bar-unsubmitted {
background-color: #ddd; background-color: var(--light);
} }
.r-grading-bar-graded { .r-grading-bar-graded {
background-color: #3a3; background-color: var(--success);
} }
.r-grading-bar-ungraded { .r-grading-bar-ungraded {
background-color: #a33; background-color:var(--danger);
} }
.r-completion-bar-incomplete {
background-color: var(--light);
}
.r-completion-bar-completed {
background-color: var(--info);
}
.r-completion-bar-completed-pass {
background-color: var(--success);
}
.r-completion-bar-completed-fail {
background-color: var(--danger);
}
.r-completion-bar-ungraded {
background-color: var(--warning);
}
.card.s-studyplan-card { .card.s-studyplan-card {
min-width: 300px; min-width: 300px;
max-width: 500px; max-width: 500px;
@ -918,20 +955,59 @@ table.r-item-course-grade-details td {
} }
.s-required { .s-required {
color: #a33; color: var(--danger);
} }
.s-required.completed, .s-required.complete {
color: var(--info);
}
.s-required.complete-pass,
.s-required.good, .s-required.good,
.s-required.excellent, .s-required.excellent,
.s-required.allgraded { .s-required.allgraded {
color: rgb(0, 126, 0); color: var(--success);
} }
.s-required.neutral { .s-required.neutral {
color: #aaa; color: #aaa;
} }
.r-tooltip.incomplete .tooltip-inner,
.r-tooltip.complete-fail .tooltip-inner,
.r-tooltip.completed-fail .tooltip-inner {
background-color: var(--danger);
}
.r-tooltip.incomplete .arrow::before,
.r-tooltip.complete-fail .arrow::before,
.r-tooltip.completed-fail .arrow::before {
border-top-color: var(--danger);
}
.r-tooltip.complete .tooltip-inner,
.r-tooltip.completed .tooltip-inner {
background-color: var(--info);
}
.r-tooltip.complete .arrow::before,
.r-tooltip.completed .arrow::before {
border-top-color: var(--info);
}
.r-tooltip.complete-pass .tooltip-inner,
.r-tooltip.completed-pass .tooltip-inner {
background-color: var(--success);
}
.r-tooltip.complete-pass .arrow::before,
.r-tooltip.completed-pass .arrow::before {
border-top-color: var(--success);
}
.r-tooltip.incomplete .tooltip-inner {
background-color: var(--danger);
}
.r-tooltip.incomplete .arrow::before {
border-top-color: var(--danger);
}
.m-buttonbar { .m-buttonbar {
display: flex; display: flex;
@ -939,7 +1015,6 @@ table.r-item-course-grade-details td {
justify-content: left; justify-content: left;
} }
.m-buttonbar a, .m-buttonbar a,
.m-buttonbar span, .m-buttonbar span,
.m-buttonbar i { .m-buttonbar i {

View File

@ -109,6 +109,8 @@ $string['condition_any'] = 'One or more entries need to be completed';
$string['courses'] = 'Courses'; $string['courses'] = 'Courses';
$string['select_grades'] = 'Grades included in report'; $string['select_grades'] = 'Grades included in report';
$string['configure_completion'] = "Configure course completion";
$string['completion_not_configured'] = "Course completion has not yet been configured.";
$string['completion_failed'] = "Failed"; $string['completion_failed'] = "Failed";
$string['completion_incomplete'] = "Not started"; $string['completion_incomplete'] = "Not started";
$string['completion_pending'] = "Pending review"; $string['completion_pending'] = "Pending review";
@ -116,6 +118,7 @@ $string['completion_progress'] = "In progress";
$string['completion_completed'] = "Completed"; $string['completion_completed'] = "Completed";
$string['completion_good'] = "Good"; $string['completion_good'] = "Good";
$string['completion_excellent'] = "Excellent"; $string['completion_excellent'] = "Excellent";
$string['completion_passed'] = "Passed";
$string['cfg_grades'] = 'Configure grade & scale interpretation'; $string['cfg_grades'] = 'Configure grade & scale interpretation';
$string['cfg_plans'] = 'Manage study plans'; $string['cfg_plans'] = 'Manage study plans';
@ -151,7 +154,6 @@ $string['selected'] = 'Select';
$string['name'] = 'Name'; $string['name'] = 'Name';
$string['context'] = 'Category'; $string['context'] = 'Category';
$string['error'] = "Error"; $string['error'] = "Error";
$string['ungraded'] = 'Needs grading'; $string['ungraded'] = 'Needs grading';
$string['graded'] = 'Graded'; $string['graded'] = 'Graded';
@ -248,4 +250,5 @@ $string["aggregation_any"] = "Complete one or more";
$string["share_badge"] = "Share badge"; $string["share_badge"] = "Share badge";
$string["dateissued"] = "Issued on"; $string["dateissued"] = "Issued on";
$string["dateexpire"] = "Expires on"; $string["dateexpire"] = "Expires on";
$string["badgeinfo"] = "Badge details"; $string["badgeinfo"] = "Badge details";
$string["badgeissuedstats"] = "Issuing progress";

View File

@ -111,7 +111,8 @@ $string['condition_any'] = 'Minimaal één onderdeel moet afgerond zijn';
$string['courses'] = 'Cursussen'; $string['courses'] = 'Cursussen';
$string['select_grades'] = 'Resultaten die meetellen'; $string['select_grades'] = 'Resultaten die meetellen';
$string['configure_completion'] = "Voltooiing instellen";
$string['completion_not_configured'] = "De cursusvoltooing is nog niet ingesteld.";
$string['completion_failed'] = "Onvoldoende"; $string['completion_failed'] = "Onvoldoende";
$string['completion_incomplete'] = "Niet gestart"; $string['completion_incomplete'] = "Niet gestart";
$string['completion_pending'] = "Wacht op beoordelen"; $string['completion_pending'] = "Wacht op beoordelen";
@ -119,6 +120,7 @@ $string['completion_progress'] = "In ontwikkeling";
$string['completion_completed'] = "Voltooid"; $string['completion_completed'] = "Voltooid";
$string['completion_good'] = "Goed"; $string['completion_good'] = "Goed";
$string['completion_excellent'] = "Uitstekend"; $string['completion_excellent'] = "Uitstekend";
$string['completion_passed'] = "Behaald";
$string['cfg_grades'] = 'Configureer betekenis van beoordelingen en schalen'; $string['cfg_grades'] = 'Configureer betekenis van beoordelingen en schalen';
$string['cfg_plans'] = 'Studieplannen beheren'; $string['cfg_plans'] = 'Studieplannen beheren';
@ -251,4 +253,5 @@ $string["aggregation_any"] = "Eén of meer behalen";
$string["share_badge"] = "Bewijs delen"; $string["share_badge"] = "Bewijs delen";
$string["dateissued"] = "Afgegeven op"; $string["dateissued"] = "Afgegeven op";
$string["dateexpire"] = "Veloopt op"; $string["dateexpire"] = "Veloopt op";
$string["badgeinfo"] = "Meer details"; $string["badgeinfo"] = "Meer details";
$string["badgeissuedstats"] = "Voortgang van uitgifte";