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 {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">
{{text['completion_'+value.completion]}}
<i :class="'fa fa-'+completion_icon(value.completion)+' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
<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,10 +2020,10 @@ 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"
<i v-b-popover.hover :class="'fa fa-'+issued_icon+' r-completion-'+completion"
:title="txt.completion['completion_'+completion]"></i>
</div>
</div>
@ -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>

View File

@ -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">
@ -2252,45 +2284,221 @@ export default {
</div>
</div>
</template>
<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>
<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"
@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 *

View File

@ -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++;
}

View File

@ -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,37 +56,16 @@ 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)){
@ -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),
];
}

View File

@ -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(),
];
}

View File

@ -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 {

View File

@ -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();
}

View File

@ -181,7 +181,7 @@ class gradeinfo {
], 'referenced course information',$value);
}
public function editor_model(studyitem $studyitem=null) {
public function editor_model(studyitem $studyitem = null) {
$model = [
"id" => $this->id,
"cmid" => $this->cmid,
@ -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();
}

View File

@ -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);
],"details about gradable submissions",$value);
}
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 [
'ungraded' => $this->count_ungraded(),
'graded' => $this->count_graded(),
'students' => $this->count_students(),
'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;
}
}
}
}

View File

@ -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();
@ -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 scales, the configuration in the gradeconfig is leading
if($gradecfg && (isset($scale) || $gradeitem->gradepass == 0))
{
// if so, we need to know if the grade is
@ -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) {
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();
@ -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 scales, the configuration in the gradeconfig is leading
if($gradecfg && (isset($scale) || $gradeitem->gradepass == 0))
{
// if so, we need to know if the grade is

View File

@ -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 {
}
}
}

View File

@ -144,8 +144,11 @@ class studyitem {
$badgeinfo = new badgeinfo($badge);
if($mode == "export"){
$model['badge'] = $badgeinfo->name();
} else {
$model['badge'] = $badgeinfo->editor_model();
} else {
// 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 $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,29 +393,32 @@ 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;
$uids = [];
// First get directly linked userids
$sql = "SELECT j.user_id FROM {local_treestudyplan_user} j
WHERE j.studyplan_id = :planid";
$ulist = $DB->get_fieldset_sql($sql, ['planid' => $this->id]);
if($this->linked_userids === null){
$uids = [];
// First get directly linked userids
$sql = "SELECT j.user_id FROM {local_treestudyplan_user} j
WHERE j.studyplan_id = :planid";
$ulist = $DB->get_fieldset_sql($sql, ['planid' => $this->id]);
$uids = array_merge($uids,$ulist);
foreach($ulist as $uid){
$users[] = $DB->get_record("user",["id"=>$uid]);
$uids = array_merge($uids,$ulist);
foreach($ulist as $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);
}
// 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);
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
*/

View File

@ -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 {

View File

@ -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';
@ -248,4 +250,5 @@ $string["aggregation_any"] = "Complete one or more";
$string["share_badge"] = "Share badge";
$string["dateissued"] = "Issued 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['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';
@ -251,4 +253,5 @@ $string["aggregation_any"] = "Eén of meer behalen";
$string["share_badge"] = "Bewijs delen";
$string["dateissued"] = "Afgegeven op";
$string["dateexpire"] = "Veloopt op";
$string["badgeinfo"] = "Meer details";
$string["badgeinfo"] = "Meer details";
$string["badgeissuedstats"] = "Voortgang van uitgifte";