Implemented group completion stats for all situations
This commit is contained in:
parent
73f4646121
commit
e698377dca
|
@ -11,6 +11,7 @@ import {call} from 'core/ajax';
|
|||
import notification from 'core/notification';
|
||||
import {svgarcpath} from './svgarc';
|
||||
//import {fixLineWrappers} from './studyplan-processor';
|
||||
import Debugger from './debugger';
|
||||
|
||||
// Make π available as a constant
|
||||
const π = Math.PI;
|
||||
|
@ -18,7 +19,8 @@ const π = Math.PI;
|
|||
|
||||
export default {
|
||||
install(Vue/*,options*/){
|
||||
|
||||
let debug = new Debugger("treestudyplan-viewer");
|
||||
debug.enable();
|
||||
let strings = load_strings({
|
||||
invalid: {
|
||||
error: 'error',
|
||||
|
@ -32,15 +34,39 @@ export default {
|
|||
unknown: "unknown",
|
||||
},
|
||||
completion: {
|
||||
completion_completed: "completion_completed",
|
||||
completion_incomplete: "completion_incomplete",
|
||||
completed: "completion_completed",
|
||||
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: {
|
||||
share_badge: "share_badge",
|
||||
dateissued: "dateissued",
|
||||
dateexpire: "dateexpire",
|
||||
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() {
|
||||
return {
|
||||
text: {
|
||||
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",
|
||||
},
|
||||
text: strings.course,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -673,9 +686,21 @@ export default {
|
|||
</div>
|
||||
<div class="r-course-detail-header-right">
|
||||
<div class="r-completion-detail-header">
|
||||
<template v-if='value.course.completion'>
|
||||
{{text['completion_'+value.completion]}}
|
||||
<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 :class="'r-timing-'+value.course.timing">
|
||||
{{text['coursetiming_'+value.course.timing]}}<br>
|
||||
|
@ -711,6 +736,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
text: strings.course,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -879,10 +905,11 @@ export default {
|
|||
<tr v-for='ci in cgroup.items'>
|
||||
<td><span v-if='guestmode'>{{ci.title}}</span>
|
||||
<span v-else v-html='ci.details.criteria'></span>
|
||||
<abbr v-if="ci.details.requirement" :title="ci.details.requirement"
|
||||
:class="'s-required ' + ci.status"
|
||||
><i class='fa fa-questionmark' ></i
|
||||
></abbr>
|
||||
<a href="#" v-b-tooltip.click="{ customClass: 'r-tooltip ' + ci.status}"
|
||||
:title="ci.details.requirement"
|
||||
:class="'s-required ' + ci.status"><i v-if="ci.details.requirement"
|
||||
class='fa fa-question-circle'
|
||||
></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>
|
||||
|
@ -913,7 +940,7 @@ export default {
|
|||
`,
|
||||
});
|
||||
|
||||
//TODO: Implement corecompletion
|
||||
//TAG: Teacher course
|
||||
Vue.component('r-item-teachercourse', {
|
||||
props: {
|
||||
value :{
|
||||
|
@ -968,6 +995,56 @@ export default {
|
|||
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(){
|
||||
const self = this;
|
||||
|
@ -1054,13 +1131,12 @@ export default {
|
|||
</b-col>
|
||||
<b-col md="11">
|
||||
<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"
|
||||
:href="(!guestmode)?('/course/view.php?id='+value.course.id):'#'"
|
||||
@click.prevent.stop=''
|
||||
>{{ 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-col>
|
||||
</b-row>
|
||||
|
@ -1082,14 +1158,16 @@ export default {
|
|||
:useRequiredGrades="useRequiredGrades"
|
||||
:plan="plan"
|
||||
></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>
|
||||
{{ value.course.context.path.join(" / ")}}
|
||||
</div>
|
||||
<div class="r-course-detail-header-right">
|
||||
<div class="r-completion-detail-header">
|
||||
{{ txt.grading[course_grading_needed] }}
|
||||
<i v-b-popover.hover :class="'fa fa-'+course_grading_icon+' r-graded-'+course_grading_needed"
|
||||
:title="txt.grading[course_grading_needed]"></i>
|
||||
<r-completion-circle class="r-progress-circle-popup" :disabled="!isCompletable"
|
||||
v-model="progress_circle"></r-completion-circle>
|
||||
</div>
|
||||
<div :class="'r-timing-'+value.course.timing">
|
||||
{{ 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', {
|
||||
props: {
|
||||
value : {
|
||||
|
@ -1191,7 +1269,7 @@ export default {
|
|||
|
||||
|
||||
|
||||
//TODO: Selected activities dispaly
|
||||
//TAG: Selected activities dispaly
|
||||
Vue.component('r-item-teachergrades',{
|
||||
props: {
|
||||
value : {
|
||||
|
@ -1247,12 +1325,17 @@ export default {
|
|||
return this.determine_grading_icon(this.is_grading_needed(grade));
|
||||
},
|
||||
is_grading_needed(grade){
|
||||
debug.info("Grade: ", grade.name);
|
||||
debug.info(grade.grading);
|
||||
if(grade.grading){
|
||||
debug.info("Ping");
|
||||
if(grade.grading.ungraded){
|
||||
return 'ungraded';
|
||||
}
|
||||
else if(grade.grading.graded){
|
||||
if(Number(grade.grading.graded) == Number(grade.grading.students)){
|
||||
else if(grade.grading.completed_pass || grade.grading.completed || grade.grading.completed_fail) {
|
||||
if(Number(grade.grading.completed) + Number(grade.grading.completed_pass)
|
||||
+ Number(grade.grading.completed_fail)
|
||||
== Number(grade.grading.students)){
|
||||
return 'allgraded';
|
||||
}
|
||||
else {
|
||||
|
@ -1312,7 +1395,7 @@ export default {
|
|||
:title="txt.grading[is_grading_needed(g)]"></i>
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -1338,20 +1421,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
text: {
|
||||
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",
|
||||
},
|
||||
text: strings.completion,
|
||||
};
|
||||
},
|
||||
created(){
|
||||
|
@ -1372,67 +1442,47 @@ export default {
|
|||
computed: {
|
||||
},
|
||||
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";
|
||||
hasCompletions() {
|
||||
if(this.value.conditions) {
|
||||
for(const cgroup of this.value.conditions){
|
||||
if(cgroup.items && cgroup.items.length > 0){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
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}}!
|
||||
<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'>
|
||||
<tr>
|
||||
<th colspan='2'>{{cgroup.title}}</th>
|
||||
<th><r-progress-circle
|
||||
:value='cgroup.progress'
|
||||
:max='cgroup.count'
|
||||
:class="'r-completion-'+cgroup.status"
|
||||
:title="text['completion_'+cgroup.status]"
|
||||
></r-progress-circle></th>
|
||||
<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-if='guestmode'>{{ci.title}}</span>
|
||||
<span v-else v-html='ci.details.criteria'></span>
|
||||
<abbr v-if="ci.details.requirement" :title="ci.details.requirement"
|
||||
:class="'s-required ' + ci.status"
|
||||
><i class='fa fa-questionmark' ></i
|
||||
></abbr>
|
||||
<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><span v-html='ci.details.criteria'></span>
|
||||
<a href="#" v-b-tooltip.click
|
||||
:title="ci.details.requirement"
|
||||
class='text-info'><i v-if="ci.details.requirement"
|
||||
class='fa fa-question-circle'
|
||||
></i></a>
|
||||
</td>
|
||||
<td v-if="ci.feedback">
|
||||
<a v-b-modal="'r-grade-feedback-'+ci.id"
|
||||
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>
|
||||
<r-completion-bar v-model="ci.progress" :width="150" :height="15"></r-completion-bar>
|
||||
</td>
|
||||
</tr>
|
||||
</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',{
|
||||
props: {
|
||||
value : {
|
||||
|
@ -1666,16 +1955,46 @@ export default {
|
|||
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: {
|
||||
arcpath(start, end) {
|
||||
const r = 44;
|
||||
|
||||
const t1 = start * 2*π;
|
||||
const Δ = end * 2*π;
|
||||
return svgarcpath([50,50],[r,r],[t1,Δ], 1.5*π);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<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"
|
||||
><svg class="r-badge-backdrop " width='50px' height='50px' viewBox="0 0 100 100">
|
||||
<title>{{value.badge.name}}</title>
|
||||
<circle v-if="teachermode" cx="50" cy="50" r="46"
|
||||
style="stroke: #999; stroke-width: 6; fill: #ddd; fill-opacity: 0.8;"/>
|
||||
<template v-if="teachermode">
|
||||
<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"
|
||||
style="stroke: currentcolor; stroke-width: 4; fill: currentcolor; fill-opacity: 0.5;"/>
|
||||
<circle v-else cx="50" cy="50" r="46"
|
||||
|
@ -1701,7 +2020,7 @@ export default {
|
|||
>{{ value.badge.name }}</a
|
||||
></h1>
|
||||
</div>
|
||||
<div class="r-course-detail-header-right">
|
||||
<div class="r-course-detail-header-right" v-if="!teachermode">
|
||||
<div class="r-completion-detail-header">
|
||||
{{ txt.completion['completion_'+completion] }}
|
||||
<i v-b-popover.hover :class="'fa fa-'+issued_icon+' r-completion-'+completion"
|
||||
|
@ -1727,7 +2046,12 @@ export default {
|
|||
v-if="value.badge.criteria"><li v-for="crit in value.badge.criteria"
|
||||
><span v-html='crit'></span></li></ul>
|
||||
<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-container>
|
||||
</b-modal>
|
||||
|
|
|
@ -172,13 +172,20 @@ export default {
|
|||
completion: {
|
||||
completion_completed: "completion_completed",
|
||||
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: {
|
||||
share_badge: "share_badge",
|
||||
dateissued: "dateissued",
|
||||
dateexpire: "dateexpire",
|
||||
badgeinfo: "badgeinfo",
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
@ -2152,14 +2159,6 @@ export default {
|
|||
};
|
||||
},
|
||||
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() {
|
||||
if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useItemConditions !== undefined){
|
||||
return this.plan.aggregation_info.useItemConditions;
|
||||
|
@ -2168,18 +2167,45 @@ export default {
|
|||
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);
|
||||
|
||||
configurationState(){
|
||||
if(this.hasGrades() || this.hasCompletions()) {
|
||||
return "t-configured-ok";
|
||||
} else {
|
||||
return "t-configured-alert";
|
||||
}
|
||||
}
|
||||
return list;
|
||||
},
|
||||
|
||||
configurationIcon(){
|
||||
if(this.hasGrades() || this.hasCompletions()) {
|
||||
return "check";
|
||||
} else {
|
||||
return "exclamation-circle";
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
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){
|
||||
call([{
|
||||
methodname: 'local_treestudyplan_include_grade',
|
||||
|
@ -2225,7 +2251,8 @@ export default {
|
|||
<b-card-body class="align-items-center">
|
||||
<a class="t-item-course-config"
|
||||
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"
|
||||
:id="'t-item-course-details-'+value.id"
|
||||
:href="'/course/view.php?id='+value.course.id"
|
||||
|
@ -2237,12 +2264,17 @@ export default {
|
|||
:id="'t-item-course-config-'+value.id"
|
||||
:title="value.course.displayname + ' - ' + value.course.fullname"
|
||||
ok-only
|
||||
size="lg"
|
||||
scrollable
|
||||
>
|
||||
<template #modal-header>
|
||||
<div>
|
||||
<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}}
|
||||
</div>
|
||||
<div class="r-course-detail-header-right">
|
||||
|
@ -2253,6 +2285,85 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<t-item-course-grades
|
||||
v-if='!!value.course.grades && value.course.grades.length > 0'
|
||||
v-model='value' :plan="plan"
|
||||
></t-item-course-grades>
|
||||
<t-item-course-completion
|
||||
v-if='!!value.course.completion'
|
||||
v-model='value.course.completion'
|
||||
:course='value.course'
|
||||
></t-item-course-completion>
|
||||
</b-modal>
|
||||
</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"
|
||||
|
@ -2286,11 +2397,108 @@ export default {
|
|||
</li>
|
||||
</ul>
|
||||
</b-form-group>
|
||||
</b-modal>
|
||||
</b-card>
|
||||
</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 *
|
||||
|
|
|
@ -50,10 +50,12 @@ class badgeinfo {
|
|||
"criteria" => new \external_multiple_structure(new \external_value(PARAM_RAW, 'criteria text'),'badge criteria',VALUE_OPTIONAL),
|
||||
"description"=> new \external_value(PARAM_TEXT, 'badge description'),
|
||||
"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);
|
||||
}
|
||||
|
||||
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);
|
||||
// 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,
|
||||
'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;
|
||||
}
|
||||
|
||||
|
@ -130,7 +139,7 @@ class badgeinfo {
|
|||
function count_issued(array $student_ids){
|
||||
$issuecount = 0;
|
||||
|
||||
foreach($student_ids as $uid){
|
||||
foreach($student_ids as $userid){
|
||||
if($this->badge->is_issued($userid)){
|
||||
$issuecount++;
|
||||
}
|
||||
|
|
|
@ -5,9 +5,6 @@ require_once($CFG->libdir.'/externallib.php');
|
|||
|
||||
use \grade_item;
|
||||
|
||||
// $gi->courseid,
|
||||
// $gi->itemmodule,
|
||||
// $gi->iteminstance
|
||||
class completionscanner
|
||||
{
|
||||
private static $mod_supported = [];
|
||||
|
@ -39,17 +36,19 @@ class completionscanner
|
|||
return self::$course_students[$courseid];
|
||||
}
|
||||
|
||||
public function __construct(\completion_criteria $crit){
|
||||
$this->course = $crit->course;
|
||||
public function __construct(\completion_criteria $crit,$course){
|
||||
$this->courseid = $course->id;
|
||||
$this->course = $course;
|
||||
$this->modinfo = get_fast_modinfo($course);
|
||||
$this->crit = $crit;
|
||||
|
||||
$this->completioninfo = new \completion_info($course);
|
||||
|
||||
// Find a related scanner if the type is an activity type
|
||||
if($crit->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY){
|
||||
// First find the course module
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$this->cm = $modinfo->get_cm($crit->moduleinstance);
|
||||
|
||||
$gi = grade_item::fetch(['itemtype' => 'mod', 'itemmodule' => $cm->mod, 'iteminstance' => $cm->id, 'courseid' => $course->id]);
|
||||
$this->cm = $this->modinfo->get_cm($crit->moduleinstance);
|
||||
$gi = grade_item::fetch(['itemtype' => 'mod', 'itemmodule' => $this->cm->modname, 'iteminstance' => $this->cm->instance, 'courseid' => $this->courseid]);
|
||||
if($gi !== false)
|
||||
{
|
||||
// Grade none items should not be relevant
|
||||
|
@ -57,38 +56,17 @@ class completionscanner
|
|||
if(($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE))
|
||||
{
|
||||
// If it's a relevant grade type, initialize a scanner if possible
|
||||
$this->gi = gi;
|
||||
$this->gi = $gi;
|
||||
if(self::supported($gi->itemmodule)) {
|
||||
$scannerclass = "\local_treestudyplan\\local\ungradedscanners\\{$gi->itemmodule}_scanner";
|
||||
$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){
|
||||
if(!array_key_exists($userid, $this->pending_cache)){
|
||||
if($this->scanner === null) {
|
||||
|
@ -112,12 +90,48 @@ class completionscanner
|
|||
}
|
||||
|
||||
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 [
|
||||
'ungraded' => $this->count_ungraded(),
|
||||
'completed' => $this->count_completed(),
|
||||
'completed_pass' => 0,
|
||||
'completed_fail' => 0,
|
||||
'students' => $this->count_students(),
|
||||
'ungraded' => $ungraded,
|
||||
'completed' => $completed,
|
||||
'completed_pass' => $completed_pass,
|
||||
'completed_fail' => $completed_fail,
|
||||
'students' => count($students),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ class corecompletioninfo {
|
|||
"requirement" => new \external_value(PARAM_RAW, 'requirement',VALUE_OPTIONAL),
|
||||
"status" => new \external_value(PARAM_RAW, 'status',VALUE_OPTIONAL),
|
||||
]),
|
||||
//"studentcompletion" => completionscanner::structure(VALUE_OPTIONAL),
|
||||
"progress" => completionscanner::structure(),
|
||||
], 'completion type',$value);
|
||||
}
|
||||
|
||||
|
@ -240,7 +240,7 @@ class corecompletioninfo {
|
|||
"type" => get_string('coursegrade', 'completion'),
|
||||
"criteria" => get_string('graderequired', 'completion'),
|
||||
// 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" => "",
|
||||
];
|
||||
}
|
||||
|
@ -274,13 +274,14 @@ class corecompletioninfo {
|
|||
];
|
||||
}
|
||||
|
||||
$scanner = new completionscanner($criteria,$this->course);
|
||||
|
||||
// only add the items list if we actually have items...
|
||||
$cinfo["items"][] = [
|
||||
"id" => $criteria->id,
|
||||
"title" => $criteria->get_title_detailed(),
|
||||
"details" => $details,
|
||||
//"studentcompletion" =>
|
||||
"progress" => $scanner->model(),
|
||||
];
|
||||
|
||||
}
|
||||
|
|
|
@ -208,7 +208,7 @@ class courseinfo {
|
|||
$gradables = gradeinfo::list_course_gradables($this->course,$studyitem);
|
||||
|
||||
foreach($gradables as $gradable) {
|
||||
$info['grades'][] = $gradable->editor_model();
|
||||
$info['grades'][] = $gradable->editor_model($studyitem);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -328,6 +328,7 @@ class courseservice extends \external_api
|
|||
return new \external_function_parameters( [
|
||||
"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),
|
||||
"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);
|
||||
}
|
||||
|
||||
public static function scan_completion_progress($criteriaid, $studyplanid)
|
||||
public static function scan_completion_progress($criteriaid, $studyplanid,$courseid)
|
||||
{
|
||||
global $DB;
|
||||
// Verify access to the study plan
|
||||
|
@ -344,11 +345,10 @@ class courseservice extends \external_api
|
|||
webservicehelper::require_capabilities(self::CAP_VIEW,$o->context());
|
||||
|
||||
$crit = \completion_criteria::fetch(["id" => $criteriaid]);
|
||||
$courseid = $crit->course;
|
||||
if(!$o->course_linked($courseid)){
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -194,7 +194,8 @@ class gradeinfo {
|
|||
"gradinglink" => $this->gradinglink,
|
||||
"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()){
|
||||
$model['grading'] = $this->gradingscanner->model();
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ class gradingscanner
|
|||
}
|
||||
|
||||
public function __construct(grade_item $gi){
|
||||
$this->courseid = $gi->courseid;
|
||||
$this->gi = $gi;
|
||||
if(self::supported($gi->itemmodule)) {
|
||||
$scannerclass = "\local_treestudyplan\\local\ungradedscanners\\{$gi->itemmodule}_scanner";
|
||||
|
@ -49,24 +50,6 @@ class gradingscanner
|
|||
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){
|
||||
if(!array_key_exists($userid, $this->pending_cache)){
|
||||
if($this->scanner === null) {
|
||||
|
@ -79,22 +62,113 @@ class gradingscanner
|
|||
return $this->pending_cache[$userid];
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function structure($value=VALUE_OPTIONAL){
|
||||
return new \external_single_structure([
|
||||
"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'),
|
||||
],"details about gradable submissions",$value);
|
||||
}
|
||||
|
||||
public function model(){
|
||||
return [
|
||||
'ungraded' => $this->count_ungraded(),
|
||||
'graded' => $this->count_graded(),
|
||||
'students' => $this->count_students(),
|
||||
];
|
||||
// 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 [
|
||||
'ungraded' => $ungraded,
|
||||
'completed' => $completed,
|
||||
'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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -218,6 +218,7 @@ class bistate_aggregator extends \local_treestudyplan\aggregator {
|
|||
}
|
||||
}
|
||||
else {
|
||||
$grade = $gradeitem->get_final($userid);
|
||||
// first determine if we have a grade_config for this scale or this maximum grade
|
||||
$finalgrade = $grade->finalgrade;
|
||||
$scale = $gradeinfo->getScale();
|
||||
|
@ -291,4 +292,5 @@ class bistate_aggregator extends \local_treestudyplan\aggregator {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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) {
|
||||
global $DB;
|
||||
$table = "local_treestudyplan_gradecfg";
|
||||
|
@ -169,6 +173,7 @@ class core_aggregator extends \local_treestudyplan\aggregator {
|
|||
}
|
||||
}
|
||||
else {
|
||||
$grade = $gradeitem->get_final($userid);
|
||||
// first determine if we have a grade_config for this scale or this maximum grade
|
||||
$finalgrade = $grade->finalgrade;
|
||||
$scale = $gradeinfo->getScale();
|
||||
|
|
|
@ -92,7 +92,6 @@ class tristate_aggregator extends \local_treestudyplan\aggregator {
|
|||
|
||||
public function grade_completion(gradeinfo $gradeinfo, $userid) {
|
||||
global $DB;
|
||||
$table = "local_treestudyplan_gradecfg";
|
||||
$gradeitem = $gradeinfo->getGradeitem();
|
||||
$grade = $gradeitem->get_final($userid);
|
||||
|
||||
|
@ -153,4 +152,6 @@ class tristate_aggregator extends \local_treestudyplan\aggregator {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -145,7 +145,10 @@ class studyitem {
|
|||
if($mode == "export"){
|
||||
$model['badge'] = $badgeinfo->name();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ class studyplan {
|
|||
private $id;
|
||||
private $aggregator;
|
||||
private $context = null; // Hold context object once retrieved
|
||||
private $linked_userids = null; // cache lookup of linked users (saves queries)
|
||||
|
||||
public function getAggregator(){
|
||||
return $this->aggregator;
|
||||
|
@ -392,9 +393,10 @@ class studyplan {
|
|||
* Retrieve the user id's of the users linked to this studyplan.
|
||||
* @return array of int (User Id)
|
||||
*/
|
||||
private function find_linked_userids(): array {
|
||||
public function find_linked_userids(): array {
|
||||
global $DB;
|
||||
|
||||
if($this->linked_userids === null){
|
||||
$uids = [];
|
||||
// First get directly linked userids
|
||||
$sql = "SELECT j.user_id FROM {local_treestudyplan_user} j
|
||||
|
@ -414,7 +416,9 @@ class studyplan {
|
|||
|
||||
$uids = array_merge($uids,$ulist);
|
||||
|
||||
return array_unique($uids);
|
||||
$this->linked_userids = array_unique($uids);
|
||||
}
|
||||
return $this->linked_userids;
|
||||
}
|
||||
|
||||
/** Check if this studyplan is linked to a particular user
|
||||
|
@ -738,6 +742,23 @@ class studyplan {
|
|||
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
|
||||
*/
|
||||
|
|
|
@ -724,6 +724,12 @@ tr.r-completion-category-header {
|
|||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.r-progress-circle-popup{
|
||||
position: relative;
|
||||
top: -0.15em;
|
||||
}
|
||||
|
||||
|
||||
.r-completion-detail-header {
|
||||
font-size: 20pt;
|
||||
}
|
||||
|
@ -760,7 +766,7 @@ tr.r-completion-category-header {
|
|||
}
|
||||
|
||||
table.r-item-course-grade-details td {
|
||||
padding-right: 10px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.r-course-detail-header-right {
|
||||
|
@ -833,12 +839,20 @@ table.r-item-course-grade-details td {
|
|||
color: #35f;
|
||||
}
|
||||
.r-graded-graded {
|
||||
color: #3a3;
|
||||
color: var(--success);
|
||||
}
|
||||
.r-graded-nogrades {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.t-configured-ok {
|
||||
color: var(--success);
|
||||
}
|
||||
.t-configured-alert {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
|
||||
.r-grading-bar {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
|
@ -865,17 +879,40 @@ table.r-item-course-grade-details td {
|
|||
}
|
||||
|
||||
.r-grading-bar-unsubmitted {
|
||||
background-color: #ddd;
|
||||
background-color: var(--light);
|
||||
}
|
||||
|
||||
.r-grading-bar-graded {
|
||||
background-color: #3a3;
|
||||
background-color: var(--success);
|
||||
}
|
||||
|
||||
.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 {
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
|
@ -918,20 +955,59 @@ table.r-item-course-grade-details td {
|
|||
}
|
||||
|
||||
.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.excellent,
|
||||
.s-required.allgraded {
|
||||
color: rgb(0, 126, 0);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.s-required.neutral {
|
||||
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 {
|
||||
display: flex;
|
||||
|
@ -939,7 +1015,6 @@ table.r-item-course-grade-details td {
|
|||
justify-content: left;
|
||||
}
|
||||
|
||||
|
||||
.m-buttonbar a,
|
||||
.m-buttonbar span,
|
||||
.m-buttonbar i {
|
||||
|
|
|
@ -109,6 +109,8 @@ $string['condition_any'] = 'One or more entries need to be completed';
|
|||
$string['courses'] = 'Courses';
|
||||
$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_incomplete'] = "Not started";
|
||||
$string['completion_pending'] = "Pending review";
|
||||
|
@ -116,6 +118,7 @@ $string['completion_progress'] = "In progress";
|
|||
$string['completion_completed'] = "Completed";
|
||||
$string['completion_good'] = "Good";
|
||||
$string['completion_excellent'] = "Excellent";
|
||||
$string['completion_passed'] = "Passed";
|
||||
|
||||
$string['cfg_grades'] = 'Configure grade & scale interpretation';
|
||||
$string['cfg_plans'] = 'Manage study plans';
|
||||
|
@ -151,7 +154,6 @@ $string['selected'] = 'Select';
|
|||
$string['name'] = 'Name';
|
||||
$string['context'] = 'Category';
|
||||
|
||||
|
||||
$string['error'] = "Error";
|
||||
$string['ungraded'] = 'Needs grading';
|
||||
$string['graded'] = 'Graded';
|
||||
|
@ -249,3 +251,4 @@ $string["share_badge"] = "Share badge";
|
|||
$string["dateissued"] = "Issued on";
|
||||
$string["dateexpire"] = "Expires on";
|
||||
$string["badgeinfo"] = "Badge details";
|
||||
$string["badgeissuedstats"] = "Issuing progress";
|
|
@ -111,7 +111,8 @@ $string['condition_any'] = 'Minimaal één onderdeel moet afgerond zijn';
|
|||
$string['courses'] = 'Cursussen';
|
||||
$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_incomplete'] = "Niet gestart";
|
||||
$string['completion_pending'] = "Wacht op beoordelen";
|
||||
|
@ -119,6 +120,7 @@ $string['completion_progress'] = "In ontwikkeling";
|
|||
$string['completion_completed'] = "Voltooid";
|
||||
$string['completion_good'] = "Goed";
|
||||
$string['completion_excellent'] = "Uitstekend";
|
||||
$string['completion_passed'] = "Behaald";
|
||||
|
||||
$string['cfg_grades'] = 'Configureer betekenis van beoordelingen en schalen';
|
||||
$string['cfg_plans'] = 'Studieplannen beheren';
|
||||
|
@ -252,3 +254,4 @@ $string["share_badge"] = "Bewijs delen";
|
|||
$string["dateissued"] = "Afgegeven op";
|
||||
$string["dateexpire"] = "Veloopt op";
|
||||
$string["badgeinfo"] = "Meer details";
|
||||
$string["badgeissuedstats"] = "Voortgang van uitgifte";
|
Loading…
Reference in New Issue
Block a user