moodle_local_treestudyplan/amd/src/report-viewer-components.js

1739 lines
69 KiB
JavaScript
Raw Normal View History

/*eslint no-var: "error"*/
/*eslint no-console: "off"*/
/*eslint-disable no-trailing-spaces */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
import LeaderLine from './leaderline';
import {get_strings} from 'core/str';
import {load_strings} from './string-helper';
import {call} from 'core/ajax';
import notification from 'core/notification';
2023-05-20 17:36:00 +02:00
import {svgarcpath} from './svgarc';
//import {fixLineWrappers} from './studyplan-processor';
2023-05-20 17:36:00 +02:00
// Make π available as a constant
const π = Math.PI;
export default {
install(Vue/*,options*/){
let strings = load_strings({
invalid: {
error: 'error',
},
grading: {
ungraded: "ungraded",
graded: "graded",
allgraded: "allgraded",
unsubmitted: "unsubmitted",
nogrades: "nogrades",
unknown: "unknown",
},
completion: {
completion_completed: "completion_completed",
completion_incomplete: "completion_incomplete",
},
badge: {
share_badge: "share_badge",
dateissued: "dateissued",
dateexpire: "dateexpire",
badgeinfo: "badgeinfo",
}
});
/************************************
* *
2023-05-20 17:36:00 +02:00
* Treestudyplan Viewer components *
* *
************************************/
/**
* Check if element is visible
* @param {Object} elem The element to check
* @returns {boolean} True if visible
*/
function isVisible(elem){
return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
}
2023-05-20 17:36:00 +02:00
Vue.component('r-progress-circle',{
props: {
value: {
type: Number,
},
max: {
type: Number,
default: 100,
},
min: {
type: Number,
default: 0,
},
stroke: {
type: Number,
default: 0.2,
},
bgopacity: {
type: Number,
default: 0.2,
},
2023-05-20 18:25:22 +02:00
title: {
type: String,
default: "",
},
2023-05-20 17:36:00 +02:00
},
data() {
return {
selectedstudyplan: null,
};
},
computed: {
range() {
return this.max - this.min;
},
fraction(){
if(this.max - this.min == 0){
2023-05-20 18:25:22 +02:00
return 0;
// 0 size is always empty :)
2023-05-20 17:36:00 +02:00
} else {
return (this.value - this.min)/(this.max - this.min);
}
},
2023-05-20 18:25:22 +02:00
radius() {
return 50 - (50*this.stroke);
2023-05-20 17:36:00 +02:00
},
2023-05-20 18:25:22 +02:00
arcpath() {
let fraction = 0;
const r = 50 - (50*this.stroke);
2023-05-20 17:36:00 +02:00
if(this.max - this.min != 0){
fraction = (this.value - this.min)/(this.max - this.min);
}
const Δ = fraction * 2*π;
2023-05-20 18:25:22 +02:00
return svgarcpath([50,50],[r,r],[0,Δ], 1.5*π);
2023-05-20 17:36:00 +02:00
},
},
2023-05-20 18:25:22 +02:00
methods: {
},
2023-05-20 17:36:00 +02:00
template: `
2023-05-20 18:25:22 +02:00
<svg width="1em" height="1em" viewBox="0 0 100 100">
<title>{{title}}</title>
<circle v-if="fraction >= 1.0" cx="50" cy="50" :r="radius"
:style="'opacity: 1; stroke-width: '+ (stroke*100)+'; stroke: currentcolor; fill: none;'"/>
<g v-else>
<circle cx="50" cy="50" :r="radius"
2023-05-20 17:36:00 +02:00
:style="'opacity: ' + bgopacity + ';stroke-width: '+ (stroke*100)+'; stroke: currentcolor; fill: none;'"/>
2023-05-20 18:25:22 +02:00
<path :d="arcpath"
2023-05-20 17:36:00 +02:00
:style="'stroke-width: ' + (stroke*100) +'; stroke: currentcolor; fill: none;'"/>
2023-05-20 18:25:22 +02:00
</g>
</svg>
2023-05-20 17:36:00 +02:00
`,
});
Vue.component('r-report', {
props: {
value: {
type: Array,
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
data() {
return {
selectedstudyplan: null,
};
},
computed: {
displayedstudyplan(){
if(this.selectedstudyplan){
return this.selectedstudyplan;
} else if(this.value && this.value.length > 0){
return this.value[0];
} else {
return null;
}
}
},
methods: {
},
template: `
<div class='t-studyplan-container'>
<b-list-group horizontal
class='r-report-tabs'>
<b-list-group-item
v-for="(studyplan,planindex) in value"
:key="studyplan.id"
:active="displayedstudyplan && studyplan.id == displayedstudyplan.id"
button
@click="selectedstudyplan = studyplan"
>{{studyplan.name}}</b-list-group-item>
</b-list-group>
<r-studyplan v-model='displayedstudyplan' :guestmode='guestmode' :teachermode='teachermode'></r-studyplan>
</div>
`,
});
Vue.component('r-studyplan', {
props: {
value: {
type: Object,
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
data() {
return {
};
},
updated(){
this.$root.$emit('redrawLines');
},
mounted(){
this.$root.$emit('redrawLines');
},
computed: {
},
methods: {
},
template: `
<div class='r-studyplan-content'>
<template v-if="value && value.studylines">
<r-studyline v-for="(item,lineindex) in value.studylines"
:key="item.id"
:color='item.color'
:name='item.name'
:code='item.shortname'
:sequence='lineindex'
:numlines='value.studylines.length'
:guestmode='guestmode'
:teachermode='teachermode'
>
<template v-for="(n,index) in (value.slots+1)">
<r-studyline-slot
v-if="index > 0"
type='competency'
v-model="item.slots[index].competencies"
:key="'c-'+index"
:slotindex="index"
:lineid="item.id"
:plan="value"
:guestmode='guestmode'
:teachermode='teachermode'>
</r-studyline-slot>
<r-studyline-slot
type='filter'
v-model="item.slots[index].filters"
:key="'f-'+index"
:slotindex="index"
:lineid="item.id"
:plan="value"
:teachermode='teachermode'
>
</r-studyline-slot>
</template>
</r-studyline>
</template>
<div :id="'studyplan-linewrapper-'+(value?value.id:'null')" class='l-leaderline-linewrapper'></div>
</div>
`,
});
/*
* R-STUDYLINE
*/
Vue.component('r-studyline', {
props: ['color','name','code', 'slots','sequence','numlines','guestmode','teachermode'],
data() {
return {
};
},
computed: {
},
methods: {
},
template: `
<div :class="'r-studyline ' + ((sequence%2)?'odd':'even') +
(sequence==0?' first':'') + (sequence==numlines-1?' last':'')">
<div class="r-studyline-handle" :style="'background-color: ' + color"></div>
<div class="r-studyline-title" >
<abbr v-b-popover.hover.top :title="name">{{ code }}</abbr>
</div>
<slot></slot>
</div>
`,
});
Vue.component('r-studyline-slot', {
props: {
type : {
type: String,
default: 'competency',
},
slotindex : {
type: Number,
default: 0,
},
lineid : {
type: Number,
default: 0,
},
value: {
type: Array,
default(){ return [];},
},
plan: {
type: Object,
default(){ return null;}
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
computed: {
sorted(){
let copy = [...this.value];
copy.sort(function(a,b){
return a.layer - b.layer;
});
return copy;
}
},
data() {
return {
};
},
methods: {
},
template: `
<div :class="'r-studyline-slot '+type + ' r-studyline-slot-'+slotindex">
<r-item v-for="(item, index) in sorted"
:key="item.id" v-model="sorted[index]"
:plan="plan"
:guestmode='guestmode'
:teachermode='teachermode'></r-item></drag>
</div>
`,
});
Vue.component('r-item', {
props: {
value :{
type: Object,
default: function(){ return null;},
},
plan: {
type: Object,
default(){ return null;}
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
data() {
return {
lines: [],
};
},
methods: {
lineColor(){
if(this.teachermode){
return "#aaa";
}
else{
switch(this.value.completion){
default: // "incomplete"
return "#777";
case "failed":
return "#933";
case "progress":
return "#da3";
case "completed":
return "#383";
case "good":
return "#398";
case "excellent":
return "#36f";
}
}
},
redrawLine(conn){
let lineColor = this.lineColor();
// prepare lineinfo link or delete old line
let lineinfo = this.lines[conn.to_id];
if(lineinfo){
if(lineinfo.line){
if(lineinfo.lineElm ){
lineinfo.lineElm.parentNode.removeChild(lineinfo.lineElm);
lineinfo.lineElm = undefined;
} else {
lineinfo.line.remove();
}
lineinfo.line = undefined;
}
} else {
lineinfo = {};
this.lines[conn.to_id] = lineinfo;
}
// draw new line...
let start = document.getElementById('studyitem-'+conn.from_id);
let end= document.getElementById('studyitem-'+conn.to_id);
LeaderLine.positionByWindowResize = false;
if(start !== null && end !== null && isVisible(start) && isVisible(end)){
lineinfo.line = new LeaderLine(start,end,{
color: lineColor,
startSocket: 'right',
endSocket: 'left',
startSocketGravity: 75,
endSocketGravity: 75,
});
let elmWrapper = (this.plan.id >=0)?document.getElementById('studyplan-linewrapper-'+this.plan.id):null;
if(elmWrapper !== null){
let elmLine = document.querySelector('body > .leader-line:last-child');
elmWrapper.appendChild(elmLine);
lineinfo.lineElm = elmLine; // store line element so it can more easily be removed from the dom
}
setTimeout(function(){
if(lineinfo.line){
lineinfo.line.position();
}
},1);
}
},
redrawLines(){
for(let i in this.value.connections.out){
let conn = this.value.connections.out[i];
this.redrawLine(conn);
}
},
onWindowResize(){
this.redrawLines();
}
},
computed: {
hasConnectionsOut() {
return !(["finish",].includes(this.value.type));
},
hasConnectionsIn() {
return !(["start",].includes(this.value.type));
},
hasContext() {
return ['start','junction','finish'].includes(this.value.type);
}
},
created(){
},
mounted(){
// Initialize connection lines when mounting
this.redrawLines();
setTimeout(()=>{
this.redrawLines();
},50);
// Add resize event listener
window.addEventListener('resize',this.onWindowResize);
},
beforeDestroy(){
for(let i in this.value.connections.out){
let conn = this.value.connections.out[i];
let lineinfo = this.lines[conn.to_id];
if(lineinfo){
if(lineinfo.line){
if(lineinfo.lineElm ){
lineinfo.lineElm.parentNode.removeChild(lineinfo.lineElm);
lineinfo.lineElm = undefined;
} else {
lineinfo.line.remove();
}
lineinfo.line = undefined;
}
}
}
// Remove resize event listener
window.removeEventListener('resize',this.onWindowResize);
},
beforeUpdate(){
},
updated(){
if(!this.dummy) {
this.redrawLines();
}
},
template: `
<div class="r-item-base" :id="'studyitem-'+value.id" :data-x='value.type'>
<r-item-competency v-if="value.type == 'competency'"
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-competency>
<r-item-course v-if="value.type == 'course' && !teachermode" :plan="plan"
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-course>
<r-item-teachercourse v-if="value.type == 'course' && teachermode" :plan="plan"
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-teachercourse>
<r-item-junction v-if="value.type == 'junction'"
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-junction>
<r-item-start v-if="value.type == 'start'"
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-start>
<r-item-finish v-if="value.type == 'finish'"
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-finish>
<r-item-badge v-if="value.type == 'badge'"
v-model="value" :guestmode="guestmode" :teachermode="teachermode" ></r-item-badge>
<r-item-invalid v-if="value.type == 'invalid' && teachermode"
v-model="value" ></r-item-invalid>
</div>
`,
});
Vue.component('r-item-invalid', {
props: {
'value' :{
type: Object,
default: function(){ return null;},
},
},
data() {
return {
text: strings.invalid,
};
},
methods: {
},
template: `
<b-card no-body class="r-item-invalid">
<b-row no-gutters>
<b-col md="1">
<span class="r-timing-indicator timing-invalid"></span>
</b-col>
<b-col md="11">
<b-card-body class="align-items-center">
<i class="fa fa-exclamation"></i> {{ text.error }}
</b-card-body>
</b-col>
</b-row>
</b-card>
`,
});
//TAG: Item Course
Vue.component('r-item-course', {
props: {
value :{
type: Object,
default(){ return null;},
},
guestmode: {
type: Boolean,
default(){ return false;}
},
teachermode: {
type: Boolean,
default(){ return false;}
},
plan: {
type: Object,
default(){ return null;}
}
},
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",
},
};
},
computed: {
},
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++;
}
});
},
methods: {
completion_icon(completion) {
switch(completion){
default: // case "incomplete"
return "circle-o";
case "pending":
return "question-circle";
case "failed":
return "times-circle";
case "progress":
return "exclamation-circle";
case "completed":
return "check-circle";
case "good":
return "check-circle";
case "excellent":
return "check-circle";
}
},
},
template: `
<b-card no-body :class="'r-item-competency completion-'+value.completion">
<b-row no-gutters>
<b-col md="1">
<span
:title="text['coursetiming_'+value.course.timing]"
v-b-popover.hover.top="value.course.startdate+' - '+value.course.enddate"
:class="'r-timing-indicator timing-'+value.course.timing"></span>
</b-col>
<b-col md="11">
<b-card-body class="align-items-center">
2023-05-20 17:36:00 +02:00
<r-progress-circle v-if='value.course.completion'
:value='value.course.completion.progress'
:max='value.course.completion.count'
:min='0'
:class="'r-course-result r-completion-'+value.completion"
2023-05-20 18:25:22 +02:00
:title="text['completion_'+value.completion]"
2023-05-20 17:36:00 +02:00
></r-progress-circle>
<i v-else v-b-popover.top
:class="'r-course-result fa fa-'+completion_icon(value.completion)+
' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
<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>
</b-card-body>
</b-col>
</b-row>
<b-modal
:id="'r-item-course-details-'+value.id"
:title="value.course.displayname + ' - ' + value.course.fullname"
size="lg"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<h1><a :href="(!guestmode)?('/course/view.php?id='+value.course.id):undefined" target="_blank"
><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a></h1>
{{ value.course.context.path.join(" / ")}}
</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>
</div>
<div :class="'r-timing-'+value.course.timing">
{{text['coursetiming_'+value.course.timing]}}<br>
{{ value.course.startdate }} - {{ value.course.enddate }}
</div>
</div>
</template>
<r-item-studentgrades
v-if='!!value.course.grades && value.course.grades.length > 0'
v-model='value'
:guestmode='guestmode'></r-item-studentgrades>
<r-item-studentcompletion
v-if='!!value.course.completion'
v-model='value.course.completion'
:course='value.course'
:guestmode='guestmode'></r-item-studentcompletion>
</b-modal>
</b-card>
`,
});
//TAG: Selected activities dispaly
Vue.component('r-item-studentgrades',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
},
data() {
return {
};
},
computed: {
pendingsubmission(){
let result = false;
for(const ix in this.value.course.grades){
const g = this.value.course.grades[ix];
if(g.pendingsubmission){
result = true;
break;
}
}
return result;
},
useRequiredGrades() {
if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useRequiredGrades !== undefined){
return this.plan.aggregation_info.useRequiredGrades;
}
else {
return false;
}
},
},
methods: {
completion_icon(completion) {
switch(completion){
default: // case "incomplete"
return "circle-o";
case "pending":
return "question-circle";
case "failed":
return "times-circle";
case "progress":
return "exclamation-circle";
case "completed":
return "check-circle";
case "good":
return "check-circle";
case "excellent":
return "check-circle";
}
},
},
template: `
<table class="r-item-course-grade-details">
<tr v-for="g in value.course.grades">
<td><span class="r-activity-icon" :title="g.typename" v-html="g.icon"></span
><a
:href="(!guestmode)?(g.link):undefined" target="_blank" :title="g.name">{{g.name}}</a>
<abbr v-if="useRequiredGrades && g.required" :title="text.required_goal"
:class="'s-required ' + g.completion"
><i class='fa fa-asterisk' ></i
></abbr>
</td>
<td><span :class="' r-completion-'+g.completion">{{g.grade}}</span></td>
<td><i :class="'fa fa-'+completion_icon(g.completion)+' r-completion-'+g.completion"
:title="text['completion_'+g.completion]"></i>
<i v-if='g.pendingsubmission' :title="text['completion_pending']"
class="r-pendingsubmission fa fa-clock-o"></i></td>
<td v-if="g.feedback">
<a v-b-modal="'r-grade-feedback-'+g.id"
href="#"
>{{ text["view_feedback"]}}</a>
<b-modal
:id="'r-grade-feedback-'+g.id"
size="sm"
ok-only
centered
scrollable
>
<template #modal-header>
<h2><i class="fa fa-graduation-cap"></i>{{ value.course.fullname }}</h2><br>
<span class="r-activity-icon" :title="g.typename" v-html="g.icon"></span>{{g.name}}
</template>
<span v-html="g.feedback"></span>
</b-modal>
</td>
</tr>
</table>
`,
});
//TAG: Core completion version of student course info
Vue.component('r-item-studentcompletion',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
course: {
type: Object,
default: function(){ return {};},
},
},
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",
},
};
},
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: {
},
methods: {
completion_icon(completion) {
switch(completion){
case "progress":
return "exclamation-circle";
2023-05-19 22:12:18 +02:00
case "complete":
return "check-circle";
2023-05-19 22:12:18 +02:00
case "complete-pass":
return "check-circle";
2023-05-19 22:12:18 +02:00
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">
<template v-for='cgroup in value.conditions'>
<tr>
<th colspan='2'>{{cgroup.title}}</th>
2023-05-20 18:25:22 +02:00
<th><r-progress-circle
:value='cgroup.progress'
:max='cgroup.count'
:class="'r-completion-'+cgroup.status"
:title="text['completion_'+cgroup.status]"
></r-progress-circle></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>
<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>
</tr>
</template>
</table>
`,
});
//TODO: Implement corecompletion
Vue.component('r-item-teachercourse', {
props: {
value :{
type: Object,
default(){ return null;}
},
guestmode: {
type: Boolean,
default(){ return false;}
},
teachermode: {
type: Boolean,
default(){ return false;}
},
plan: {
type: Object,
default(){ return null;}
}
},
data() {
return {
text: {
select_conditions: "select_conditions",
select_grades: "select_grades",
coursetiming_past: "coursetiming_past",
coursetiming_present: "coursetiming_present",
coursetiming_future: "coursetiming_future",
grade_include: "grade_include",
grade_require: "grade_require",
required_goal: "required_goal",
},
txt: {
grading: strings.grading,
}
};
},
computed: {
course_grading_needed(){
return this.course_grading_state();
},
course_grading_icon(){
return this.determine_grading_icon(this.course_grading_state());
},
filtered_grades(){
return this.value.course.grades.filter(g => g.selected);
},
useRequiredGrades() {
if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useRequiredGrades !== undefined){
return this.plan.aggregation_info.useRequiredGrades;
}
else {
return false;
}
},
},
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++;
}
});
},
methods: {
course_grading_state(){
let ungraded = 0;
let unknown = 0;
let graded = 0;
let allgraded = 0;
const grades = this.filtered_grades;
if(!Array.isArray(grades) || grades == 0){
return 'nogrades';
}
for(const ix in grades){
const grade = grades[ix];
if(grade.grading){
if(Number(grade.grading.ungraded) > 0){
ungraded++;
}
else if(Number(grade.grading.graded) > 0) {
if(Number(grade.grading.graded) == Number(grade.grading.students)){
allgraded++;
} else {
graded++;
}
}
} else {
unknown = true;
}
}
if(ungraded > 0){
return 'ungraded';
} else if(unknown) {
return 'unknown';
} else if(graded){
return 'graded';
} else if(allgraded){
return 'allgraded';
} else {
return 'unsubmitted';
}
},
determine_grading_icon(gradingstate){
switch(gradingstate){
default: // "nogrades":
return "circle-o";
case "ungraded":
return "exclamation-circle";
case "unknown":
return "question-circle-o";
case "graded":
return "check";
case "allgraded":
return "check";
case "unsubmitted":
return "dot-circle-o";
}
},
},
template: `
<b-card no-body :class="'r-item-competency '+ (value.course.amteacher?'r-course-am-teacher':'')">
<b-row no-gutters>
<b-col md="1">
<span
:title="text['coursetiming_'+value.course.timing]"
v-b-popover.hover.top="value.course.startdate+' - '+value.course.enddate"
:class="'r-timing-indicator timing-'+value.course.timing"></span>
</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>
</b-card-body>
</b-col>
</b-row>
<b-modal
v-if="true"
:id="'r-item-course-details-'+value.id"
:title="value.course.displayname + ' - ' + value.course.fullname"
size="lg"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<h1><a :href="(!guestmode)?('/course/view.php?id='+value.course.id):undefined" target="_blank"
><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a>
<r-item-teacher-gradepicker v-model="value.course"
v-if="value.course.grades && value.course.grades.length > 0"
:useRequiredGrades="useRequiredGrades"
:plan="plan"
></r-item-teacher-gradepicker>
</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>
</div>
<div :class="'r-timing-'+value.course.timing">
{{ text['coursetiming_'+value.course.timing] }}<br>
{{ value.course.startdate }} - {{ value.course.enddate }}
</div>
</div>
</template>
<r-item-teachergrades
v-if='!!value.course.grades && value.course.grades.length > 0'
v-model='value.course'
:useRequiredGrades="useRequiredGrades"
></r-item-teachergrades>
<r-item-teachercompletion
v-if='!!value.course.completion'
v-model='value.course.completion'
:course='value.course'
></r-item-teachercompletion>
</b-modal>
</b-card>
`,
});
//TODO: Selecte activities to use in grade overview
Vue.component('r-item-teacher-gradepicker', {
props: {
value : {
type: Object,
default: function(){ return {};},
},
useRequiredGrades: {
type: Boolean,
default(){ return null;}
}
},
data() {
return {
};
},
computed: {
},
methods: {
},
template: `
<a v-if="value.canselectgradables" href='#'
v-b-modal="'r-item-course-config-'+value.id"
@click.prevent.stop=''
><i class='fa fa-cog'></i>
<b-modal v-if='value.canselectgradables'
:id="'r-item-course-config-'+value.id"
:title="value.displayname + ' - ' + value.fullname"
ok-only
scrollable
>
<template #modal-header>
<div>
<h1><a :href="'/course/view.php?id='+value.id" target="_blank"
><i class="fa fa-graduation-cap"></i> {{ value.fullname }}</a></h1>
{{ value.course.context.path.join(" / ")}} / {{value.displayname}}
</div>
<div class="r-course-detail-header-right">
<div :class="'r-timing-'+value.timing">
{{text['coursetiming_'+value.timing]}}<br>
{{ value.startdate }} - {{ value.enddate }}
</div>
</div>
</template>
<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.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><a
:href="g.link" target="_blank">{{g.name}}</a>
<s-edit-mod
:title="value.fullname"
@saved="(fd) => g.name = fd.get('name')"
v-if="g.cmid > 0"
:cmid="g.cmid"
:coursectxid="value.ctxid"
genericonly></s-edit-mod>
</li>
</ul>
</b-form-group>
</b-modal></a>
`,
});
//TODO: Selected activities dispaly
Vue.component('r-item-teachergrades',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
useRequiredGrades: {
type: Boolean,
default: false,
},
},
data() {
return {
txt: {
grading: strings.grading,
}
};
},
computed: {
pendingsubmission(){
let result = false;
for(const ix in this.value.grades){
const g = this.value.grades[ix];
if(g.pendingsubmission){
result = true;
break;
}
}
return result;
},
filtered_grades(){
return this.value.grades.filter(g => g.selected);
},
},
methods: {
determine_grading_icon(gradingstate){
switch(gradingstate){
default: // "nogrades":
return "circle-o";
case "ungraded":
return "exclamation-circle";
case "unknown":
return "question-circle-o";
case "graded":
return "check";
case "allgraded":
return "check";
case "unsubmitted":
return "dot-circle-o";
}
},
grading_icon(grade){
return this.determine_grading_icon(this.is_grading_needed(grade));
},
is_grading_needed(grade){
if(grade.grading){
if(grade.grading.ungraded){
return 'ungraded';
}
else if(grade.grading.graded){
if(Number(grade.grading.graded) == Number(grade.grading.students)){
return 'allgraded';
}
else {
return 'graded';
}
}
else {
return 'unsubmitted';
}
} else {
return 'unknown';
}
},
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);
},
},
template: `
<div>
<table class="r-item-course-grade-details">
<tr v-for="g in filtered_grades">
<td><span class="r-activity-icon" :title="g.typename" v-html="g.icon"></span
><a
:href="g.gradinglink"
target="_blank" :title="g.name">{{g.name}}</a>
<s-edit-mod
:title="value.fullname"
@saved="(fd) => g.name = fd.get('name')"
v-if="g.cmid > 0"
:cmid="g.cmid"
:coursectxid="value.ctxid"
genericonly></s-edit-mod>
<abbr v-if="useRequiredGrades && g.required" :title="text.required_goal"
:class="'s-required ' + is_grading_needed(g)"
><i class='fa fa-asterisk' ></i
></abbr>
</td>
<td v-if='g.grading'
><i :class="'r-course-grading fa fa-'+grading_icon(g)+' r-graded-'+is_grading_needed(g)"
: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>
</td>
</tr>
</table>
</div>
`,
});
//TODO: Core completion version of student course info
Vue.component('r-item-teachercompletion',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
course: {
type: Object,
default: function(){ return {};},
},
},
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",
},
};
},
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: {
},
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">
<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>
</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>
<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>
</tr>
</template>
</table>
`,
});
Vue.component('r-grading-bar',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
width: {
type: Number,
default: 150,
},
height: {
type: Number,
default: 15,
}
},
data() {
return {
text: strings.grading,
};
},
computed: {
width_unsubmitted() {
return this.width * this.fraction_unsubmitted();
},
width_graded() {
return this.width * this.fraction_graded();
},
width_ungraded() {
return this.width * this.fraction_ungraded();
},
count_unsubmitted(){
return (this.value.students - this.value.graded - this.value.ungraded);
}
},
methods: {
fraction_unsubmitted() {
if(this.value.students > 0){
return 1 - ((this.value.graded + this.value.ungraded) / this.value.students);
} else {
return 1;
}
},
fraction_graded() {
if(this.value.students > 0){
return this.value.graded / 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-grading-bar-ungraded'
:title="text.ungraded + ' (' + this.value.ungraded + ')'" v-b-popover.hover.top
></span
><span :style="{height: height+'px', width: width_graded+'px'}"
class='r-grading-bar-segment r-grading-bar-graded'
:title="text.graded+ ' (' + this.value.graded + ')'" v-b-popover.hover.top
></span
><span :style="{height: height+'px', width: width_unsubmitted+'px'}"
class='r-grading-bar-segment r-grading-bar-unsubmitted'
:title="text.unsubmitted + ' (' + count_unsubmitted + ')'" v-b-popover.hover.top
></span
></span>
`,
});
Vue.component('r-item-junction',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
data() {
return {
};
},
computed: {
completion(){
if(this.value.completion){
return this.value.completion;
} else {
return "incomplete";
}
}
},
methods: {
},
template: `
<div :class="'r-item-junction r-item-filter completion-'+completion">
<i v-if="value.completion=='incomplete'" class="fa fa-circle-o"></i>
<i v-else-if="value.completion=='failed'" class="fa fa-times-circle"></i>
<i v-else-if="value.completion=='progress'" class="fa fa-exclamation-circle"></i>
<i v-else class="fa fa-check-circle"></i>
</div>
`,
});
Vue.component('r-item-finish',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
data() {
return {
};
},
computed: {
completion(){
if(this.value.completion){
return this.value.completion;
} else {
return "incomplete";
}
}
},
methods: {
},
template: `
<div :class="'r-item-finish r-item-filter completion-'+completion">
<i class="fa fa-stop-circle"></i>
</div>
`,
});
Vue.component('r-item-start',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
data() {
return {
};
},
computed: {
completion(){
if(this.value.completion){
return this.value.completion;
} else {
return "incomplete";
}
}
},
created(){
},
methods: {
},
template: `
<div :class="'r-item-start r-item-filter completion-'+completion">
<i class="fa fa-play-circle"></i>
</div>
`,
});
Vue.component('r-item-badge',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
data() {
return {
txt: strings
};
},
computed: {
completion() {
return this.value.badge.issued?"completed":"incomplete";
},
issued_icon(){
switch(this.value.badge.issued){
default: // "nogrades":
return "circle-o";
case true:
return "check";
}
},
},
methods: {
},
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;"/>
<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"
stroke-dasharray="6 9"
style="stroke: #999; stroke-width: 6; fill: #ddd; fill-opacity: 0.8;"/>
<image class="badge-image" clip-path="circle() fill-box"
:href="value.badge.imageurl" x="12" y="12" width="76" height="76"
:style="(value.badge.issued||teachermode)?'':'opacity: 0.4;'" />
</svg></a>
<b-modal
:id="'r-item-badge-details-'+value.id"
:title="value.badge.name"
size="lg"
ok-only
centered
scrollable
>
<template #modal-header>
<div>
<h1><i class="fa fa-certificate"></i>
<a :href="(!guestmode)?(value.badge.infolink):undefined" target="_blank"
>{{ value.badge.name }}</a
></h1>
</div>
<div class="r-course-detail-header-right">
<div class="r-completion-detail-header">
{{ txt.completion['completion_'+completion] }}
<i v-b-popover.hover :class="'fa fa-'+issued_icon+' r-completion-'+completion"
:title="txt.completion['completion_'+completion]"></i>
</div>
</div>
</template>
<b-container fluid>
<b-row><b-col cols="3">
<img :src="value.badge.imageurl"/>
</b-col><b-col cols="9">
<p>{{value.badge.description}}</p>
<ul v-if="value.badge.issued" class="list-unstyled pt-1 mb-1 border-grey border-top">
<li><strong><i class="fa fa-calendar-check-o r-completion-complete-pass"></i>
{{txt.badge.dateissued}}:</strong> {{ value.badge.dateissued }}</li>
<li v-if='value.badge.dateexpired'
><strong><i class="fa fa-calendar-times-o r-completion-complete"></i>
{{txt.badge.dateexpired}}:</strong> {{ value.badge.dateexpired }}</li>
<li><strong><i class="fa fa-share-alt r-completion-complete-pass"></i>
<a href="value.badge.issuedlink">{{txt.badge.share_badge}}</a></strong> </li>
</ul>
<ul class="list-unstyled w-100 border-grey border-top border-bottom pt-1 pb-1 mb-1"
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>
</b-col></b-row>
</b-container>
</b-modal>
</div>
`,
});
},
};